Introducing react-use-database: client-side relational data just got easier
By: Austin Malerba
Tags: Tech, JavaScript, Web Development, Web Design, React

An issue that’s plagued React apps everywhere: after updating a resource on the server, how do I update my UI state to reflect the changes?

Typically there are two solutions:

Use the app’s existing data-fetching mechanisms to reload the data (essentially refresh the page on the user’s behalf) Take the response data from the resource update and carefully update pertinent bits of state throughout the app

The first solution is easy but inefficient. It’s simple code, but it’s not particularly performant and it’s hard on the network.

The second solution is efficient but complex. It minimizes network calls, but it requires more logic and usually makes it harder to navigate the code.

Of these two implementations, I would consider the latter superior. The problem with the latter solution is that, until now, there’s been no concise, opinionated solution to facilitate relational state updates in React apps.

redux-orm had the right idea, but I found it still required a lot of boilerplate. Prior to implementing this library with hooks, I used to implement relational state management with Redux/Normalizr. It worked well, but there was too much boilerplate, and trying to onboard a new-hire to the idea was daunting.

Redux and Normalizr are an incredible pairing, Dan Abramov was really onto something with his creation of each (He’s even done an Egghead tutorial series in which he describes almost the exact design pattern I’ve implemented in this library).

The problem with Redux is that it lacks opinionation and it can be overwhelmingly verbose. It’s vulnerable to anti-patterns and there’s nothing inherent to its API to enforce its proper use. When used improperly, redux can become more of a burden than an asset.

Introducing react-use-database

With react-use-database you get the best of both worlds. It forces you to think about your client-side data in the context of a queryable database. It gives you two global data stores: an entity store and a query store.

The entity store contains all of your model data. It’s just a big json blob that is the source of truth for any database entity that you have defined via Normalizr’s notion of schemas (eg UserSchema , TodoSchema , CommentSchema , etc. (Note: Normalizr is a peer dependency of react-use-database, you should familiarize yourself with it).

But, what good is a database without a way to pull data from it? That’s where the query store comes in. A query is comprised of a schema and a value. The query {schema: UserSchema, value: 32} would return you a user whose id is 32. The query {schema: [UserSchema], value: [32, 33, 34]} would return you an array of three users. By tracking and updating your entities and queries in their respective stores, every component can have access to the latest data with almost no additional code.

Hold on, what the heck is Normalizr?

Normalizr lets us extract model data from JSON structures through schemas. Normalizr provides two inverse functions, normalize and denormalize . Say we received a list of todos from the server.

We could define Normalizr schemas to extract model data from this response.

We can identify two model types within this response data: User and Todo.

Normalizr lets us define these models as Entity schemas.

If we now normalize the todosResponse we can pull out the model instances from the data and replace them with ids.

The result field contains our depopulated data. The entities field contains our indexed model data. We can now denormalize (repopulate) these values to get back our original response.

This is useful for three primary reasons:

We can have a single source of truth for our model instances and pass ids around our application as opposed to entire objects. This greatly simplifies state updates after CRUD operations because we only have one place to update state. We can deep merge new entities onto old entities because entities are in a normalized form. This allows us to keep our entity store up to date with ease. We can look up an object by id in O(1) instead of O(n) because entities are represented as a map as opposed to an array.

Normalizr’s denormalize function powers react-use-database under the hood to provide querying of the centralized entity store.

Show me the library already

Too much talking, right? Let’s go through some example implementations illustrating how you’d pipe data from API to database to component using react-use-database.

As you’re walking through the following code, you may find it helpful to look through the API reference.

Creating the Database

Once we wrap our app in the DatabaseProvider we’re good to go. We can now use our database hook in any component to query the database.

Fetching Todos

Boom, now you can fetchTodos from anywhere via the useNormalizedApi hook and your TodosComponent will re-render with the latest list of todos. After we normalize the todos response, we call db.mergeEntities(entities) to deep merge the latest todo data into the entity store. And remember that ALL_TODOS query we defined while creating the database? We gave it a default value, but db.updateStoredQuery allows us to change its value to the latest list of all todo ids received from the server. Both db.mergeEntities and db.updateStoredQuery trigger re-renders on components consuming useDB .

Updating a Todo

This has to be my favorite one. You don’t even need to update any queries. You just take the updated todo from the response body, normalize it, and deep merge it into the entity store and then your TodosComponent re-renders with the updated data. Virtually zero code is necessary to update state across your app following PUT’s and PATCH’s.

Creating a Todo

You might be seeing a pattern here. Updating the database is almost always as simple as normalizing data and passing the entities object to mergeEntities . This deep merges the entities patch into the entity store immutably to produce the next database state. Once our todo is created, we need to add its id to our ALL_TODOS query. Once this is done, our TodosComponent will re-render with the new todo.

Deleting a Todo

To delete a todo, we can add a soft delete indicator to the todo in the database and we can update any relevant queries to omit the deleted todo id. Here’s a case where we probably don’t want to just normalize the data from the response and call mergeEntities on it.

Optimistic Updates

We also can easily perform optimistic updates by normalizing and merging entities before the API call has finished. And then if the API call comes back with an error level status code, we can merge the original todo back into the entity store to revert the update.

Other Queries

Because a query is just a schema and value, we can create our own queries whose state is not tracked by the query store. For example, if we received a todo id as a url parameter, we could do something like the above.

This has been a brief introduction to react-use-database. If it tickles your fancy, I recommend you check out the live demo and docs. Also, to get you up and running quickly, here’s a react-use-database playground you can fork on CodeSandbox to start playing with the library. Enjoy, folks!