This site runs best with JavaScript enabled.
Listen on Apple Podcasts AppleListen on Google Podcasts GoogleListen on Spotify SpotifySubscribe via RSS RSS

Tanner Linsley Separates UI State And Server State

Tanner Linsley talks about creating libraries and separating UI state and server state.

Tanner Linsley is the creator of react-table, react-charts, and react-query. Tanner also has a startup called nozzle.io where they track rankings in Google and do cool things around technical SEO.

A lot of Tanner's libraries were born out of necessity at nozzle. We often reach for abstractions that were built to solve problems that we don't have and that ends up creating awkward problems for us. We wind up with poor performance or a bigger bundle size than we need, so having a custom made solution can be a good thing.

Your UI state is not the same as your server state and they should be separate things. By keeping these types of state separated from each other you can simplify your app. Server state is different enough in structure, persistence strategy, consumption, and lifecycle to be managed in smarter systems that are configurable to an apps needs of data freshness.

Homework

  • Resources

    Tanner Linsley

    Tanner Linsley


    Transcript

    Kent:
    Hello, friends. This is your friend Kent C. Dodds, and I'm joined by my friend Tanner Linsley. Say hi, Tanner.

    Tanner:
    Hello.

    Kent:
    Hey, Tanner and go several years back when a mutual friend introduced us. Well, actually how long ago was that? That was like forever ago. This was when your hair was long and curly and stuff.

    Tanner:
    Yeah, boy that was a long time ago. I think it was actually almost six years ago.

    Kent:
    Oh, my goodness. This is back in the end of the JS days. Good times. Good times. So yeah, so Tanner and I, we actually just went out to lunch a few ... Was it last week or two weeks ago? We got into talking about UI state and server state. Tanner has some really opinions that resonate really well with me. And so I said, "Hey, let's turn this conversation into a chat with Kent," and so here we are. But before we get into that, I'd like my friends to get to know my friend Tanner. So Tanner, could you introduce yourself a little bit?

    Tanner:
    Yeah, I'm Tanner Linsley. I am probably known for doing a lot of open source libraries, and I'm somewhat addicted to open source. I have a startup called nozzle.io. We track rankings in Google and do a bunch of cool things around technical SEO. And then on the side, I like to build a lot of open source libraries in JavaScript.

    Kent:
    Yeah, well you do a lot. Some of the things that people might know you for, react-static. You're the creator of that react-table, react-charts.

    Tanner:
    Yep.

    Kent:
    What else is there? Is there anything ... Well, we've got react-query we'll talk about today. Is there anything else I'm missing?

    Tanner:
    No, I think there's a few, but they're not as popular. I have like react-location. There's even a hook that it's kind of like drop down or downshift.

    Kent:
    Oh, really?

    Tanner:
    Yeah. But it's not used a whole lot. I have so many libraries. I actually had a lot more and I had to prune a lot of them off because they were just taking up too much time. Oh, react-form is another one. It kind of goes unmentioned because there's so many great solutions around forms out there already. It doesn't need a lot of attention, but it's there.

    Kent:
    Yeah. What motivates you to create libraries? Especially in spaces like forms or you mentioned you have a hook that's like downshift. What motivates you to create different libraries rather than contribute to existing ones or just use the existing one?

    Tanner:
    Well, usually it actually starts out I try and use existing libraries as much as I can. I don't like to create more work for myself.

    Kent:
    Yeah, [crosstalk 00:02:49].

    Tanner:
    Whenever I can, I'm going to try and use other libraries. But I do create a lot of libraries, I guess, because I'm very opinionated about APIs and developer experience. While I may not always be correct, I guess, about those opinions, I have them, and I like to enjoy the software that I use. So if I see a problem, I go out and create a library just to ... Even if it's just to have fun and learn more about the concept. That's actually how I created the select one was I loved downshift. I wanted to know how it worked. So I thought what better way than to just go build my own. A lot of these problems come about mostly because of nozzle. Building a lot of like dashboarding and admin utilities around forms and just managing CRUD objects. And yeah, there's just so many different challenges that come when you're building like a SAS application, like nozzle, that I want to be able to solve those problems in a way that I understand. So a lot of open source libraries I have were born out of the necessity at nozzle.

    Kent:
    Honestly that's how open source libraries should be born is out of necessity, so I think that's great. We often reach for abstractions that were built to solve problems that we don't have, and then have maybe been retrofitted to solve the problem that we have. That ends up creating awkward APIs or, yeah, just problems. We wind up with poor performance or a bigger bundle size than we need or whatever. So having a more custom-made solution can be a really good thing. React-query is a really good example of this as well because there are so many libraries for managing asynchrony and in particular like getting data from a backend as the biggest use case for that kind of asynchrony stuff. There are countless solutions to this problem, but you built your own. Can you tell me a little bit about why it was that you decided to build your own and what really motivated the creation of react-query?

    Tanner:
    Yeah, absolutely. I think something that everybody does, like you said, is asynchrony. A lot of times asynchronous data, at least in my experience with all the apps that I've been building, has just been like communicating with some type of a CRUD server. It's either like ingesting and consuming CRUD objects or mutating them. There's been so many solutions, I guess you could say around this concept from the beginning. Even just from the very beginning when I started programming, I noticed patterns around this, even in Angular with creating services and trying to manage these objects. When I came over to React, it felt similar. There wasn't really like a specific way on how to do this, a prescribed way.
    And so I found myself jumping around to a lot of different libraries. Like I jumped on the Redux train for sure and installed my Redux thunk and started just making requests and stuff. This problem for me personally has been around for a really long time, and I've never really been happy with it. Like Redux was okay and it got the job done. The context API came out, and I tried to start managing my state locally and in components with render props, which honestly was not half bad. Then I started trying other libraries that abstracted that away, like just react-async, and there's even libraries that come fully baked in with like GraphQL and stuff. I was really interested in all those solutions because they were just kind of dropped in and it just works. So I got really interested in this area of libraries pretty quickly because it's a really difficult problem to solve.

    Kent:
    What makes it so much of a difficult problem that I can't just make a React provider like a context provider for each resource on my backend. I have a user provider, I have a post provider, I have a comments provider or whatever. Like I just have one provider for each resource. That seems pretty simple. What is it about this problem that makes it more difficult than just loading up with a bunch of providers, one per resource or whatever?

    Tanner:
    Yeah. It's interesting you bring up the provider pattern. I think we're all used to that because of all of the UI state that we see with boiler plate demos and things like that where we're just toggling to-do's on and off and things like that. But at server state, if we were talking about a real to-do app that may be synchronized to dos across multiple users and things like that, those to-dos are coming from a server. The life cycle of data on a server is absolutely nothing like the life cycle of local data that you have in your app. It's actually completely out of your control most of the time. And it's potentially and usually changing without you even knowing about it. So it's constantly on the move and it's synchronous.

    Kent:
    Excuse me for interrupting, but it sounds like you're saying that servers, the state that I have on the front end, that is stuff I got from the server, what I actually have in state, but I have a cache. I have a cache of what's on the backend and a cache invalidation is one of the hardest problems in computer science. And so that's why like you said, at any point in time, the state that I have in the front end could be out of date. Because it's a cache, it's not actually state. It is just a cache upstate that lives somewhere else.

    Tanner:
    Absolutely. It is simply a cache. So when you're talking about making providers for all of these things, I mean really what we're talking about is a massive spectrum of caching trade-offs, right? It's a spectrum of how much performance and work are you willing to put into a cache to keep it up to date versus how stale are you willing to let your data become before you need to update it again?

    Kent:
    Mm-hmm (affirmative). I guess to add on to that, how willing are you to just invalidate all of the data you have and go do another round trip to the server? Whereas there are lots of libraries that will try and piecemeal update the state that you have on the front end and say, "Oh, I think like this one property of this one resource is stale. So I'm going to go request that property of that resource just to get that little piece." Or instead of all of that complexity, we just say, "Okay, this resource is stale. It may be just one property, who cares? I'm just going to say the whole thing's stale and get the whole thing back from the server."

    Tanner:
    Right. Yeah. It definitely comes down to how accurate is the data that you're getting from the server too. You don't want to have some of your state in sync. You want to have all of it right. It's just difficult problem to solve. It's funny because I see people jump for managing this in Redux with a thunk or even just the most basic promise hook that probably if you're used to hooks, you've written this hook a couple of times, where it uses an effect to fire off a promise, and you're keeping track of the data and the error, and the prom state, and everything.
    That's great. A lot of people think, well, that's good enough. Then the question of, well, when do I refetch that? When do I refetch that asset? What if you have multiple things, multiple components on a page that are requesting the same asset. Now you have problems of scale too, and now you are in charge of of refetching and invalidating all that data on your own. That's something that we're just not good at. We're not good at remembering to do that and remembering all of this, the use cases when we need to do that.

    Kent:
    Yeah. Especially if you're requesting the same data in two different places, then you could have one that's up to date and one that's stale, and then you wind up with a really awkward user experience.

    Tanner:
    Yep, exactly.

    Kent:
    You used a whole bunch of these other query libraries or asynchronous UI libraries, and they didn't quite suit your need, or fit the API you were looking for, or your use cases, and things. So how did you finally come up with react-query? Maybe this is the part where you can tell us a little bit about what it is.

    Tanner:
    Sure. Actually, one of the libraries that I did try that I ended up not using just for other reasons, but it was Apollo. Apollo made me start thinking about how you know this life cycle of server data exists. I ended up not going with Apollo mostly just because it's tied very strictly to the GraphQL ecosystem. Afterwards I started finding some other libraries that were imitating Apollo a little bit, but most of them revolve around a ... It's like a stale while revalidate model, right? Basically what it is is you're caching data all the time, but anytime that data is displayed you're invisibly going and refetching that data and updating it to the user. What they see on the page may be ... It may be potentially out of date but only for a split second, only for the amount of time that a query is being refreshed.
    That's the concept behind react-query is that all you have to do is define an asset, like a query, and a way to fetch the result of that query. Then out of the box that mostly takes care of everything else. The very first time you see that query, it's going to be in a loading state, but every time after that it's just pulling from the cache. You never see a loading state afterwards. You're always seeing the last known piece of data that was received from the server. But it's also every time that you're loading it, or changing it, or looking at it, it's optimistically going and fetching a new version of that data and invisibly updating it. So there are situations where you might see a small flash of data that may have been outdated. If all you're doing is using the query part, then that's totally fine. Seeing some stale data for a split second is worse than seeing a loading screen over, and over, and over.

    Kent:
    Is there any way for me as a user to hook into a pending state where I can ... I'm fine showing the stale data, but I want to show some indication to the user that it is stale, so it's like grayed X or something.

    Tanner:
    Absolutely. There's a hard loading state that you can use to show that like I have nothing to show so I'm going to show loading. There's also an is-fetching state that's provided to you that even if it's cache and it's loading in the background, you can use that as fetching state to hint to your users that, "Hey, what you're seeing is a preview," show them a little spinner up in the corner or something, and it eases your mind as a user like, "Oh, okay, this is being updated."

    Kent:
    Yeah, that makes a lot of sense. With this strategy, I think one thing that I want to get into before we get too far into some of the implementation and stuff like that, is I just really, really want to drill home the value that is in the phrase or in the concept that your UI state is not the same as your server state and the those two should be separate things.

    Tanner:
    Absolutely.

    Kent:
    Can you talk about that a little bit and where that value is?

    Tanner:
    Oh yeah, for sure. In fact, I'll use nozzle as an example. In nozzle itself, in the SAS application, there's so much state going around everywhere and up until I created react-query, all of that was just held in global contexts and passed around through context [crosstalk 00:15:54].

    Kent:
    Can you give me some examples of the different kinds of states [crosstalk 00:15:59]?

    Tanner:
    Absolutely. It's all workspace oriented, so there's workspaces and then teams are in a hierarchy of workspaces. And then teams are a CRUD object that own a bunch of other objects like brands, and keywords, and polls, and just all of the tiny CRUD objects that make nozzle run. they're all in this hierarchy and they're relational. I basically had a state in my app for all of those things.

    Kent:
    Unless I'm mistaken, that's all server state, right?

    Tanner:
    It is. We probably have about 12 to 13 crud objects, like types, that are server state types.

    Kent:
    So what are some examples of UI state then?

    Tanner:
    I think the most common ones and the ones that we also have in nozzle are like keeping track of toasts and popups. Dark mode is one that probably everybody has now in their app. Another form of state that isn't kept in your global state usually, but it's like routing state is another type of client state.

    Kent:
    What would you say is the distinguishing characteristic? If I'm going into my app and I'm trying to separate between UI state and server state, when I'm looking at it, how do I identify it as one or the other?

    Tanner:
    I don't know. Definitely there's a spectrum, but I think a good starting point is probably asking yourself where has this data persisted and how is this data updated? Because I think if you look at a piece of data and it's persisted either in local storage or it's just not persisted at all and it's just initialized every time you load up your app, it's probably a good indicator that's like UI state. If you look at something and it's like, "Oh, this has persisted in some offsite cache or in a SQL database somewhere, or a Mongo, or whatever, I think that is an easy indicator that this is likely something that you don't own and you don't have control over. I would label that as server state.

    Kent:
    Yeah, that makes a lot of sense. Yeah, I guess it's really key to realize that those two are different types of state and that by keeping them separate from one another, you can simplify your app. You were telling us a little bit about your experience at nozzle in the process of separating these two and what kind of value that brought to you, and I interrupted you. So sorry about that. You can continue.

    Tanner:
    Oh no, not at all. Yeah. We just have so many types going around, so many things, and it was easy to start treating the server state and the UI state in the same way. Just sticking in some context to the top of your app and managing it. When it really became important to me personally was when we started opening up our app to teams who were working on the same CRUD objects together. Definitely not in real time, but throughout the day they may be editing the same objects. This is one example of where getting out of sync can become really easy for applications that are interactive, that share assets between users.
    And that's something that I didn't want. In fact, it became very mission critical that our users don't see out of date CRUD objects in their UI, giving them some false sense of security that they're editing the latest thing. It became very mission critical to reduce the out of sync time that a user was experiencing. And that's what led me down this path to finally coming to the realization that server state, although you can manage it on your own and re-request it manually, we really needed some more robust tooling around managing server state and mitigating all of the risk that comes with managing it on your own.

    Kent:
    Yeah, yeah, absolutely. At that point it just becomes a trade off of caching and where you started getting into a situation where, yeah, if this data stays this way for as long as they're on this page, then that's not a big deal. But actually when people start using and manipulating the same data at the same time, then that becomes a problem. It's no longer acceptable and eventually you move from the data is this way until they navigate away, all the way to the Google Docs model where you can see people editing and stuff. So there's a spectrum there. Where would you say most apps fall on that spectrum? Like a typical application that has multiple users manipulating that data.

    Tanner:
    I don't know. I see a lot of applications that offer business value to people or value usually have some aspect of collaboration, which is where their value comes from. I think most applications that are built as apps or SAS applications are probably going to have some amount of this situation where objects are being touched and manipulated by multiple people, where they're being shared. Maybe not to the degree of a Google Doc, right?

    Kent:
    Right.

    Tanner:
    Where everything is shared. But at some point, I think if you're modeling your data in a relational database or even any database where you're linking things together, you're going to run into this problem. I think early on it's easy to deceive yourself that this isn't going to happen. I love the example that we talked about during our little lunch was everybody loves the Pikachu, the Pokemon, Pokey Decks website example. And going and getting the stats for a Pokemon is a good example of thinking like, "Oh, this Pokemon's never going to change." So why would I ever need to reinvalidate it? And that's really the basic use case everybody always thinks about. It's easy to deceive yourself into thinking that everything's going to be that way. But once you start letting other people edit Pokemon and update Pokemon whenever they want, now you've got an issue.

    Kent:
    Yeah, absolutely. So there's absolutely a spectrum there. I think there could also be a danger of saying, "Well, we got to put WebSockets on every single one of our resources or whatever because we just never want to have stale data ever. There's definitely trade-offs there.

    Tanner:
    It's interesting you bring up WebSockets because when I implemented react-query and nozzle, even without WebSockets or any type of real time events, it was already so much more up to date just by using the stale wall revalidate model. All it was is that the revalidation events were user-driven instead of server-driven. Just out of curiosity, I went ahead and implemented WebSocket events coming from our database so that now the reinvalidation or the revalidation events are not even user-driven anymore. They come directly from the server as soon as something's manipulated in there. If a query is active to that thing, it will get updated immediately. User doesn't even have to intervene. So even in our tooling, there's a spectrum of how far do you want to go to ensure that something's never out of date?

    Kent:
    Yeah, absolutely. It sounds like that's just two steps right next to each other on that spectrum where you can say, "Okay, anytime the user does something that invalidates this object, we're going to go request it. But oh now we could have things happening on the backend that could affect what the user is seeing, the accuracy of what the user is seeing. So we're going to start having the server let us know when things need to invalidate. Is that the cache-busting strategy that you recommend or at least for that type of scenario is say, okay, WebSocket? Like you need to have a WebSocket to tell the server or I guess you could do polling, but yeah.

    Tanner:
    I was just about to say polling is probably the easiest way to get started going down that route out of the gate using your interactions and mount and render events to trigger the invalidation. That works really well. But I wouldn't say jumping to WebSockets is your next step. Polling is something that depending on if you want your users to be polling all the time, it depends on devices and how much work you want to put on your users' devices and their bandwidth data.

    Kent:
    Right, sure.

    Tanner:
    Yeah, you can implement polling as well. For a SAS application where we're not delivering to low power devices on LTE networks. We're mostly pushing to desktops, and offices, and stuff. Polling is not a big issue and you can turn on polling every five or 10 seconds if you wanted. I know a lot of companies that use polling and that's all they really rely on. I actually think, I'm not totally sure, but I think Zites dashboard uses some type of polling to keep things up to date there.

    Kent:
    That would be kind of ironic considering Guillermo is the author of socket.io. But yeah, that's [crosstalk 00:26:03].

    Tanner:
    I believe that they're looking towards doing some SOCAN integration. Obviously their scale is so large. It's easier said than done, but they've had a lot of success just going with a stale wall revalidate and polling here and there, I think.

    Kent:
    Yeah, absolutely. Just to clarify for folks, so WebSockets is like a connection between the browser and the server that just stays alive all the time. The server can just push updates whenever. Polling is there's no living connection between the front end and the backend, but every now and then the front end will make a request to say, "Hey, do you have updates?" I know that you could have WebSockets send the updates and say here are the things that have updated. And then on the UI you just go and update the parts of the objects and the resources that have changed. But I don't think that's the approach that you recommend. Instead like your, whether it's polling or WebSockets, whatever, it just says here are the things that now need to be updated. So it's a lot smaller response that you get back and then you invalidate those queries and then react-query takes care of fetching those again. Is that correct? Do you want to expound on that a little bit?

    Tanner:
    Sure. As with anything, there's a spectrum. I believe there are use cases for sending payloads and WebSocket events. But at least for our use cases where we're just building management dashboards and admin dashboards that need to be relatively up to date, we didn't want to go through all the hurdles and the overhead that it takes to manually take data sent from the server and have to go and inject it into every nook and cranny of the app where it's being used. There are systems in a lot of the existing tools that go into that realm a little bit. Apollo comes to mind with their simple cache and they will optimistically take fragments from GraphQL, and inject them into certain active queries, and even some Redux implementations like Redux data, I think.
    Some of them do the same thing, but that's not the approach that I took, like you said. Mostly because ... There's a few reasons. One of them is I like only having one way to do something and that includes how I get my data from the server. Introducing two pathways for data to come into my application doubles a lot of complexity in my opinion. That's one reason for going with the invalidation approach. And the other reason is that side effects. Side effects happen all the time on the server. Just because you're getting an event from the server that says here's the new object, it doesn't necessarily mean that you have any other objects that may have changed along with it. For me, it's all about mitigating risk at the cost of fetching.

    Kent:
    You're willing to fetch more data in favor of reducing that risk.

    Tanner:
    Exactly. Some people might not be willing to make that trade-off, but I think most can for applications like this. In our situation, we just received very, very small events about what types of CRUD objects are changing, those types of changes. And we actually use those same messages for notifications, and popups, and things. So and so edited this team, but we also use that to invalidate any active queries to any objects or any queries that would pertain to that type of CRUD object. If a new team is created, then we're going to have to reinvalidate every query that's calling on all teams. But if a team was edited and we're not looking at any queries that have all teams, we're just looking at a single team, we only have to update that one query. A lot of it goes into coordinating what queries are active on the page at any given moment and only making requests for things that are being displayed on the page. Right?

    Kent:
    Right. Yeah. It's so interesting. There's so much more I want to talk about. We're already out of time, but I did just want to ask one thing really quick and that is how much of this work that we're talking about here, like cache and validation, how much of that is happening within react-query and how much of that is stuff that you add on top of it? So what exactly is react-query doing for us?

    Tanner:
    React-query out of the box is going to be handling all of the invisible updating for you. If you're just querying things, every time that a new query pops up or some query variable changes, it's going to be automatically handling those refetches for you and without you doing anything else.

    Kent:
    And it's not just fetches, right? It's anything asynchronous. So it could be geolocation too, right?

    Tanner:
    Yep. Anything that uses a promise or that could be turned into a promise. Yep. And then the next level is it's declarative refetching through mutations. So again, similar to Apollo, you can wrap a promise that's going to perform some mutation on your server. If you know what that mutation is going to do, you can tell the rest of react-query to invalidate certain queries when that mutation succeeds. It's a little bit of work, not much, but just to define declaratively, "Oh, I'm adding a new to-do, so I need to invalidate to-do queries." That's not a lot of work there. Yeah. And then there's a lot of other little nice things that are happening under the hood like detecting if the tab is active or retrying. If queries fail, it will do invisible retries for a little bit with some exponential backoff. There's a lot of nice things under the hood that make it a very hands-off library.

    Kent:
    Very cool. Well, I'm looking forward to using it. I haven't had the chance to use it yet, but I like the ideas behind it. I think it's applicable to a lot of people. As cool as GraphQL is and technically you can use react-query with GraphQL, right?

    Tanner:
    Yeah. You're not going to get the granularity that you'll get with something like Apollo or Dracula. It doesn't have an AST or a parser that's managing all the little nested pieces of data you can get with GraphQL. But if all you're interested in is caching the whole result in and of itself, yeah, you can definitely use it with GraphQL and get 80, 90% of the way there.

    Kent:
    Yeah. Well, and it's the one app that I use GraphQL and that was like a production consumer facing app. It was actually a pretty small. The bundle size was really important and there was no way we were going to use Apollo for that because it is enormous. It is so big, way bigger than React. So yeah, I'm looking at react-query right now. It looks like it's 11.8 kilobytes [inaudible 00:33:43] which is stupendous. And then [inaudible 00:33:45] it's like 3.8. Yeah, that's another thing to consider as well.

    Tanner:
    Absolutely.

    Kent:
    For like simple GraphQL, whether it's GraphQL, or REST, or whatever for simple use cases, it seems like a no brainer honestly.

    Tanner:
    Yeah. I would say don't forget that most of the internet is still implemented in REST, and we're still going to be using promises to communicate between mostly internet for a while. So it's good to have a tool out there that isn't locked into something like GraphQL.

    Kent:
    Yeah, absolutely. Well, cool Tanner, it's been such a pleasure to chat with you. For the homework, we're going to give you, Tanner and I decided that we want you to take an inventory of your state, so go through your application, maybe write it down or whatever. That probably would be a good idea. Write down all of the different pieces of state you have. So like we have this, the user state, the currently locked in user, we have the users' posts. Those kinds of things are often the types of things we think of when we think application state. But then don't forget you have a dropdown that is open state, and you have your theme state, and all those different states.
    Write all the different types of state that you have in your app and separate it between the list of UI state and server state. And Tanner, one thing that you didn't mention in the conversation I'll just mention here is, when you moved things at nozzle over to react-query, you found out that there was just so little UI state left that react-query wasn't managing. That it just made your application state way easier to manage. So I think that people will find like just take this inventory and look at that giant list of server state, which is actually just a cache, and see if you can separate that from your UI state. I think you'll find it'll make things a lot simpler to manage. Any other thoughts, Tanner?

    Tanner:
    No. Can't think of any.

    Kent:
    Cool. Where can people find you on the internet?

    Tanner:
    Well, Tanner Linsley is my handle everywhere. T-A-N-N-E-R-L-I-N-S-L-E-Y, GitHub, YouTube, Twitter. Those are most of the places I hang out.

    Kent:
    Cool. All right. Well, thanks for spending 30 minutes with us and we'll see everybody later.

    Tanner:
    Cool. Thanks, Kent.

    Kent:
    Bye.