David Khourshid

Goodbye, useState

Conference talk at BeJS, 2025. 32:02 long, 15.3K views on YouTube. Topics: react hooks, state management, forms & ux, react.

Watch on YouTube (BeJS)

What is this talk about?

State management is easy, until it's not. Sure, useState() is fine for simple component state management – that's literally what it's for. But as our React apps grow in complexity, the inevitable long chain of useStates make it more difficult to understand and maintain app logic, turning our components into Rube Goldberg machines. (From the video description.)

“In general, useState setters because they're going to give you the previous state and it's going to be a more reliable way of updating your state.”

“Whenever you change the value in useState, it will rerender everything that's in that component and under it and that can be really undesirable.”

From the opening of the talk.

Key takeaways

  1. Whenever you change the value in useState, it will rerender everything that's in that component and under it and that can be really undesirable. 4:00
  2. In general with forms, use native form data if you want to. 16:00
  3. Instead of doing that, we can eliminate the useEffect and honestly just compute derived states directly in render. 22:00
  4. In general useReducer for complex UI logic and state updates where your pieces of state actually have to depend on each other. 25:00
  5. Instead of saying like, oh yeah, this use date update triggers this useEffect which calls these other use dates and then this other useEffect changes that state over there. 31:00

Quoted from the transcript.

Full transcript

From YouTube auto-captions. Technical terms were normalized; some errors remain. 5,140 words.

0:00 Introduction

[Music] all right. Hello everyone and goodbye you state. So first of all, who am I? It doesn't matter because we have so much to talk about right now. I only have half an hour. So I'm really just going to get started with this. You if there's one thing that you should know about me, it's that a couple years ago I gave a talk about useEffect. him. The talk was actually titled goodbye useEffect where I talked about the defects and how really it's a foot gun. And the first couple times that I gave that talk, people just came out with pitchforks. They were like, "Oh, you call yourself a senior React developer. You don't even know useEffect, etc. Why are you saying useEffect is so bad?" And then the React team actually wrote documentation on how you might not need an effect. This was great because my future talks on goodbye useEffect were basically just reading the documentation which you all should do anyway.

So we are here to talk about use dates and now I'm not going to say like you should you should never use dates or anything like that but I just think we're using it a little bit too much. Now, I get it. In the React survey that came out, uate had like the highest retention, the highest happiness. Like, it's the most used thing in the React ecosystem. And I mean, there's other surprising things over here like who still uses Axio? Fetch is right there. Like, stop. Okay, it's 2025. All right. So, but in order to level set with you, what we're going to be talking about is better patterns now that React 19 is out and patterns that we could even revisit now that we, you know, or can organize our React applications a little bit better. So, first I want to talk about just some of the weird stuff about state like closures, batching, etc.

I mean this is something that a lot of beginner and even more experienced React developers fall into where we just depend on like oh this count is going to be updated.

Play from 0:00

2:00 The thing is that this will cause a rerender and nothing…

So when you get 60 + 8 + 1 obviously you're going to get well one because it's really taking the last set count and it's using that. If you thought it's a different number don't be dirty. It's not the number. but you're going to want to use a setter instead. So with a setter, you avoid the problem of closures and this is all going to be batched and it's going to be calculated one at a time so that you get your final count of 42. So that's just one of the weird things that a lot of you probably have gotten used to already with use states. One that I actually encountered because I like using these obscure but native JavaScript things like maps and sets is that ustate doesn't really work with maps or sets and I these are the most terribly named variables. We have set and then you know you could set and then set add set and then this button you could click set.

But this is not actually going to work. And why is it not going to work? Can anyone tell me? because it is the same object identity, right? So, React's going to detect that nothing really changed. So, you could sort of trick it, I guess, by well, what I'm doing is I'm wrapping it in an array and then basically we're just I'm I'm hearing a lot of moaning and trust me, this is a disgusting pattern. But if you do need to use set, then you could just do that. And again, some of you will be like, why would I ever do that? the truth is that working with maps and sets and reacts just good luck figure it out. I don't know. But yeah, in general, useState setters because they're going to give you the previous state and it's going to be a more reliable way of updating your state.

