Why we had to move away from React Query
Last year we started using React Query for all of our API calls (and we talked about it in this article about optimizing API calls). However, the more we were using it, the more obvious it became that React Query was not providing the ideal structure for our data. We ended up moving away from React Query, and are much happier with our new setup.
How things work with React Query
When you make an API call with React Query using the useQuery
hook, the resulting data will be stored in a React Query cache. You can read from the cache using getQueryData
.
The benefit of this is that React Query handles all the complexity of caching and reloading data as needed, so you don’t have to do all of that manually.
The main problem with React Query appears when you need to update data in your application. React Query provides a useMutation
hook that you can use to make API calls that will update data on your server, and then you can update the data in the React Query cache in the onSuccess
or onMutate
callback functions.
The easiest way to update the data in the React Query cache is to call invalidateQueries
, where you can specify all the queries that should be refetched as a result of data having changed on the server. However, there are a few problems with having to invalidate queries like this:
- You need to make sure you’re always invalidating any query related to the data you’re updating. This is very error prone since it’s easy to forget which queries should be invalidated.
- You need to make sure that the query keys you are using in your
invalidateQueries
call line up with the query keys specified in theuseQuery
hook. This caused a lot of problems for us since some of our query keys are large objects and it was easy to forget a property in the query key object. TypeScript can help mitigate this to a certain extent, but many of the bugs we encountered were caused by this problem. - Some API calls take a long time to complete, and so the UI would not update instantly if relying on
invalidateQueries
.
Using invalidateQueries
is not the only way to update data in the React Query cache. You can also perform optimistic updates by surgically updating the data in the React Query cache using setQueryData
. Updating the data in the cache directly has the advantage of making the UI update instantly, but came with the following downsides:
- You need to know every place where the data could exist in the React Query store. The same piece of data could be fetched from different
useQuery
calls, so you would need to update each instance of that data in the store. It’s easy to forget to update data in a certain spot in the React Query cache. - The data structure in which the data is structured in the React Query cache is not optimized to allow for updating pieces of data.
Relying on Redux instead
After about a year of struggling with React Query, we decided to use a new (old) approach.
We opted to the use native fetch
API to make our API calls, and then stored the results in a global store using Redux. With Redux with Redux Toolkit, we’re easily able to structure data in our store in such a way that there is a single source of truth thanks to a normalized state structure.
As an example, we we can create a slice for roles using createEntityAdapter
which has the following TypeScript types:
type RoleEntity = { id: number; createdAt: Date; updatedAt: Date; name: string; workspaceId: number; memberIds: number[]; viewIds: number[]; } // Redux state for roles looks as follows: type ReduxState = { ... roles: { ids: number[]; entities: { [id: number]: RoleEntity; } }; ... }
Notice how RoleEntity
has memberIds
and viewIds
. These ID arrays are used to reference the members
and views
entities in our normalized store, rather than nesting objects, ensuring that there is only a single source of truth for our entities. As a nice side effect, this more closely matches how data is being stored in our SQL database.
Because the entities in our global store are never duplicated, we are able to update data in our store in a single place, and the UI elements that reference that piece of data will all update properly.
It’s also easy to update entities in our redux store since all entities are accessible via an entities
object that is a mapping of entities by ID. Redux toolkit also provides a lot of helper functions that can be used to easily update entities. Contrast that with React Query where some of the entities could be within deeply nested arrays making it quite inefficient to try to pinpoint and update those entities.
Why Redux?
Redux wasn’t the only option we could have chosen to use for our global store. In fact, we made an effort at trying out valtio for a while (see the pros/cons comparison we made in the image below). However, we ultimately chose Redux since it provides better built-in tooling (via Redux Toolkit) for structuring our store and updating data.
Pros and cons list we made when deciding what global store solution we would like to use.
We are much happier with the state of our data fetching and caching in our web app now. We are running into fewer bugs with the usage of Redux, and the team is able to quickly develop in Redux with the help of Redux Toolkit.
If you’re interested in seeing the results of our newly Redux-ified app, you can sign up for Basedash and join our demo workspace. We’re building a tool that lets you view and edit your database with the ease of a spreadsheet. On top of that, you can build views of your data and share them with your teammates to give them limited read/write access to certain tables.
If you’re interested, you can sign up here: https://app.basedash.com/signup
Invite only
We're building the next generation of data visualization.
How to Center a Table in HTML with CSS
Jeremy Sarchet
Adjusting HTML Table Column Width for Better Design
Robert Cooper
How to Link Multiple CSS Stylesheets in HTML
Robert Cooper
Mastering HTML Table Inline Styling: A Guide
Max Musing
HTML Multiple Style Attributes: A Quick Guide
Max Musing
How to Set HTML Table Width for Responsive Design
Max Musing