Why we had to move away from React Query

The admin panel that you'll actually want to use. Try for free.

July 25, 2022

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 the useQuery 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.

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

You could ship faster.

Imagine the time you'd save if you never had to build another internal tool, write a SQL report, or manage another admin panel again. Basedash is built by internal tool builders, for internal tool builders. Our mission is to change the way developers work, so you can focus on building your product.

TOC

July 25, 2022

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 the useQuery 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.

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

You could ship faster.

Imagine the time you'd save if you never had to build another internal tool, write a SQL report, or manage another admin panel again. Basedash is built by internal tool builders, for internal tool builders. Our mission is to change the way developers work, so you can focus on building your product.

July 25, 2022

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 the useQuery 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.

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

You could ship faster.

Imagine the time you'd save if you never had to build another internal tool, write a SQL report, or manage another admin panel again. Basedash is built by internal tool builders, for internal tool builders. Our mission is to change the way developers work, so you can focus on building your product.

What is Basedash?

What is Basedash?

What is Basedash?

Ship faster, worry less with Basedash

Ship faster, worry less with Basedash

Ship faster, worry less with Basedash

You're busy enough with product work to be weighed down building, maintaining, scoping and developing internal apps and admin panels. Forget all of that, and give your team the admin panel that you don't have to build. Launch in less time than it takes to run a standup.

You're busy enough with product work to be weighed down building, maintaining, scoping and developing internal apps and admin panels. Forget all of that, and give your team the admin panel that you don't have to build. Launch in less time than it takes to run a standup.

You're busy enough with product work to be weighed down building, maintaining, scoping and developing internal apps and admin panels. Forget all of that, and give your team the admin panel that you don't have to build. Launch in less time than it takes to run a standup.

Dashboards and charts

Edit data, create records, oversee how your product is running without the need to build or manage custom software.

USER CRM

ADMIN PANEL

SQL COMPOSER WITH AI

Screenshot of a users table in a database. The interface is very data-dense with information.