And beware of closures and object identity because it's, you know, something that trips a lot of React developers up when you are returning the same or a different object and you might be causing multiple rerenders. And speaking of rerenders, here's another weird thing about ustate is that let's say that you have a value in ustate and it is being read by multiple components. So, we might want to just lift this state up and use just prop drilling to bring that value into those components. But the problem is that this is going to cause a lot of rerendering. So whenever you change the value in useState, it will rerender everything that's in that component and under it and that can be really undesirable. But sometimes it's unavoidable. However, in situations like this, let's say that we only want to track the number of times the button has been clicked. So we have handle click over here and then we're setting the click count and then we're tracking however many times the button was clicked.

You might think, yeah, this is a really good use case for states because it's literally states that we have to keep track of. But the thing is that this will cause a rerender and nothing is actually rerendering in the component. So instead, you could just use a ref. And so this is where refs are really useful in place of useState is when you don't actually need that state value to rerender anything in your component. So in general you really as much as you can try to keep us state as local as possible. Don't like don't give into the temptation to lift it up and you know just have it be shared between multiple components. That's what things like context and other things we'll talk about later are for. And you can useRef for inter internal state that should not cause any rerendering. So basically states that doesn't really have to do with the UI.

Play from 2:00

6:00 Is loading error data useEffect set is loading true

Another thing and this is where we're really getting into how we could remove our usage of useState. This really cool concept of using URL as states. So take this for instance. We have a sort component and we want to set the sorting on some table to ascending or descending. useState is fine here. But guess what? If you change it to descending and then the user reloads the page, guess what? It's going to go back to whatever the original sort was. I put it as null, but you know that might be ascending or descending. You don't know. And so what can we do? We can actually use the URL and search parameters in the URL. I'm not going to go too deep into this because there's a following talk that will but for example you could use next and next has a hook called use search params and that way you could grab the sort but updating those search params is a bit awkward because you have to create a query string and then you have to basically do router.bush and then set the query string to you know whatever that value is.

Really really weird. This is something where I think Remix really shines because this is all you need to do. You just, you know, it's basically like use states but for search params in a way. You know, there's still a little bit of awkwardness here, but you know, it actually works a lot better than Nex.js. But there's also KNX. And so KNX, this is what the following talk is going to be about. This is basically useState but for your search parameters. Why do you want to do this? Why do you want to put state in your URL instead of in your component? There's a lot of advantages. Again, we're going to hear about that from from Qua Francois. I hear he's the best. So, he will talk to you about that. And I tried it. It works really well. So in general consider using search parameters in the URL for URL persistent states because that can be read in multiple components.

It's like this global beyond the app context and you could also persist that state and France is going to talk about much more of those benefits coming up next. All right, another one of my favorite topics fetching data. We've all written this a million times. We have is loading set is loading error set error etc. In fact, if you were to search your code bases just search for set loading you're going to find like a million of these. Why? Because if you're an experienced React developer, these are baked into our muscle memory. Like we know, oh, I have to fetch some data. Okay, is loading error data useEffect set is loading true. Like all this is muscle memory. And in fact, if you're a more experienced React developer, you'll remember to do cancellation, which I forgot to do here. Like let is canceled equals false. And then you set is canceled equals true whenever the effect has to rerun.

It's it's just really confusing. But guess what? We have server components. This is the only logo I could find of it, but RSC's very very useful. Yeah, I swear this is like one of the first logos I saw. But, basically we could just fetch right in the component. And we're going to hear a talk from Aurora in this same block more about just server components and server side fetching. But it really eliminates the usage of or the need for use states because we could just fetch directly in the component and then use that data directly. And then you could also use suspense if you really want to control the loading states. I actually didn't have time to write a code example for this, but hopefully Aurora will show you that. Otherwise, you could just read the docs. Suspense is really useful for that too. There's another very cool hook called starts trans or sorry called use transition. And so with use transition, you can actually start a promise inside of it.

And that way you don't have to have that set loading is loading boolean state because that is handled in the hook. itself. Now, the purpose of use transition is a little bit more nuanced as the React team likes to call it because it's more about like dep prioritizing state updates and marking something as not a priority so that the rest of your UI could remain responsive or something like that. I'm not smart enough to understand that. So, I just use it for handling you know promise dates and things like that. But it works really well for that purpose. Or you could just use React query or Tanacqu query because honestly it's going to handle fetching data better than you. And even if you manage to figure out like, oh yeah, these are all the states I need and this is the cache I need to set up and everything like that, they've already figured this out.

