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