Now weâve worked with a list of data and single entities with the Firebaseâs realtime database to create an admin dashboard in the previous sections. In this section, I want to introduce a new entity to demonstrate a business-related feature for a Firebase in React application, a message entity that lets you create a chat application. Weâll cover how to interact with Firebaseâs realtime database; specifically, how to structure data, work with lists of data, and how to create, update, and remove data. Also, you will see how ordering and pagination works with Firebase. In the end, itâs up to you to decide whether your application should become a chat application with a message entity or a book application with a book entity in the database. The message entity is only there as example.
Our Firebase class is the glue between our React application and the Firebase API. We instantiate it once, and then pass it to our React application via Reactâs Context API. Then, we can define all APIs to connect both worlds in the Firebase class. We completed it earlier for the authentication API and the user management. Next, letâs introduce the API for the new message entity.
Messages are readable and writeable on two API endpoints: messages and messages/:messageId. You will retrieve a list of messages and create a message with the messages reference, but you will edit and remove messages with the messages/:messageId reference.
If you want to be more specific, put more informative class methods for the message API in your Firebase class. For instance, there could be one class method for creating, updating, and removing a message. We will keep it general, however, and perform the specifics in the React components.
The HomePage component might be the best place to add the chat feature with messages, which is only accessible by authenticated users due to authorization. Letâs add a Message component that has access to the Firebase instance:
The Messages component has a local state for a loading indicator and the list of messages. In the lifecycle methods of the component, you can initialize (and remove) listeners to get messages from the Firebase database in realtime. When messages change (create, update, remove), the callback function in the listener is triggered and Firebase provides a snapshot of the data.
The new MessageList and MessageItem components only render the message content:
If you run the application, the loading indicator disappears after a few seconds when the Firebase realtime database listener is called for the first time. Every other time the loading indicator isnât shown, because it is only true when the component mounts and the first message fetching starts.
It could be that there are no messages yet, which is the case for this application since we didnât use the message API to create a message yet. Weâre only showing the messages for now. To show conditional feedback to users, we need to know if the list of messages is empty (see constructor), if the message API didnât return any messages and the local state is changed from an empty array to null:
Lastly, you need to convert the messages from the snapshot object to a list of items. Since Firebase comes with its own internal representation of data, you need to transform the data as before for the list of users on the admin page:
Since you have no messages, nothing shows up. Creating chat messages is our next task.
We were able to get all messages from the Firebase realtime database. Itâs even updated for us using the Firebase listener on a reference with the on and not once method. Next, letâs implement a React form that lets us create a message entity in the Firebase realtime database:
Next, add the new initial state for the component to keep track of the text property for a new message and its two new class methods to update the text in an input field element and create the actual message with Firebase:
We can use the push method on a Firebase reference to create a new entity in this list of entities, though we donât want to create a message just yet. One piece is missing for associating messages to users, which needs to be implemented before we create messages.
If you look closer at the MessageItem component, you can see that a message not only has a text , but also a userId that can be used to associate the message to a user. Letâs use the authenticated user from our React Context to store the user identifier in a new message. First, add the Consumer component and add the identifier for the authenticated user in the class method call that creates the message:
Next, use the authenticated user to associate the user identifier to the message. It makes sense to use the authenticated user, because this is the person authorized to write messages:
Now go ahead and create a message. Since we only can access this page as an authenticated user due to authorization, we know that each message that is written here will be associated to a user identifier. After you have created a message, the realtime feature of the Firebase database makes sure that the message will show up in our rendered list.
So far, we have chosen to keep the footprint of a user entity within a message as little as possible. There is only an user identifier which associates the message to a user. Generally speaking itâs good to structure data in your database this way, because it avoids plenty of pitfalls. For instance, letâs imagine you would associate the whole user entity to a message and not only the identifier. Then every time a user entity changes in the database, you would have to change the message entity with the user as well. Thatâs a common problem when not following the principal of the single source of truth when designing your database models. In our case, we are associating entities with each other only by their identifiers instead, whereas each entity in the database is the single source of truth without any duplications.
Another thing we decided earlier is giving the messages their dedicated API reference with messages . In another scenario, it could have been users/:userId/messages to associate users directly with the message via the reference. But doing it this way, we would have to fetch messages from multiple API endpoints in the end to show a nice chatroom as we do it right now.
We are reading a list of messages and created our first message. What about the other two missing functionalities to remove and edit a message. Letâs continue with removing a message. Pass through a new class method that will remove a message eventually:
The MessageList component in between just pass the function through to the MessageItem component:
Finally it can be used in the MessageItem component. When clicking the button, we will pass the message identifier to the function. Then in our parent component that has access to Firebase we can remove the message associated with the identifier.
Last, implement the class method that deletes the item from the list. Since we have access to the identifier of the message, we can use the reference of a single message to remove it.
Deleting a message works, and you can also make Firebase instance available to the MessageItem component and delete the message there right away. The real-time connection to the Firebase database in the Messages component would still be called to remove the message, which keeps the displayed messages in sync. However, aggregating all the business logic in one place, in this case the Messages component, makes sense for a better maintainability and predictability of the application. Only a few components have the more complex logic whereas the other components are just there to render the content.
Itâs abnormal to update a message in a chat application, but weâll implement this feature anyway. Eventually, weâll give other users feedback that a message was edited. That way, all statements made in the chat keep their integrity. Again, implement the class method first, which we will fill with details later, and pass it down to the MessageList component:
Again, the MessageList component just passes it through to the MessageItem component:
Editing a message involves a few more rendered elements, business logic, and state in the MessageItem component. Thatâs why we refactor it to a class component:
Next, weâll keep track of the mode of the component, which tells us if weâre showing the text of a message or editing it. Also, if we are editing a message, we need to track the value of the input field element. As initial state, it receives the text of the message entity which makes sense if we only want to edit a typo in the message:
Now, letâs implement three class methods, the first of which is a class method for toggling the mode from edit to preview and back. If this mode is toggled, we always fill in the text of the message as a value for the input field element to improve the user experience when the mode is toggled:
Second, a class method for updating the value in the input field:
And third, a class method to submit the final value to the parent component to edit the message:
Later, we will see why we send the message with the edited text. Next, letâs implement the render method of the MessageItem component. Make sure that the button to delete a message is not displayed in edit mode:
Next add âEditâ and âResetâ buttons to toggle between preview and edit mode. Depending on the edit mode, the correct button is displayed, and a âSaveâ button is shown in edit mode to save the edited text:
Last, we need the input field element to edit the text. It is only displayed in edit mode. If we are not in edit mode, the actual text of the message is shown:
Now we can edit the text in edit mode, and we can also reset the whole thing using a button. If we save the edited text, the text and the message will be sent through the MessageList component to the Messages component, where the message can be identified by id to be edited with the text property. Using the spread operator, all other properties of the message entity are kept as before:
If we set only the new text for the message, all other properties (e.g. userId) would be lost. Also we can add createdAt and editedAt dates. The second date gives users feedback that someone changed a chat message:
When using Firebase, itâs best not to choose the date yourself, but let Firebase choose it depending on their internal mechanics. The server value constants from Firebase can be made available in the Firebase class:
In the MessageItem component, give users feedback that shows when a message was edited:
As before, we could have used Firebase directly in the MessageItem component. Itâs also good to keep the MessageItem component encapsulated with its own business logic. Only the message itself and the other functions to alter the message are passed from above to the component, and only the Messages component speaks to the outside world (e.g. Firebase).
You have implemented the popular CRUD operations: create, read, update, delete, which is everything you need to manage the new message entity in your Firebase database. Also, you have learned how to assign dates to your Firebase entities, and how to listen for real-time updates when a message has been added, edited or removed.
So far, every user can edit and remove messages. Letâs change this by giving only owner of messages the power to perform these operations within the UI. Therefore, we need the authenticated user in the MessageItem component. Since we already have the authenticated user in the Messages component, letâs pass it down to the MessageList component:
And from there down to the MessageItem component:
Now in your MessageItem component, you can secure the buttons to edit and remove messages by comparing the messageâs userId with the authenticated userâs id:
Thatâs it for only enabling users who are owners of a message to edit and delete the message in the UI. You will see later how you can secure the Firebase API endpoint as well to not allow users to edit/delete entities; otherwise it would still be possible to alter the source code in the browser to show the buttons for deleting and editing messages even though the user has no permission to perform it.
Currently, messages are retrieved in no specific order from the Firebase realtime database, which means they would be in the order of their creation. This is appropriate for a chat application, but letâs make this behavior more explicit by ordering them by the createdAt date property since we have introduced this earlier:
Pass the property that should be used to retrieved the list as ordered list from the Firebase realtime database. By default Firebase is ordering the items in ascending direction. To reverse the order, add a reverse() after transforming the list of messages from an object to an array.
You might see a warning about indexing data in Firebaseâs realtime database, because weâre fetching data in a specific order, and Firebase uses the property createdAt to fetch it more efficiently. You can index messages using the createdAt property to give Firebase a performance boost when fetching the messages with this ordering. Head over to your projectâs Firebase dashboard, open the âDatabaseâ tab, and click the âRulesâ tab. You can add the indexing of the data there:
The warning should no longer appear, and Firebase became faster at retrieving messages by creation date. Every time you see the warning popping up, head over to your rules and index your Firebase entities. It makes your Firebase database operations faster.
Next is the ordering feature, and we will paginate the list from the Firebase realtime database as well. You can pass the Firebase API a limit method with an integer to specify how many items you are interested in:
Limiting the items is half the task for enabling pagination for our chat application. We also need to move the limit to the local state of the component to adjust it later with user interactions to fetch more than five items:
Move this functionality outside of the lifecycle method to make it reusable for other user interaction, and to use it outside of when the component mounts:
Next, letâs add a button to indicate that we are interested in more than five items:
The button uses a new class method that increases the limit by five again. Afterward, using the second argument of Reactâs setState method, we can renew the Firebase listener with the new limit from the local state. We know that the second function in this React-specific method runs when the asynchronous state update happens, at which point the listener can use the correct limit from the local state.
Everything you have learned in this chapter should make you proficient with structured and list data in Firebaseâs realtime database. You have learned how to get, create, update and remove entities in a Firebase realtime database, and how to keep a synchronized connection to Firebase and always show the latest entities. Finally, we went through the pagination and ordering features offered by Firebase.