Oh wow it's been quite some time since I last updated this series. You don't need to read the previous articles, just read the last paragraph from the intro to the series to set your expectations right about what this is.
On the flip side of me taking so long to write this article, coming back to it months after having written some parts was the same as coming back to code you've written weeks ago: nothing made sense anymore. So I made a table of contents while thinking about how to restructure it in a way that makes more sense, and you get that bonus table of contents here for free:
-
UI State
- Local
- Global
-
Server State
- aside: Data fetching in javascript (fetch API)
- Data fetching in react
- loading, errors, data
- race conditions
- Caching
- React query
-
Example
- (serverless) backend aside
- another aside about REST vs GraphQL vs tRPC
- Code and demo
- last aside ever: animations
In the tradition of past articles, let's start with suggested reading. This is the only one I felt I needed to read to get up to speed with state management:
https://leerob.io/blog/react-state-management
One major point to get from that article is that there are different types of state, most importantly two:
- UI state
- Server state
UI state
Further divided into local and global state.
Local State
The react docs have been rewritten since the last time I was writing an article in this series. Whether you're learning react for the first time, or need a quick refresher, that is the perfect excuse to go through all of the new docs site, or, at least, just the managing state part. In my opinion, state is react, so you'd be doing yourself a favor by learning and understanding it very well.
Global State
Have you ever felt like quitting programming and becoming a farmer in a place far away isolated from the rest of civilization? If not, now you, too, can experience that feeling by setting yourself on a quest to find the best way to handle global state in react apps.
To avoid that feeling I suggest you follow the advice from Lee's article above:
- Start with local state
- When your app evolves to become like figma such that "... [interacting with one element] affect other elements outside of its "local" state, or where the component is rendered", reach for
useContext
so you can keep state in one place and read/write to it from anywhere, and maybeuseReducer
to make the writing part easier. - "If optimizing requires a state management library, you can track that metric (e.g. frame rate), measure it, and verify it solves a real problem." In this case pull for a granular library solving that very specific use case.
Also, I particularly liked one of his suggested solutions to "ask your tech lead." If you're a junior dev you shouldn't have to make decisions like this on your own.
With all that said, if for some reason, you must still follow that route that leads to you in a farm, here's an entry to that rabbit hole that I discovered recently (at least recent as of july 2022 when I wrote about 99% of this blog and then procrastinated on finishing up and publishing it for another year):
https://frontendmastery.com/posts/the-new-wave-of-react-state-management/
Server State (Data Fetching)
aside: Data Fetching in JavaScript
Here's a word I bet you haven't heard in a long time: AJAX. Or maybe you've never even heard it before. But it's what got this whole JS-industrial-complex™ started.
We all know browsers can make web requests. It's what web browsers do. You type kosovahackersclan.com (okay maybe you don't, but I did... around 2005... a lot) in your address bar and your browser (aka the client, aka the user agent) makes a request on your behalf to that l33t forum's server. The server runs phpBB to get the forum posts out of MySQL, marks them up in HTML and sends it back to you. You want to know if the user named n0thinG responded to your thread asking for some warez? You press the refresh button.
But what if the JavaScript you already loaded in the page could also send requests on your behalf, without you having to type in a new address, click a link, or hit that refresh button? It could do that in the background (asynchronously). It could do it every second. And it could update the current page's markup with the new response every second without the browser updating the whole page. What would that result in? Infinite warez!!! Or a web page that shows charts of real-time stock prices. Or a web app that shows you a feed of your friends' posts, each one complete with stats of how many likes and comments it got, while simultaneously overloading your sensors with everything it can crunch in one screen from a live chat window to dopamine-inducing red counters of how much engagement your own content got.
As it turns out, if you're building an app like that, it makes sense to go full Single Page Application mode (get new data in background, only update the relevant parts without refreshing the whole page) and even try an experiment where you throw out the old markup each time you want to make a change and generate completely new markup as a pure function of the new data you got.
As you know, Facebook did exactly that with React and as they say the rest is history. Now, with the recent one eighty of "server first", this whole way of doing front end engineering might become history in a different way, but that's a topic for my next article in the series.
JavaScript Fetch
So, how do you fetch data in the background from javascript? You use the aptly named Fetch API. Straight from the MDN page:
fetch("http://example.com/movies.json")
.then((response) => response.json())
.then((data) => console.log(data));
Pretty straightforward, especially compared to what we had before. What we had before was another browser API called XMLHttpRequest
and many libraries that wrapped around it to offer cross-browser compatibility and much better APIs.
The most famous of those libraries, axios, is still widely used today. Now that Fetch is available in all modern browsers, you would think we'd get rid of libraries and just use Fetch directly. And that's what we did for a while. But to come full circle again, new libraries popped up that wrap around the modern Fetch API, and there are again good reasons to use those.
I will use ky later on, and you can check its readme to find out about some of those reasons in the list of "benefits over plain fetch
". This article, which recommends another library called wretch, does an even better job at highlighting what's wrong with using Fetch in its most simple form like the MDN example above.
Here's what safe data fetching with ky would look like:
try {
const data = await ky.get("http://example.com/movies.json").json()
console.log(data)
} catch(error) {
// handle error
}
Data fetching in react
This article, which is even linked to by the react docs, is a good place to start reading about data fetching in react. You don't need to get into all the advanced topics it discusses. What I liked most about it is how it starts as simple as possible like this:
// you fetch data in `useEffect` and update state with results
function App() {
const [data, setData] = useState([]);
useEffect(async () => {
const result = await fetch('url here');
setData(result);
}, []);
return (
<div>
{data}
</div>
);
}
What's wrong with this example? Well, everything, according to recent tech twitter. But here's how I feel about that recent discourse (again, recent in july 2022 when I wrote most of this blog):
But seriously, I might revisit this topic in the next blog about frameworks and rendering strategies. But for now, I'd like to focus on a few fundamental shortcomings of this simple approach that Robin also addresses in his article.
loading, error, data
The famous pattern of: loading, error, data. You'll notice this pattern everywhere, whether you're rolling data fetching on your own or using different libraries from React Query to Apollo GraphQL. When you go out of the house remember: Phone, Keys, Wallet. When you do data fetching in react remember: Loading, Error, Data.
Adding loading to our simple pseudocode:
function App() {
+ const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
useEffect(async () => {
+ setLoading(true);
const result = await fetch('url here');
setData(result);
+ setLoading(false);
}, []);
return (
<div>
+ {loading ? (
+ Loading ...
+ ) : (
{data}
+ )}
</div>
);
}
We already talked about error handling when fetching data with ky. Here's what it looks like when we combine it with state management:
function App() {
const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null)
const [data, setData] = useState([]);
useEffect(async () => {
setLoading(true);
+ setError(null)
- const result = await fetch('url here');
-
- setData(result);
+ try {
+ const data = await ky.get("url here").json()
+
+ setData(data)
+ } catch(error) {
+ setError(error)
+ }
setLoading(false);
}, []);
+
+ if (error) return (<div>Error: {error.message}</div>);
return (
<div>
{loading ? (
Loading ...
) : (
{data}
)}
</div>
);
}
Race conditions
I can't do a better job at explaining this than this exercise from the react docs. If the hash part of the link doesn't work it's the 4th exercise in the challenges section. If you click show solution it explains very simply how a race condition bug happens and how to fix it.
In our running example it goes like this:
function App() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null)
const [data, setData] = useState([]);
useEffect(async () => {
+ let cancelled = false
setLoading(true);
setError(null)
const result = await fetch('url here');
setData(result);
try {
const data = await ky.get("url here").json()
-
- setData(data)
+ if (!cancelled) setData(data)
} catch(error) {
- setError(error)
+ if (!cancelled) setError(error)
}
setLoading(false);
}, []);
if (error) return (<div>Error: {error.message}</div>);
return (
<div>
{loading ? (
Loading ...
) : (
{data}
)}
</div>
);
}
Caching
Caching is one of those big topics in computer science. As I was searching for the easiest way to explain caching I came across the slides for a talk on the subject, and I extracted the following excerpt from it:
[...] The speaker asked the audience “What’s 3,485,250 divided by 23,235 ?” Everyone fell silent. Some people pulled out calculators to do the maths, and finally someone yelled out the answer after a few seconds.
Then the speaker asked the exact same question again. This time everyone was able to immediately call out the answer.
This is a great demo of the concept of caching. The initial time-consuming process was done once, then after that, when the same question was asked, the answer was readily available, and delivered much faster.
Ah! Don't you just love these types of explanations? As always though, that's just scratching the surface and it gets more complicated as you go deeper. For once, this is only talking about one kind of caching — and there are all kinds of those for you to easily get lost in whichever way you want. To narrow it down a bit, lets look at a proper definition of caching, this time from wikipedia, and focus on the second part of it:
In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.
Caching of a computational result (memoization)
Throughout this article (and this series) I've advocated being practical, and only learning the minimum that you need to correctly finish the task that is immediately in front of you. But the beauty of memoizing a recursive fibonacci function was just too much for me not to geek out on. I invite you to also channel your inner geek and watch this 4 minute walkthrough of how that works.
I'm always amazed by the huge difference that memoization makes here. Without memoization, every time you'd want to calculate the millionth fibonacci number you'd have to recursively call the function as many times as needed to calculate all the previous fibonacci numbers before it (2*F(1,000,001)-1
times to be exact). With memoization, once you've got the previous results in the cache, it's just one call that returns the sum of the earlier computations of F(999,999)
and F(999,998)
.
Caching of data previously retrieved from an underlying slower storage layer
And to complete our geeking out field trip let's start this part about the second type of caching with this famous video:
No, of course you don't nEeD To kNoW ThEsE NuMbErS As a pRoGrAmMeR. It's just a cool thing that exists and I thought it would be cool to link to it here.
Aaaand back to front-end development. How does this all relate to the type of caching we need to worry about daily? Well, this second type of caching is exactly what we must keep in mind when fetching data in our web apps. You can probably imagine how the simplest scenario goes:
The first time the user needs the data, they're gonna have to make a network request to get it, no way around that. After that though, if they request the data again, and we have good reason to believe the data hasn't changed since the last time they got it, it would be a waste to make a new network request (going to the "underlying slower storage layer") instead of serving a copy we previously stored in their browser (do I need to give you nanseconds to illustrate the difference it makes for the user?).
And that's the type of caching that we most frequently have to implement as front-end developers. That's how it starts at least. It starts deceptively simple like that. Makes you think "I can code that in five minutes". And you can. I'm sure you can. But you shouldn't. Why? Because it's a dead end. Or more precisely a "death by a thousand cuts". I googled to make sure I'm using that phrase right and here's one explanation that came up: "a slow, painful, demise caused by the cumulative damage of one too many ‘seemingly’ minuscule problems". And here are some of the not so minuscule problems you'll have to think about when implementing caching on your own, as described by people who (I imagine) think about it daily.
As you may have gathered by now, unlike for the other problems we encountered with data fetching in react, I won't try to implement a solution for caching on my own and use react query instead. Do I think I have convinced you to do the same and never implement a full caching solution from scratch in a production app? I don't know. A while ago I would've thought that it's one of those things you can only learn by going through it yourself, but the popularity of react query gives me hope that's that not the case anymore.
React Query
I just realized that this whole section on data fetching with react reads like a giant ad for react query. Here's a list of problems I had, and TADA, react query solves them all!!! Order now!
Lol I swear I didn't write it that way on purpose, but it's true, react query solves all the problems I listed above — especially caching; oh my god caching — and that's not even what I liked most about it (kill me now I just realized I'm a react query fanboy).
What I liked most about it was the declarative way of doing data fetching and how much it simplifies the code. Even in the simple example I'm using the code was simplified to the point of not needing the useEffect
anymore, and the shift from fetching the data and then imperatively updating your state to having the data fetching happen as a result of you managing your local state was visible.
Speaking of the example.
Example
As I mentioned in the beginning, when I was almost done writing this article I reorganized it in a way that I think makes it easier to read. That's why I moved everything that had to do with the example project that I've been working on since the start of this series to its own section here at the end of the article. Everything up to here was theoretical enough that it could (hopefully) benefit anyone wanting to learn about data fetching and state management in react and hasn't read the previous articles in this series. This last part is about how I added data fetching to this website.
By the end of this section I will discuss a bit about the data fetching and state management part of the example, but before that I had to write three (not-so) short asides about other stuff that I had to do before getting to that part. The first of these was building a backend for the products.
I left this part at the end of the article, but I still want you to read it though, that's why I will lure you in with this artisanally-crafted-with-my-own-two-hands pepe silvia meme.
APIs amiright. Client-server architecture. "How the web works" youtube videos. "Describe what happens when I visit google.com" interview questions.
All good to know, fundamental stuff — but this is 2023.
(serverless) backend aside
I've mentioned in previous articles that I like "serverless" for many reasons; and one of them is how it changed our way of thinking about the backend. Whereas before we would think of the backend as one big monolithic box (literally called a box to refer to the single machine running everything like the web server, the database server etc.), our thinking now has shifted to picking and choosing only the services we need. Your app needs authentication? Find out who offers authentication as a service. You need a database? Find a serverless solution for that. Need a place to run some code, storage for images, a proxy for caching, load-balancing or any of the other traditional backend needs? Same solution in all cases.
So, what services do we need for our sample website? I'm going to use the products section to learn about data fetching and state management. So we need:
- a database to store the products info, and
- a place to store the product images.
Here's a quick overview of how I think of some of the most popular solutions that fill our database needs.
- Amazon AWS: The biggest of the original cloud computing platforms (alongside Microsoft's Azure). Now, I've talked about different learning styles before, and IME I don't find the AWS docs to be as bad as everyone says. IMO if you're learning about backend concepts for the first time you're gonna have a steep learning curve ahead of you no matter which serverless platform you choose. But that's not a popular opinion. In fact the opposite (that AWS is very hard to learn) is so wide-spread that it has at least two important implications:
- You can make a career by learning it well and earning certifications to prove it.
- A plethora of other services have proliferated who use AWS under the hood and their entire business model is based on you paying them money to not have to learn AWS yourself.
- Google Firebase: Google's cloud solution, also offering virtually everything you might need. For some reason not as well respected in the industry as AWS, so instead of making a career in big tech companies you could get good at building fullstack apps with firebase and make a solo freelancing career.
- Supabase: They describe it as "The Open Source Firebase Alternative" and it fully delivers on that promise and more. Literally, if you're thinking of starting a new project with google firebase, I can't think of a single good reason why you shouldn't go with supabase instead. Not just less vendor lock-in but also just better tech like the postgress database most of supabase is built around (they won't shut up about it though, kinda giving me some linux-flavored gatekeeping vibes).
- Planetscale: This is the perfect example of what I said above about only picking the services you need. A small company dedicated to a single purpose: offering the best serverless MySQL database out there. And in my experience it delivers — from getting you started in literally two minutes, to all the scaling needs you might have.
- Some CMS like Strapi: Or, you know, WordPress. Or shopify. This is the real-world. The above solutions only apply if you're building the back-end yourself. If you're just working on the front-end of a furniture shop like this one, the back-end will be some CMS or e-commerce solution, and you'll only care about the REST API it provides.
Speaking of which:
another aside about REST vs GraphQL vs tRPC
I loved the idea of GraphQL when it came out, especially what it meant for frontend developers. Yet, as one ex-famous javascript guru from the era of javascript gurus, who shall remain unnamed here, used to say: "Who is the first group to go against things that make developers' lives easier? Developers".
Now I know there are many reasons as to why one technology becomes successful or not, but I can't help but think of the big role that cultural reasons play in it.
TypeScript's marketing? "Bringing strong, enterprise-class types to this weakly-typed, toy language finally making it serious enough for you to write it while wearing a suit and tie". No wonder brogrammers flocked to it (don't get me wrong, I love typescript; just commenting on the high number of people who would praise it online, many of them not even javascript programmers.)
GraphQL's — a similar technology to typescript — marketing? "Empowering frontend developers". Get outta here! REST is trad and lindy enough for me, don't need this shiny new thing from these facebook hipsters always reinventing the wheel.
All this rant was just to say that the state of API protocols is in a kind of limbo after GraphQL was supposed to replace REST and it didn't. REST is still the most widely used one. GraphQL is used in many big companies. And tRPC is the new kid on the block.
This article is targeted at juniors, probably preparing to get their first dev job, and I can't tell you for sure that my decision to only use REST examples is befitting towards that goal. If I was a hiring manager and my company used GraphQL, I would still hire a junior dev who only knows REST and expect them to learn GraphQL on the job. But I'm not a hiring manager. I know there are a few hiring managers who think like me and many others who don't. So, depending on your job applying strategy, you'll have to decide where you draw the line when it comes to learning technologies used by the companies you want to apply to, before applying.
Code
Two quick things that may distract you from the data fetching part:
The AWS link. Long story short, I used Amazon's AWS from the options above to setup the backend. Amazon S3 to store the images, DynamoDB for the product info, and API Gateway for the REST API. Ultimately, as a frontend developer, that REST endpoint is the only thing you'll care about.
The other thing which may confuse you in the code is the animation which is now being done with framer, so I have to write my last aside ever about animations.
last aside ever: animations
Okay, so, super quick. Animation is hard. Animation in react is even harder. Learn framer motion and learn it well to survive if you have to work with animations in react.
I made the animation work exactly as I wanted and as I had described in the first article, but it cost me EVERYTHING. Okaybe maybe not, but I had to learn framer motion and that's what it felt like.
Looking back at the code (this is perfect cause I did the animation work quite a while ago but didn't write this part of the blog to explain it until now) it's a bit verbose, VS code even shows me that this
exitBeforeEnter
property that I had to use to make the animation work right in a few edge cases is now deprecated. Other than that it's pretty readable I think. I like that the transition properties are now in javascript and located close to the animation components. I also like alot the truly(™) declarative style over what we had going on before with React Transition Group.Would I go back and try to make it work without the deprecated property for a hobby blog/side project/whatever this is? Hell naw.
Here's all the data fetching code with react query:
function ProductsSection() {
function onChangeFilter(filter) {
setActiveFilter((activeFilter) => ({
prev: activeFilter.current,
current: filter,
}))
}
const [activeFilter, setActiveFilter] = useState({
current: 'All',
prev: null,
})
let endpoint =
'https://c300bbvloc.execute-api.us-east-1.amazonaws.com/dev/products'
if (activeFilter.current !== 'All') {
endpoint += '/' + activeFilter.current
}
const {
isLoading,
error,
data: products = [],
isPreviousData,
} = useQuery({
queryKey: ['products', activeFilter.current],
queryFn: async () => await ky.get(endpoint).json(),
keepPreviousData: true,
staleTime: 1000 * 60 * 5,
})
return (
<Container smPadding="left" mdPadding="horizontal" role="main">
<ProductsHeading>
<ProductsTitle>Products</ProductsTitle>
<ProductsFilters
filters={filters}
activeFilter={activeFilter}
onChangeFilter={onChangeFilter}
></ProductsFilters>
</ProductsHeading>
{error ? (
error.message
) : (
<ProductsList
loading={isLoading || isPreviousData}
activeFilter={activeFilter}
products={products}
/>
)}
</Container>
)
}
Again, at the risk of sounding like an ad for react query, look at how much code we removed from what we had before. We got rid of useEffect
entirely and that's always a win!
I pretty much said all I had to say about react query in the previous sections. One funny thing though is something I had to do to get animations to work with it.
The way the loading animation works is it expects only the products that are added/removed to be actually added/removed to the markup. If you go let's say from 'All' to 'Tables' it should go from this:
<div>Table 1</div>
<div>Chair 1</div>
<div>Table 2</div>
directly to this:
<div>Table 1</div>
<div>Table 2</div>
That's how it knows to only animate out the "Chair 1" div. But with react query's default behaviour it goes from this:
[
"Table 1",
"Chair 1",
"Table 2",
]
to this for a second:
[]
and then finally to this:
[
"Table 1",
"Table 2",
]
As you can see that will mess with our animation because all the product divs would be gone for a moment before the other ones are loaded. Looking through the docs, I tried to fix it with initialData
and placeholderData
to no success. My first solution then was this:
<ProductsList
...
products={
products && products.length
? products
: queryClient.getQueryData([
'products',
activeFilter.prev || 'All',
]) || []
}
/>
It worked, but then, spending one friday evening, as one does, watching an episode of "Learn with Jason" with Dominik Dorfmeister, I realized all I had to do was add keepPreviousData: true,
to the useQuery
call. By the end I also added staleTime: 1000 * 60 * 5,
to ease my OCD and make sure it behaves like a proper legacy website not some fancy always up to date app.
Conclusions
Whew! I can't believe it's done. Finished. Complete. And congrats to you if you read all the way to here. If you didn't, and just scrolled here while skimming quickly, that's fine too, no judgement from me. Whatever brings joy. For me it surely brought lots of joy to finally publish this after such a long time, and I can't wait to get started on the next article. That will most probably focus on deployments, rendering strategies, and how different frameworks especially Next.js handle these. Looking forward to that! For now here are my short conclusions about data fetching and state management in react:
- I love the movement towards specific state management libraries for specific needs. Feels like a sign of maturity and a good middle ground after the pendulum swang from redux sagas to "just use react context".
- Use react query. That's it. That's the second conclusion.
- Animations, not even once. Just kidding... Unless? No, but seriously, this one was on me. I approached animations with the seriousness that a backend developer approaches frontend work and found myself in the same surprising position. If you're serious about animations in react, getting good with framer motion can be very rewarding.
Top comments (0)