Sitemap
Duda

We love working on our product and we love writing about it. You can find posts about tech, product, growth, management and dilemas written by our dudes and dudettes.

What I learned from React-Query, and why I will not use it in my next project

--

The beginning was exciting. The company’s lead front-end architect suggested checking out React-Query for our new Do-It-Yourself editor. Looking it up on NPM made it very clear — React-Query could not be ignored.
React-Query is on it’s way to becoming one of the most popular libraries in React’s ecosystem.

Zoom image will be displayed
By Duda
Zoom image will be displayed
React-Query vs React vs Redux. Downloads in library’s first two years, according to `NPM trends`

The next thing that surprised me was the `quick start` page.
This is how a request is managed on React-Query:

Seeing it for the first time, you might ask yourself “Where is all the cycle of managing the request’s state? Changing it to loading, saving an error message, or hopefully, saving the returned data? This is React-Query’s magic — it does it for you.

To create the same functionality prior to React-Query, I’d have to write:

From a product point of view, both snippets are equivalent. But managing the cycle in the old pattern required 19 lines of code, and using React-Query requires only one. The query object returned by React-Query includes all the state information we had before. We are released from the burden of repetitively managing the request process over and over — React-Query does it for us. By this stage, to put it in React terms, I was hooked.

Does React-Query qualify as a request framework?

React-Query does so much more than just manage the request state cycle. It provides out of the box tools related to network requests, such as re-fetching, prefetching and caching. It shares query state across an entire React-tree, which you can access by a key. It comes with an out-of-the-box mechanism to keep data fresh, by re-fetching data automatically when it is needed and defined as stale. And it also provides tools to manage your mutation requests (accessed via the `useMutation` hook).

But above all, React-Query comes with a new and different paradigm. According to this paradigm, server state was improperly managed till now. UI state and server state are of a different nature, have different needs and should be handled separately.

Furthermore, the way we managed global state until now is only appropriate for client state.

“Redux, MobX, Zustand, etc. are client-state libraries that can be used to store asynchronous data, albeit inefficiently when compared to a tool like React Query”

“For a vast majority of applications, the truly globally accessible client state that is left over after migrating all of your async code to React Query is usually very tiny.”

(Official site)

Going all in

Back to the project I was working on — Duda’s new DIY editor.

The idea is to provide Duda’s partners with a tool, with which their users can painlessly, easily and quickly create their own new site according to the partner’s specific needs. Duda already has the technology to create websites, we just wanted to create a simpler flow for a certain segment of users.

As you know, by this stage I was excited about React-Query, and decided to give the React-Query paradigm a fair try. Our plan was to hold all server state in a React-Query cache, and access it via the useQuery hook. It looked good.

Too good.

Once we had added complex user interactions issues started bubbling up.

User adds new section to their site

We’ll demonstrate those issues with a specific user flow — user tries to add new section to their site.

In the side panel the user can multi-select the kind of sections they want to add to their site, such as: “about”, “testimonials”, “FAQ”, “Contact us”, etc.

We send a request to the server to add this section to the website. If the request succeeds, the website then updates, and can edit various fields in that section in a simple form.

Optimistic updates

In our new DIY editor, we want to provide the user with a live experience. For that we use optimistic updates. React-Query’s approach to optimistic updates (link) is to update the query cache, according to what the data should look like if the request succeeds. If it ends up failing, we revert to the previous value.

We tried to implement it, and we reached a dead end:

  1. To determine if a section kind in the multiselect should show as selected, we look at the current data of our sections (sectionsDataQuery.data), and check if one of the sections in the website is of that kind. The thing is, at the time the user clicks to add a section, we have no way of knowing what the new section data will look like. (See gist)
  2. There is a conceptual issue. The whole idea of separating client state from server state is that we’ll have a clear source of truth which we can trust. A source of truth which represents the real persistent state of things. But now the cache dedicated to server state isn’t reliable any more.
  3. There is also a practical issue. A user can select multiple sections from the list. What if there are many optimistic updates taking place at the same time, some succeed and some fail? how do you know which state version to revert to. According to React-Query’s approach you might revert to an unreliable version.

Weird flows

Following the React-Query approach we had a minimal store for client state, and we accessed server state via React-Query’s hooks.