Don't reinvent the wheel. They've made a very very good wheel. And people do get upset at me when I talk about that. But yeah, there's a reason that Tanacquery exists. So fetching data we can eliminate a lot of useState from that. We could use server components whenever it applies. We could use suspense for controlling loading states and hooks like use transition for local pending states and other things like dep prioritizing UI updates. And honestly, if we really want to eliminate boilerplate and eliminate all of those used dates for fetching data, query libraries have already solved this such as tan stack query. All right, so now forms, this is another big one where we just have use states all over the place.

Play from 6:00

12:00 When you submit a form, you already get that form data

Here's an example form and this is one of the smaller ones. They do get pretty big. So we have a few variables for first name variable I have a variable called variable I don't know why I have that and bio and so this you know this is something that we all learn when we're you know first learning React we make a simple form we have a few fields and then in our inputs we just handle updating those with onchange this is very basic React 101 stuff. However, for simple forms, we actually might not really need that because the browser has a built-in API called form data. And so when you submit a form, you already get that form data. So the best way to think about this is that we're actually redundantly copying states from the inputs into our React components, but the browser is already holding on to that state for us. when you type in an input, that input has an input value.

And so that state's already there. It's like the browser's own built-in used state. So why are we duplicating it? There might be some good reasons where you might want to display the types value somewhere else. Although I've really only seen that in toy examples and not too often in places where it actually matters. Like usually it's just whatever's on the form is there. We're not needing to duplicate data anywhere else and we just need to submit all of that data all at once somewhere. And so this is a great use case for form data which is native HTML 5. And if you are worried about type safety then you could use a client side type library or a runtime type validation library like ZOD. So we have this user shape over here or user schema and then we could parse the form data where the form data gives us objects from entries because you could have many inputs with the same name.

So yeah you just call object from entries you get a nice object and then you can ensure that it fits that user schema. Now I mention this because I know that you all are not going to just rewrite your forms and be like let's get rid of all this useState and let's use form data but it is important to mention this because the way that frameworks such as remix and nexjs and I'm sure even tan stack start which was just recently released they work with the same concept in mind. So with remix you have the form components and so basically you there's no useState here at all. You just have method post action some action just like we're doing PHP again. And then all of that data gets sent up to the action as form data that we could use directly. And then for big triangle company we have something similar where we have sorry I forgot that I added that we have use action states and with use action states you could actually pass in that form action.

So for example create user so we have use action state with create user and then you already get a pending variable. So it's actually pretty useful where you don't really have to ma manage those loading states and yeah so again there's no useState insight here this is just going to go directly to that action that you've defined we have the form data over here again you could use zot to validate that shape and make sure that it is exactly the same shape as you expect and then you could use it directly in that action so Next.js uses form data. Remix uses form data. Or you could use form data directly. But I know some of you will say like, hey, I have a form with complex validation requirements and it needs to be validated on blur, but only after a debounce or something like that. And what you're going to end up doing is reinventing one of these libraries. Don't do that again. These libraries exist for a reason. And so if you really want to control complex form logic, I highly recommend either React hook form, which is one of the most popular ones, or Tanac form, which is a really solid form library, and it was also recently released, I believe.

So in general with forms, use native form data if you want to. And also runtime type checking libraries like Zod are extremely useful. take advantage of the framework features for your forms or maybe even use form libraries for more advanced forms.

Play from 12:00

17:00 Instead, immmer is taking that as patches and it's saying like…

All right, so now overusing states. You might be thinking like I don't really have a problem with useState. It's easy. It's a very simple API. But eventually, you know, it never starts like it never ends like this. It starts like this, but you eventually go from use dates to use eight. And then and then things get worse and worse and worse and worse. You know what we call this? This is vibe state management. And this is this is what cursor is going to output when you just let it run wild. So yeah, what can we do about this? And thi this is an exaggeration but this is how let's be honest a lot of our code looks especially with bigger and bigger forms. So one technique that we could do not getting rid of all the use but instead just grouping data together where if we have related pieces of states then honestly they should really stay together.

