David Khourshid

Goodbye, useState

Conference talk at G2i, 2025. 23:58 long, 50.8K views on YouTube. Topics: react hooks, state management, react, forms & ux, xstate.

Watch on YouTube (G2i)

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.)

“The thing is useState and useEffect sort of have this symbiotic relationship where it's just it gets really messy because useState triggers a useEffect which might trigger another useState which might trigger another useEffect which triggers the same useState.”

“We are tracking the number of clicks but we could just do that in a ref instead of having to useState.”

From the opening of the talk.

Key takeaways

  1. The thing is useState and useEffect sort of have this symbiotic relationship where it's just it gets really messy because useState triggers a useEffect which might trigger another useState which might trigger another useEffect which triggers the same useState. 1:00
  2. We are tracking the number of clicks but we could just do that in a ref instead of having to useState. 4:00
  3. Use something like that because I know I'll get a lot of hate for this too but it will handle fetching data better than you. 9:00
  4. Sure you could useEffect and useState but there's another very useful pattern which is useSyncExternalStore and that allows you to provide both a client snapshot and a server snapshot. 18:00

Quoted from the transcript.

Full transcript

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

0:00 Introduction

Okay, so goodbye you state and hello reacts Miami. So, first of all, let's get through this. Who am I? It honestly doesn't matter. We have a lot to talk about today. But if you really want to know, I'm David Corsy, @DavidKPiano pretty much everywhere online because I play piano. I live here in the sunshine states. I also really enjoy TypeScript too. And I am the creator of XState. Also, I gave a talk a while ago called goodbye useEffect. Some of you might remember that where basically I was talking about all of the defects of useEffect and how it's honestly a major foot gun. And the first time I gave that talk, I actually got a lot of hate for it. Like, oh, he doesn't understand useEffect, etc. Like, useEffect has been fine for us. Like, you know, we could manage it, etc. But also a lot of people agreeing. jokes on them because the React team actually came out with docs you might not need in effect which basically validates my entire talk.

So basically I'm going to be doing the same today because we need to talk about use date. You know I know a lot of you love use date whoever wrote this tweet please see me after class. But recently there was a React survey and in terms of retention and usage use dates was at the very top. I get it. You love this hook. It's the very first one you learn. The second one is probably useEffect and then you're like, I don't need any other hooks. You know, makes sense. But like, you know, old habits die hard. I mean, take a look at this. We're still we have Axios here. Fetch is right there. What are you doing? Okay, just use fetch. But yeah, so what is the problem with useState? You might be like, hey, it's such a simple hook and you know, it just works. I could just put my state in there and my components work just fine.

Well, the thing is useState and useEffect sort of have this symbiotic relationship where it's just it gets really messy because useState triggers a useEffect which might trigger another useState which might trigger another useEffect which triggers the same useState. And if you're not careful, this is where you go into infinite loop territory. And also this might just affect props of another component which is going to trigger another useEffect which it probably has its own useState here which triggers props. And like basically this is what your application logic tends to look like when you decide to put everything inside of useState and useEffect. It becomes very hard to maintain. So we're not talking about zero to one where oh yeah we just need app logic we need you know to put state in components. It's really easy to do with ustate. I'm talking about when you're adding so many features to your application that you have trouble understanding what is happening where and what is causing the states to change and also why is useEffect rer running so many times.

But you might also be thinking hey windsurf generated this useState useEffect code or whatever else you're using you know copilot cursor etc. And so you might think it's fine, right? It's because AI, it's smarter than us. But I need you to understand something. And this is pretty much going to be the only AI thing I, you know, post today. LLMs weren't trained on good code. They were trained on your code. Okay? So I'm not saying your code is bad or good, but anyway. All right. So let's jump into use dates. So first of all, controlling renders. Something you need to know and something hopefully everyone knows is that with use states when you change state even if you need it in only one part of your component or maybe another inner component etc. You are basically causing a rerender for the entire component tree that it's under.

And so for a lot of applications, sure, this might be fine, but eventually once you get to performance issues, you could usually trace it to a useState that's just causing way too many rerenders.

Play from 0:00

4:00 Another thing that we could do is use the URL state