The thing is, in a complex app those two sources of state interact. We have computed values that are derived from both. And we have async flows that have to do with both. To go back to the same example:

  1. User can select a template of a section they want to add to their site
  2. Request is fired to the server
  3. Response with the data of the new section is sent back to the client
  4. Data of the new section is merged into the section’s array (server state)
  5. Id of the new section is saved to the store as selectedSectionId (client state)
  6. Value of selectedSectionData is derived from the siteSections data and the selectedSectionId (computed state)
  7. selectedSectionData is used on various components in our app, and also sent to an Iframe window

We think that such a process should not be managed within the component. It affects the entire app, even beyond (the Iframe), and might be triggered by various components.
But if we were serious about accessing React-Query’s server state from the component, the server state would only be available from the components themselves. This also means that computed values and managing the async process is pushed down to the component.
We then had to access those values and functions by various components in the tree, so we had to add a context provider.

State and functionality that relate to the entire app started having various centers: React-Query, store, services and context providers.

App structure

Until now components were only aware of one app level source for logic/side effects and state.

Now, there was another separate source that exposed functionality and processes.

Zoom image will be displayed

Not only that — those centers had to be accessed in a different mechanism.
It seemed like the component’s responsibility had grown significantly.

To be honest, we followed the React-Query approach and we got lost.

Our current solution

React-Query’s API does expose a QueryObserver, that allows you to trigger a query and listen to any changes. It can be accessed from anywhere. We can then subscribe to any change, take the QueryResult value, and place it in our MobX store in a dedicated property. When a query is relevant to more than one component, we moved away from managing the queries via useQuery (from the component), in favor of managing them on the app level, via the QueryObserver.

The solution isn’t perfect though:
1. React-Query does not provide a non-hook tool to trigger mutation requests.

2. The data structure returned from React-Query was built with hooks in mind — it is a work around.

React-Query is a revolution we should learn from

Don’t get me wrong. React-Query is a revolution, and I think it has greatly contributed to the community. Mostly, because it has shown us that things can be done differently.
But it is a revolution that is taking place at this very moment, and we might need some time for the noise and dust to settle, so we can distinguish its great parts from some of its pitfalls.

React-Query — The good parts

React-Query automates the request process, including the requests’ state cycle. I can’t underemphasize this, because it is the first time, AFAIK, that automation occurs on a state related process.

It provides valuable tools to improve reliability of server data (invalidating data and marking data as stale).

It provides Tools to improve fetch UX (prefetching, re-fetching, caching, and more).

React-Query — The pitfalls

I often think of front-end development as the art of creating Lego pieces and composing them. For a code unit to be easily composable, it should have a single responsibility and an interface that can easily fit potential consumers.
Let’s look at Redux and MobX for example. You can consume them independently from React, and then they provide a second package (React-Redux for instance) that makes consuming them in React components more convenient. Each library has a single responsibility, and can be consumed independently from React, or consumed in React in various ways. There is an abstraction that allows it to play nicely in different architectures.

At present this isn’t the case with React-Query. This lego piece is big, and can only fit in a specific way to a specific structure.
The concept of automating and managing the request flow could be relevant for various architectures. But the React-Query package was built to be consumed by a specific kind of architecture.

React-Query presents a paradigm that is aligned to the architecture it was built for — “Global state is not suitable for server data”.
I have a different point of view. I believe that you should always start from local state. But there comes a time when you need to pick state up. And sometimes you need to make it global. I believe that global state makes sense, also for server data.

I understand the meaning of server state differently. Like React-Query, I think that server state is its own thing and should be treated differently from client state. It is different because we are not the masters of that data, and we have a responsibility to represent it reliably. I do think that we should keep an independent representation of server state, that is our reliable source of truth. But does it mean we need to hold server state and UI state in a different kind of infrastructure?
React-Query suggests keeping server state and client state in different kinds of entities, accessed in a different way. But do they actually keep server state independent from client state?
React-Query’s method for optimistic updates actually mixes server and client state. Treating server state as a reliable representation of server data is what counts, not having a different infrastructure to hold it.

What I will use in my next React Project

React-Query showed me that there is a need for a layer that manages the request cycle and state. I have other things to do, and I’m happy to delegate this responsibility to an external tool. But in my next React project, I’ll try to find a tool that can integrate nicely into an existing and changing codebase.

--

--

Duda
Duda

Published in Duda

We love working on our product and we love writing about it. You can find posts about tech, product, growth, management and dilemas written by our dudes and dudettes.

Responses (28)