Now this does require a little bit more work in order to for example update each individual field. So, I have a custom function here called handle change that helps me do this. But honestly, having a single-use date makes it much more manageable and easier to reason about, especially when you're working with a bigger team. If you really don't want to like just manage all of those or sorry, create those custom functions for managing things, use something like immmer has a very useful produce function where you could actually manipulate a draft and that's it's not really mutating the actual data. Instead, immmer is taking that as patches and it's saying like these are the operations I need to do immutably to the original object and then it returns that object without actually mutating it.

Play from 17:00

19:00 You basically import create store from XState store

So very simple oneliner. Another pattern I've seen too is when we're doing things like steps. So for example, we have an address step, payment step, confirmation step, complete step and we could really reduce these into just a single string enume like this. So now all we have is address and then when we go to the next steps we could just set that directly. So that's another important concept I want you to keep in mind is using string enumes to just eliminate all of those boolean state values that we have. So group related state together use immeras easily managing complex state updates string enumes really help you get rid of all those boolean values and think before vibe coding. Now let's go a little bit further with enumes because again this is code we have seen all the time. We have data loading error. And the problem with this though is that how can you guarantee that for example loading is not true at the same time error is true or like we have data while we're still loading or you know just basically there's many possible states that can happen here and we might end up with some impossible state.

So how do we manage that? Well, the first step we could use the string enum. So instead of is loading is success etc. we could have that over here. And that doesn't really solve the problem though of that type safety that we really need. And so that's why I think that using distributed or sorry discriminated unions is actually really helpful because we could narrow down what the state of our shape or what the shape of our state can be based on that status value. So for example, if it's success, we know that data is defined. If it's error, we know that we have an error string. And if it's loading, we know that we don't have any data. So we're not going to have any type errors. And we can guarantee the types and the shape of our state just by that discriminated union. And check it out. We've actually eliminated a lot of state here. So we really only need to set one thing.

And again, this is completely type safe. So you're going to have autocompletes. you're going to ensure that the result is exactly what you expect. So that's discriminated unions. Another way we could get rid of states while I have only eight minutes left is d getting rid of derived states. So I feel like there's two main types of states. There's primary states which is like primary from the source and there's derived states which is derived from that primary state. So in this example, we have total items, subtotal, etc. And whenever those items change, then we're recalculating all of those in a useEffect, which is my second favorite hook. So instead of doing that, we can eliminate the useEffect and honestly just compute derived states directly in render. And I know we're talking about donuts over here. I know croissants are a big thing in Paris. This is a cronuts, by the way. I don't know if that's illegal here, but they're really, really delicious.

So, sorry for all the doughnut references. But yeah, I'm going to breeze through this a little bit as I'm short on time, but now we're going to talk about reducing logic. I wouldn't say states, but more logic. I know what you're thinking. Not not this. I know you're thinking, I hate you state, or maybe that's just me. And you might be thinking I'm gonna come to you and say, "Hey, there's this thing called useReducer, right?" Yeah. So, it's true. No one likes to useReducer, but it can honestly be very useful in certain situations. In fact, like even the simplest situation like this where we have a simple toggle like nothing gets simpler than this. we have a toggle instead of doing useState you know the opposite of whatever the value is like not is active or is active equals true then set to false whatever just use a reducer and then that's all you have to do and you could just use toggle and it will just change the value according to that but where useReducer really shines is when you have more complex state logic and you need to really constrain that logic so you can't just update state from anywhere.

So, for example, if we have you know, we have a doughnut shop and you know, we're adding and removing donuts. Instead, we can combine that logic into a doughnut reducer. And the reason I'm doing it here is because in this doughut logic, we're actually having to check the inventory. So, going back here, we have to check the inventory to make sure that we can actually add those donuts. So we have that extra safety and we have those constraints to make sure that our logic and our state actually ends up being correct. And another great thing about using a reducer just the general concept of it is that it can be individually tested and it is portable. So really when your logic gets more complex consider useReducer but it's one of those things where you'll know when you need it. I'm not encouraging you to just useReducer everywhere, but you know, just where it helps. And so, I I would call useReducer indirect state management because you're not directly updating state. you're using events or as the React team calls it actions in order to indirectly update that state which allows you to actually express events which cause that state change which give you a lot of benefits.