And without significant refactors, it's sort of hard to handle that. One simple strategy that you could do though is if you have state that has nothing to do with the UI or the rendering, just put it in a ref. That's what it's for. It's like React secret use dates where you really don't want React to know about what's going on. So you just hide it in a useRef. So in this case we're handling like we're it's a trivial example. We are tracking the number of clicks but we could just do that in a ref instead of having to useState. So as a rule of thumb useRef for internal state that's not related to rendering. Another thing that we could do is use the URL state. And this is an extremely powerful pattern that I don't think many of us are taking advantage of. So let's say that you have a component where you're sorting, you have ascending, descending, etc.

You might have all these filters. The problem is that once you refresh the page, the user is going to get really mad because all of their filters are instantly gone. And I get anxiety whenever I go to a page which has all these complex UIs and is definitely built in React because I know once I refresh everything I set there is going to be gone. There's no URL that I could point to. So that's why it's really useful to consider using state on your actual URL using search parameters. And actually both next and remix do encourage this. You could tell it's next by the giants triangle over here. I made it pink which is against their design guidelines but I think it looks better that way. With next it's actually sort of sort of obtuse. You have to do a bunch of things. You have to create a query string and you have to sort of push that you know the path name with the constructed query string to the router.

But it's it's possible to do and you know Nex.js does give you the tools for this. If you're part of Versel cover your eyes because it's a lot easier in Remix. So but in regardless of whether you're using Remix or Nex.js JS. There's also a very awesome library called nux nuks, however you pronounce that. I forget what it stands for something. And essentially you are replacing useState with useQuery state. And I've tested this. It works really nicely. So it's just almost a onetoone swap where basically whatever state you have in your component, it reflects in the URL and then all of a sudden you have a URL where it captures the state for you. So, in short, I want you to consider if state is something that you want to persist or you want people to keep in the URL and then refresh the page and have everything exactly where they put it, consider putting state in the URL.

There's a lot of libraries and frameworks help you to make it really, really easy to do. And I do also want to shout out shad cnui and nux.

Play from 4:00

7:00 We have this form data API

These two libraries have a couple things in common. They're great libraries and they have really weird names, but you know, all good libraries have weird names. All right. Now, if you were at Chance's talk yesterday, I had to implement fetching data. And every single time that we fetch data, this is essentially muscle memory for experienced React developers. We have a bunch of use dates where we have said is loading error and you know some other stuff. In fact, I forgot some things here. I forgot to add the is cancelled or now it's like you could use the abort controller and then have an abort signal and just pass that in. I can't say that word too much in Florida by the way. It's illegal. So anyway, if you search your codebase you see you will pretty much find like set loading everywhere. We're we're doing this pattern all over the place.

Just look in your codebase. I guarantee you will find that. What's a solution? Adam talked about it or at least I think he did. the slides were backwards there so I couldn't really see but I'm assuming he talked about RSC's where you could essentially just get rid of all that embrace RSC's and which stands for React server components and just do away with all of that you know that th those variables. So essentially all you have to do is await grab the product and then just render it on the page as a server component. But let's say or actually there's another really cool hook which is use transition and this actually takes care of setting that is pending or is loading whatever you want to call it. It sets that variable for you. So you could actually just make your away call in there and then have that variable without having to do all of that boolean juggling.

So this is a very cool React 19 hook that you could use. But of course if you're like oh I actually want to do you know client side state managements you know with fetching data etc. Just use something like well I guess now it's called tanstack query. Use something like that because I know I'll get a lot of hate for this too but it will handle fetching data better than you. Okay it does the whole it even goes beyond useEffect. is using external store under the hood but it handles so much more like every use case you could think of and more. Now forms this is another place where useState comes up very often and you might be thinking like this is also React 101. Obviously if you have a form you will have a use dates for each field in your form and you could just for examples have a first name set the first name and then put that in your inputs. You're like things can't get simpler than this. You bring it into handle submit and then you could do whatever you want with the form data.

But here's the thing. Ever since, I don't know, the beginning of the internet, these form inputs and these form fields and the form itself, they're actually storing that state for you in the component. So you're essentially sort of duplicating efforts. So essentially, we have let's get rid of that useState. We just have a name on there, first name, and then we could handle that form data in handle submit. We have this form data API. Again, it's been around for a long time. And if you really want to be type safe, then use something like sod parse that data. This is something that you should be doing anyway. And it does give you validation superpowers, too. Now, Remix does encourage you to do this too. It has, you know, form actions, capital form, and you know, you could just handle that data right in the Remix loader or remix, whatever they're called, actions.

