Managing Client and Server Data - React

6 Minutes

charlie beemy profile picture
By
  • code
  • data
  • intermediate
  • react
Database Updates pofo

Sections

This article assumes you know the basics of Javascript, how Promises work, and have a working knowledge of full-stack applications.

Dealing with data is the foundation of any interactive application. Handling fetches and updates, although foundational to how the web works, can be a challenge to get right. Here are some common patterns that will help you better manage client and server data.

The Basics

Async Await

When we need data from a server, this request will take some time to resolve. It's not instantaneous like synchronous code. Because of this, we need to await the data before continuing with code that may depend on that data:

Global Data

Often times in an application, this data is reused amongst different components or even pages in your application. How would you share this data amongst the different parts of your application? You could just fetch the data again, but this is pretty wasteful since the data is already on the user's device. This is where a global state management tool comes into play. Instead of isolating data in a local component, we can instead set data into a global context. This can be managed either through React's built-in useContext function or through a more verbose library. My preference is react-query, which is an all-in-one data management solution:

Granular Updates

Oftentimes, you are not going to be updating your entire database schema with each update. When sending data to the server, make sure to only send data that is being changed/updated!

Summary:

  1. Await the data before updating state

  2. Try not to over fetch data and use a global context if needed

  3. Only send data that is needed to the server

Updates

One concern with updates is whether or not the update is successful or not. When I click or type something into my device, I expect it to change immediately. However, actually making this happen has a couple of layers to it. Syncing user inputted data with the backend database and the client application can be a challenge. Depending on the type of application, there are 2 main kinds of updates you can perform to make this happen:

Regular Updates

Updating data, especially in an editor-like environment, will result in consistent updates from the user. Depending on the type of application, there may already be a natural state that stores user inputted data. A common example of this would be the beemy editor or Google Docs. When we are writing into any content editable element, the innerHTML of the website content is changed, therefore no manual state change is required.

Because of this, we are not concerned with updating state from user input. But what about if the update fails?  Because updates are performed on a regular basis (in the case of the beemy editor, every 0.2s after you finish typing something), a failing update is not a big deal because new data is sent over a regular interval. So if one update fails, it is very unlikely that the next update will fail, or the next one... and so on.

Optimistic Updates

Updates that don't have a natural built-in state should be optimistic. This means that the change is reflected immediately on the UI even though the backend data is still in the process of being updated. react-query has a built-in mechanism for optimistic updates that beemy leverages. In the event that the update does fail, we roll back the update on the UI to what it was before the update. Make sure to notify the user if the update fails! Optimistic updates are useful because a vast majority of the time, updates will not fail.

A common pattern so far is that we can perform updates on the client before contacting the server. This is because we already have the needed data! 

Putting it Together

Oftentimes, the application will need to make use of both regular updates and optimistic updates. We can combine the 2 into one function:

Accessing previous data in a lower component

Oftentimes, you might need additional data that is not passed down the component tree to perform an update. Instead of passing down this data through props, you can do the following as an alternative:

  1. Declare all your queries and updater functions on the highest component in the tree that needs access to the data. The majority of times, this will be done on the page level.

  2. Adjust the updateResource function to allow a function to be passed as input that passes the needed data as a parameter to an updater function. This is similar to React's setState function where it accepts both raw data and an updater function. It would look something like this:

Queuing Fetches

For complicated updates or queries, there may be times when you will need to make multiple queries in the same function. Instead of individually awaiting the result of each of these queries, make use of Javascript's built-in Promise.all function to fetch all the data simultaneously. If you had 3 queries you needed to perform, Promise.all can resolve the 3 queries in 1/3 of the time in comparison to if you individually await each query. It would look something like this:

This is great, but oftentimes there is some preparation needed before we can fetch all the data. We usually need to parse some existing data structure in order to get the query variables. Take the following simplified example:

We have a list ids that we need to fetch. How would we go about fetching this data at once? Well, it would look something like this:

Managing ids

Whenever you create a resource, you need to make a new id for that resource. This poses an important question:

Do you create that id on the client or server?

I don't really have an opinion on this, but I'll offer one challenge I faced when creating ids on the server. If you do this, it makes it very difficult or impossible to perform the create action optimistically. This is because you need the id to sync the client and server data. You could create a dummy id until the resource is finished creating, but that's complicated. I would argue that you want to await the creation of the resource because, in the event that the user performs an update before the resource is created, that update would obviously fail.

Made with