A lot of React applications out there run for state management solutions such as Redux or Mobx and use it mostly for server side state like 'isLoading', 'isError', etc. We need to stop mixing between server state and client state. Don't get me wrong, client state is important, but 70% of the state is actually a server state. In this talk I will demonstrate how we can encapsulate server state using our own custom hook or using a (perfect) solution such as react-query.
Stop Abusing Client State Management
React Summit 2022
Hi everyone, my name is Nir, I'm 34 years old from Israel, married to a frat and father to three little wonderful girls, Bar or Nadas. I've built webapps since I was a kid, my first website was the biggest Israeli Pokemon fans website and it was a great project. Since then I had my BSc in Computer Science at the Hebrew University of Jerusalem, I worked as a full stack at Intel for a couple of years. Afterward I was a frontend team leader at Curve, a blockchain startup which acquired by PayPal. Currently I run a consulting company in webapps development, both in the world of web 2.0 which you are all familiar with and also in the web 3.0 world. Enough about me, let's talk about state management common abuse. So we've all been there, starting a React application, choosing our favorite state management tool like Redux or Morbix or many others out there and connecting our components using these tools to our backend API functionality and start using state management awesomeness. Let's see it in action. I created a simple React application using code sandbox and I'll start by declaring an API function called fetchPokemon which will get in the parameters the Pokemon name and we will use a Pokemon API service to fetch the relevant Pokemon data. I wrapped it with a promise to make sure we get some delay in the response to mock a common behavior of API functions. So there's a set timeout and I set it later to two seconds of delay. Now I'll do the fetching with the Pokemon name, handle the response which is in a JSON format so I'll use the JSON and return the data. Cool. Great let's add the two seconds delay I talked about. And now I can call the endpoint in the UI, declaring the state Pokemon data and set Pokemon data using the useState hook and creating a fetch random Pokemon function so the app won't be boring. So I'll be using or I'll be choosing out of three common Pokemons, Pikachu, Charizard and Squirtle and we'll use some random logic using the math library so multiplying math random with the array length minus one and round it up. And now I can use it. I'm calling the fetchPokemon with the chosen Pokemon name. Cool. Return the data. Great and now for the best part, the UI. So I will declare a div container and I will display the Pokemon data in a div and create a button for using the fetch function. So in the div I will display the Pokemon name as well as its image using the sprites data which we get from the service API. Put it in the image source. And great, back to the button, I will attach the fetch random Pokemon which we implemented. Add some text. And let's add some alternative text to remove the annoying warning of the code sandbox. Add some typo here and test it. Cool, we got Squirtle. Let's add some style just to move the text a little bit up above. And we're done. Got Charizard and Squirtle and Pikachu. Great. I think to myself, an app is not an app without some loading indicator. That's the part where most developers will use Redux or some other solution to hold this kind of states. I will use Redux for simplicity reasons which is very similar to Redux Redux. So I'll create a Pokemon reducer, get in a state and an action and switch in the action type. So first let's handle a default case. Throw in some error. And now add the common loading state. Status is loading and great, but what if I get an error? So let's add a state for this as well. With the error data. And surely we need a success state to hold the data getting from the service API. And since we can be sometimes not in neither of these states, let's add an idle state. And declare an initial state to begin with. Right, idle starting from idle. And now I can just use it in the application instead of the regular use state. So extracting the state and the dispatch function out of the reducer using the Pokemon reducer and the initial state. And let's adapt the UI to use our new state variable. So when I'm idle, I will display some text. Click to fetch a Pokemon. And on success, I want to display the Pokemon data. So let's implement this as well. Pay attention that Pokemon data doesn't exist anymore, so we'll replace it with the state variable. And on error, I want to display the error message, which I saved in the state. And finally, I want to disable the button while I'm on loading state. Now I can finally implement the loading indicator easily. So what I will do, I will switch the button text to loading when I'm in the status of loading. And display the fetch Pokemon text when I'm not. Fetching. Okay, and don't forget to dispatch the right state when loading and when receiving the data. And when encountering some error. Let's hit the refresh and test it. And I see we don't get the loading indicator. So I think I know why we have some bug in the setTimeout. Let's fix that. Great. Test again. And we got this. We missed the style, so let's move the style to the inner div we implemented. And great, test again. Cool. Usually, for this kind of async functions, developers write different reducers. But I know a better way to share some logic. Custom hooks. So let's create one. So let's take the reducer, move it down, and declare a custom hook, which I will name usasync. Okay. The usasync will get an async function. And actually, we can use our reducer. Let's just rename it to something more generic. So fetchReducer might be better. And we can use it in our hook. Great. And now let's create a function that will be responsible to changing the state using the dispatch. So we'll name it execute. And this function will get parameters, of course. And start by dispatching the loading state. Then we will call the async function argument we got with the params. And on success, dispatch the success state. With the data, of course. And last but not least, when getting an error, we will dispatch the error state, similar to what we did earlier. Great. Error with the error data. Now I'll have the execute function with use callback to avoid re-renders, except the time, of course, when the function does change. And finally, we'll return from the hook the execute function and the state, which we can be used in the application. And let's actually use it. So let's extract execute and state out of the user sync custom hook. And we need to pass it our function, so fetch random Pokemon. And let's make some adjustment, removing the dispatch. And moving this function out of the application. Call in execute. Great. And this is working. Nice. So actually, I didn't invent any of that. I just used a pattern. I met in the school library, use hooks. And you can see here, user sync, which is implemented very similar to what we did. And it's actually amazing. And this library has many other custom hooks. I really suggest checking it out. So with your permission, and to make some point, I'm getting rid of the Pokemon randomness for a while and fetching only the great Pikachu. Notice that I'm fetching the data every time I click, even though I have the same data all the time, Pikachu. So I think it would be nice to add some cache mechanism. I'll add a cache variable, which will hold the data. And add some flag named with cache. And let's call it with true. And let's implement the caching. So if the data does exist, I will return it from the cache. Right. If it exists. And otherwise, I will fetch it using the sync function and save it in the cache. Great. Let's test it. Now it's fetching. And for the second time and third time and so on, it won't fetch again because it's using the cache data. And let's add some invalidation to make sure we support when the data is not up to date. So if invalidate flag is true, I'm resetting the cache. Let's add it to the dependencies array. And now let's set the flag to true. And if I click over and over again, I'm fetching. This is good when I want to tell my custom hook that the data is not relevant anymore and I want to refetch it. So I can connect a timer to it or something like that. And you know what, it will be so cool to add some retry mechanism. And I think you know where I'm going with it. We don't have to work this hard, although it is fun. So I want you to meet React Query, which is my first choice when dealing with the API LL. So first, I'll wrap it up real quick with the React Query provider. Starting from the React Query library, which I just installed. And wrapping the entire application with the provider and the client. And now I can use the built-in hook from React Query called useQuery. So this hook actually holds everything we just implemented in UserSync, including the cache system, the retry mechanism, the invalidation, and many other cool functionalities. We can get rid of the UserSync and let's use useQuery instead. It has two parameters, actually. The first one is the query key, which serves as the ID of the query. And the second one is the query function, as similar as the UserSync hook we just implemented. So this will be the fetchPokemon function. Currently with Pikachu. And let's extract everything out of the result. So we have the data. We have some indicators, like if there was an error, if it's loading, and so on. And let's use it in the UI, make some adjustments. Great. The error. We can remove this and use the isError flag, which we got for free. And the error variable, which we can extract as well. And the refresh function, which will replace the execute from the UserSync hook. Great. IsLoading here instead. And here as well. Not isLoading. Cool. And great, we got the caching out of the box. Let's clear things out. All of this. Oh, this is such a good feeling. And we can even use the fetchRandomPokemon once again and get in the cache. Great, everything works. So here is the React Query Library website, which is very great. It has some cool documentation. And I really suggest going over it. It has so much functionality, which is useful, and really makes things very simple related to handling the API layer. So it's really worth checking. So in conclusion, what we did was take a simple API function and connect it to a really simple UI. But as you can imagine, as the UI gets bigger and bigger, it gets more and more complicated to handle this kind of API functionality. And we don't need fancy tools such as Redux, MobX, or many others out there to handle what is called server state. These tools are meant for client state operations, such as if the dialog is open, if a message menu bar is open, if I'm logged in or not logged in. This is client state. In any case of server state handling, such as fetching, caching, loading, and so on, I really suggest either build some similar solution to use query or use async, custom hooks we just saw, or really consider using React Query, which is really, really helpful in actually every application, every React application I saw so far. So hope you had a great time. I had a great time. And you know where to find me.