I don't use remix, but I should. Or you could use Verscell. And Verscell also encourages this pattern with server actions. So I really recommend you use server actions because I think you know you saw in previous talks that it makes it very easy to just capture that form data and basically use it. You could do anything async with it. But of course you might be thinking I have complex form needs. I have validation and masking and I want to debounce and I have all of these things that this simple form, you know, stuff is not going to handle. Use a library. There's tansac form, there's React hook form, there's final form, there's other libraries like that. They will handle complex forms better than you. Again, they're going to handle the edge cases that you haven't really thought of. All right, now let's talk about overusing state because I see these patterns a lot. You might think useState is simple and then you end up using eight and then just things blow out of control.

I call this vibe state management. So so what can we do here? There's there's many reasons why this date explosion can happen.

Play from 7:00

12:00 With immate nested data really easily using mutation-like patterns

But what we could do is first of all it's a habits to just have granular use dates. Just group related data. It doesn't make things that much more complex. You know, now we have everything in a big object instead. We could have a function for handling the change. Or we could use something like immer if you don't want to do all that spreading or dstructuring, constructuring, whatever it's called. You could just use immer and it becomes one line. You're not really mutating here. But with immate nested data really easily using mutation-like patterns. Another one which is near and dear to my heart is when you're doing stepby-step forms, you know, you might have all of these boolean variables and then in order to just change the step, you're basically like a switchboard operator. You're setting some to true, some to false, essentially getting the right boolean to have the right value. And honestly, that's where string and numes come in.

It enforces that you can only have one of these values at a time, which is extremely useful. And if you do this, you are one step closer to making an actual state machine, which I'm not going to talk about at this talk. Save that for another talk. But going further than that, discriminated unions. This is also something that I showed at Chance's talk yesterday. We have a lot of use dates here and also some of these are booleans and you can't really guarantee that some of them are going to be true while the others are false. Like you don't have that exclusivity. So what you could do instead the first step is using a string enum where you could say it can only be one of those values either idle loading success or error and that solves part of the problem but unfortunately we still have no type safe guarantees that we have a data when it's success or we have an error message when it's an error. So that's where we could actually use what's called a discriminated union.

And so a discriminated union allows us using this discriminance value the status to say when it's success we definitely have data when it's error we definitely have this error string and then we could just use that and the lines of code are exactly the same.

Play from 12:00

14:00 You could essentially have I'm using the word essentially a lot

It doesn't really add much more complexity to your code and it makes it more type- safe and robust. All right so another one derived state. So this is something that I see a lot and I guarantee it's in your codebase. You know we might have some state like let's say we're opening up a doughnut shop and we might have derived states where we are calculating things like total items subtotal etc using states. So what does this mean? It means that we have to recalculate those in a useEffect every single time. And this is a huge antiattern because essentially you could just calculate these things directly in render. And I know some of you might be like, "yeah, I already know this, but I'm going to useMemo to be safe." Honestly, don't even bother with that unless you know it's going to be a significant performance impact.

But essentially, you could just do derive states directly in render. All right. So now let's talk about getting further reducing state or reducing logic for that matter. I know what you're thinking. Not this. I know what you're thinking. You know, okay, I hate you state at this point and I'm coming to you showing you something awesome. I know. So this hook exists for a reason. And the reason is managing complex logic. So let's say we still have that same doughnut store. We have complex logic for adding a donut, removing a doughnut. We might want to check the inventory etc. These are things where we don't necessarily want to rely on use states because you state implies you could update the data directly when it shouldn't be the case in here. You want to check the inventory. So I'm I'm sorry for making you actual software engineers but like we are going to do a little bit engineering here you know handle each of the actions or I like to call them events in a simple function and the beauty about this is that you know there's it's still the same over here we're adding a donut removing a donut except we're dispatching events instead of manipulating state directly and you could also directly test this.