Events contain information that direct state updates don't. They contain causality, the context like what causes state change and why. You can even add timing information to it or you could collect events and have traceability. So very useful and that's why I think that having indirect state management with all those constraints makes your state management a lot more simple. So in general useReducer for complex UI logic and state updates where your pieces of state actually have to depend on each other. All right. So, going to breeze through this, but we have hydration mismatches, too. Let's say that you want to just show the dates. This isn't going to work in your client's component because how dare you try to just show a date in your component. So, you might have to useEffects and say like, okay, when we're on the server, show null, otherwise set dates. But, this won't actually work either. Oh, yeah. So you can useSyncExternalStore to actually show a client snapshot versus a server snapshot and ignoring those errors.

The right way to do this would be to basically have it as a string. And so that string gets passed from the server to the client and then you can just show the client's value versus the server value. And really you don't need ustate for that either. This actually comes from a really great article by Dominic called avoiding hydration mismatches with USync external store. And I mentioned that just to quickly talk about external stores because really external stores are that missing piece between having global states with actual logic and having local states. because React doesn't really have a built-in solution for wanting to share states between different components. So, that's where you sync external store really comes in. And if you don't know about this hook, I would excuse you because it is hidden in the other hooks section. And so they give an example on the React documentation with the to-dos app. where basically you just pass in a way to subscribe to the store, a way to get a snapshot and from the client and from the server and then you can connect to that store.

So I'm going to breeze through here a little bit. However, the React team does still suggest that you useState and you useReducer because you sync external store it's really for third-party libraries. It's not something that's really meant for you to directly integrate with, but if you're using browser APIs like checking the window width and things like that, it's extremely useful. So now while I have two minutes left, I knew I had too many slides. Going to talk about third party libraries. So what's the motivation for this? Let's say that you want counts and you basically want to constrain this. So eventually you get to useReducer. I will share these slides. So sorry that I'm breezing through it at one second at a time. But basically we end up in a place where we end up reinventing Redux at the end of the day. That's basically what all those slides were showing.

So there's two different strategies for this. There's basically having a direct store object that you could subscribe to and send events to and that is imported directly into the components. This is what Zustan does. or you could basically pass that store via context and that way you could isolate where that store goes and it's actually very useful for testing. So lots of libraries do this but I want to talk to you real quick about XState not XState but actually XState store. If you aren't familiar with this library it's actually a much smaller version of XState which allows you to very easily define stores. So you basically import create store from XState store. You provide the context which is your state data and you provide basically actions or ways that you transition the state and then you could subscribe to state changes and then you could trigger those events just like that. You pretty much know the entire API.

If you want to learn more about XState store then please talk to me after or during the break. But I think that it's really exciting because it makes state management very simple. It's basically like sustand. And hopefully you hate it less than useReducer. Oh yeah, you could use stores in a local components too. So it could be a global store, it could be a local store. And also it handles effects as well. So we could have the ability to emit events and then to subscribe to those and actually execute effects declaratively. So yeah and this is something that I feel very strongly about. Effect management is part of state management and this basically defines how your entire application works. Whenever an event happens it changes the state but it also produces effects. And this is known as a state transition. I think the translation in French is this.

I'm not sure. But anyway, the last thing I really quickly want to talk about is local first date where instead of just talking to APIs and HTTP and fetching data, you could essentially have a direct synchronization with your database. And so there's tools like electric SQL and a bunch of other tools that allow you to essentially subscribe directly to your database and just show that real-time data. And so this is why I'm very excited for local first state. I think it's going to be the future. So in conclusion, I would say useState. I want you to truly use it for only components internal UI state as much as possible and ask yourself, is there a better pattern? Because especially with React 19, there usually is. And so my general advice to you, start with use dates and then refactor to better patterns as your application gets bigger and focus on maintenance. Like instead of saying like, oh yeah, this use date update triggers this useEffect which calls these other use dates and then this other useEffect changes that state over there.

Get rid of that. That's the easy approach and focus on simplicity where this event changes this state and then it renders to the component and you have a very easy mental model of thinking about your app no matter how complex the state gets. So with that, thank you so much React Paris. [Applause]

Play from 19:00

Related talks