So as much hate as useReducer gets and I understand that you know it is somewhat complicated to use it does make things a lot nicer when you reach that threshold of my application logic is too complex for simple use date and also it embraces an indirect pattern of updating state and this is something I could talk about for hours but essentially when you use events instead of directly updating state you get a whole bunch of information you get basically the cause of the change, any parameters related to the change, when the change happened, and essentially a log of traceability. So you could actually see what are all of the events that basically led up to this date instead of the state just being updated from who knows where. All right, so moving on, syncing with external stores. There's this really cool hook called useSyncExternalStore where essentially you give it three arguments. subscribe, get snapshot, and get the server snapshot. And you would be forgiven if you haven't heard of this hook or used it because it is hidden in those other hooks like use debug value and use ID etc.

But this is a pattern that I see a lot too. We have a useState here for calculating something like the window width and useEffect for subscribing to hint an external store or an external source of truth which is the browser API for listening to when the window resizes instead of having that useState and useEffect thing. I'm just spilling my water. Apologies forever whoever stands here next. But instead you could just put everything in a simple subscribe function and then pass that to usync external store and then you have this function that gets the snapshot and things become a lot simpler. Now I actually well one of the first times where I ran into an actual need for usync external store was when I was trying to do the simplest thing which is show a date. How hard could this be? It's a client component. So surely this will only run on the client. Not exactly. So next we'll yell at you for some reason because the server doesn't match the clients. But we're like please I just want the clients.

So sure you could useEffect and useState but there's another very useful pattern which is useSyncExternalStore and that allows you to provide both a client snapshot and a server snapshot. Unfortunately this won't work either. next provides some very beautiful errors showing you that sorry you can't actually do this. So we do have to convert it to a string so it is serializable and it doesn't yell at us for not caching the value or something like that. There's a really good blog post on this if you want to learn more external store is great for when you need something to show or something to reconcile between the server and the client first state. Now, of course, React is going to tell you like, hey, using external story is mostly for non-React code and thirdparty libraries, etc. I'm I'm of this opinion though, but you know, anyway, third-party libraries. So, this of course is a hot topic where, you know, we might think, okay, why would I need a third party library?

And why would I need like global states, etc. Because most of the time I could just get by with use states, useReducer, whatever. So the situation happens more often than not that you want state, useState. The state has special logic like useReducer. I want to share it between my components. So use context. Right now it's causing too many rerenders. So I need to use some sort of pub sub pattern. I hear my flidians. I don't need the entire state. So you know you have a useSelector hook. And at that point you essentially just reinvented Redux. And if you want to know how just talk to Mark Ericson he's in the crowd somewhere. But these external stores essentially work either by you know directly having this global store or having the store provided through React context. There's a lot of options to choose from but since I'm on the stage I'm going to be talking to you about not XState but XState store which is a related library from XState.

So the syntax for this is pretty simple actually and it's meant to be simple because it's only like two kilobytes less than two kilobytes. You create a store where you provide the context and transitions for how the data changes and then you could subscribe to those data changes and trigger events to send. We have this new dot syntax instead of send. So it makes it a lot easier to autocomplete. But you could just pass this into a components using the use store hook and then you could have all of your logic basically inside here. All of the types are inferred for you. So you also have selectors so that you could control how often it rerenders and then you just trigger changes by calling trigger with whatever transition actions or events that you defined. So you know hopefully you hate this less than useReducer because we tried to make all the types inferred for you and make it a nice global or local store experience and also you can emit.

So essentially you have declarative effects over here where you could now useEffect for its intended purpose listen to any emit events that you've emitted and react to them with whatever effects that you want. It handles both doing things in a global store and a local store too. It's crazy, right? So, you could essentially replace useReducer with this. And we also recently introduced atoms. So, you could essentially have I'm using the word essentially a lot. It's a great word. You can ch sorry change that value and you know create combined atoms and things like that. And yeah, so please check out XState store. And the last thing I really want to talk about quickly is local first aid or as we heard earlier this is a sync engine. And sync engines are really awesome because instead of doing the roundtrip of fetching data, you essentially just sync data with your database and things just happen automatically.

Go look at the talk from earlier today on this. It's really great. So there's a lot of libraries that do this for you. So basically, if you want to just have your database and your front end React and sync with each other, there's a lot of magical libraries for doing that and it's a great way to have offline local first state. So just let that sink in. All right, so in conclusion, use dates. You should use it for a truly component internal local state and realize that not all state is UI state and continue to ask yourself is there a better pattern? React 19 introduces so many better patterns. So overall start with you state refactor to better patterns and focus on long-term maintenance. Simple is better than easy. Thank you so much, React Miami.

Play from 14:00

Related talks