What’s the best way to build React apps in 2021? What has changed since 2016? What libraries is everyone using these days?
This post is inspired by a now-deleted Reddit post from someone who had learned React in 2016, and was concerned about how to get back into it and refresh their skills.
I started using and teaching React in 2016 myself. Over the last few years, there have been some big changes in React itself, and the ecosystem has evolved quite a bit too.
Here’s where things stand in 2021.
Thinking in React: Pretty much the same
The core skill of “thinking in React” hasn’t changed much at all over the years. It’s still all about one-way data flow, props, state, and JSX. Things like being able to break a design into components are still crucial, as is deciding which components should “own” data and which should merely display it.
I still advocate for learning pure “vanilla” React before adding on a bunch of libraries.
Hooks vs Classes: Most new React components use hooks
In recent years, the biggest shift in React is from classes to hooks. Hooks were added in React 16.8 (Feb 2019), and have pretty quickly become the standard way that people write React components. You can check out the hooks intro I wrote back when they were first announced — they work the same now as they did then.
Initially, hooks look weird, especially if you’ve been programming a while. Variables that seemingly maintain state between function calls seem pretty magical. It’s more about arrays than magic, though.
Once you settle in with how hooks work, and get a feel for useState, the next big hurdle to overcome is the useEffect hook.
useEffect is the answer to how to do lifecycle methods in function components. Except it doesn’t really work the way lifecycles do, at all. It’s crucial to grasp the mental model of useEffect. Once you have that down, solving problems will get easier and easier.
The Best React Libraries in 2021
On the library front, community favorites have changed over the years, and continue to evolve.
Routing
React Router is still the dominant router (and despite the name, not actually part of React itself). It is currently up to v5 (almost v6) and the API has changed a bit since earlier versions. Less “declare your routes at the top” and more “routes are components; render them whereever”. The docs cover v5, and v6 has this preview blog. The v6 API is actually closer to v3, and having used it a bit, I think it will be a nice API.
State Management
Redux is still used in a lot of apps, hovering around 30-50% last I saw. The new official Redux Toolkit is excellent, too. It helps cut down boilerplate quite a bit, in combo with Redux’s hook. If you are going to use Redux, be sure to check those out.
Redux is less of the de-facto standard than it once was, though. More people are realizing that React’s built-in state management is enough for a lot of use cases, especially for simple ones.
There are also some newer purpose-built libraries for things that you might’ve used Redux for before. I’ll mention a couple below.
MobX is probably the most popular alternative to Redux outside of the built-in Context API. Where Redux is all about being explicit and functional, MobX takes the opposite approach. It uses ES6 Proxies behind the scenes to detect changes, so updating observable data is as easy as using the plain old =
assignment operator.
I’ve used MobX State Tree on a project, and enjoyed working with it. It’s good if you have a lot of state to manage and want to create structure around it with models.
Recoil and Zustand are a couple other lightweight state management options.
There are, as ever, lots of choices in the realm of state management.
Context API
If your global state consists of a couple things that rarely change (current user, current theme, current language, etc), you don’t need a library to pass that stuff around.
The Context API + useContext are good for passing around simple global state managed by useReducer.
The Context API was re-done in React 16.3. The old contextType
thing is out, and the old guidance about avoiding Context unless you’re a library maintainer has been gone for a while. The useContext hook makes it really nice to use.
There’s some long-standing confusion about whether to use Context or Redux, and what the differences are. Check out Mark Erikson’s blog post about Context vs. Redux for an in-depth comparison.
Data Fetching
On the data-fetching front, the strategy of putting everything in Redux or a global store is getting less common.
react-query does a good job at fetching data and managing loading/success/error states. It takes care of maintaining that global data cache across component boundaries without you really having to think about it. IMO it gets the abstraction right. Definitely worth a look.
Why react-query?
It’s less about the specific library and more about the pattern. (swr is another good option)
Take a common scenario like a ListPage / DetailPage for a set of items. You open the ListPage, it fetches all the widgets or whatever. Good so far.
Normally you’d probably keep that data around in Redux or something, so that when you click in to one of the DetailPages, that item has probably already been loaded. (oh! but what if the user loads a DetailPage route directly? welp, gotta fetch that item as a one-off)
And then the user clicks Back, and they’re back at ListPage again, but you already have the data so you can just display it.
It all works fine, but there are edge cases. What if an item went stale between the time the user loads ListPage and they click into a DetailPage? What if, while they were perusing the DetailPage, some new items were added to the list?
When should you re-fetch that data? And how do you deal with merging those two kinds of things – a list response that could maybe replace the whole list, and a single item response that should replace only one item? In Redux, the reducer handles that, but you’ve gotta write that stuff manually for the most part.
It all gets even more complex once you start thinking about pagination, and whether you want to cache pages, or refetch all the pages, or whatever.
All of this stuff, I think, falls under the “client data management” umbrella, and we’ve been using generic state management libraries for that for a long time now. And we have to solve these problems over and over again, or we ignore them and hope they don’t happen, or patch them as they crop up.
Libraries like react-query slice the problem up differently.
It knows you’re going to fetch data, and it knows you’re going to want to cache that data globally under some key (maybe items
or a nested items[id]
). It also knows that you’re going to want to update that data sometimes – based on a timer, or when the user tabs away from the app and back again, etc.
Because this stuff is stored in a globally accessible cache, every component that needs access can call useQuery('items', fetchItems)
to grab that data, and it’ll automatically be fetched if it’s not already available. And it deals with idle/loading/error/success states too.
It takes any Promise-returning function, so it works with fetch
or axios
or whatever data fetcher you want to use.
This is what I meant when I said that I think it gets the abstraction right - we can use whatever we were already using to make the HTTP call, but react-query steps in to handle the oft-repeated heavy lifting that’s common to most data fetching use cases.
State Machines are Awesome
XState is a library for building state machines, which are excellent for representing complex logic. Actually, they’re pretty great for not-so-complex logic, too. Next time you find yourself juggling a bunch of boolean values or trying to update a bunch of variables in the right places, check out XState. egghead.io has a nice course on XState by Kyle Shevlin.
There’s an alternative called Robot and I wrote a tutorial using it to build a confirmation modal flow, if you want to get a feel for how state machines can be useful.
Bundlers
Webpack is still everywhere. It’s up to version 5 now. The config syntax changed a lot somewhere around v2 or v3.
Most people use Create React App to start new apps nowadays, which is great, and shields you from Webpack unless you really need to customize it. The defaults are pretty solid. If you need to customize things, check out craco.
For throwing together a quick demo, CodeSandbox is great, and they even have this handy https://react.new URL that’ll drop you right into a new project.
Forms
The story around forms has continued to evolve. I remember using redux-form years ago, and how the app would freeze every time I pressed a key 😂 Looking back, “keep every ounce of state in Redux” was never really a good idea.
Formik and react-hook-form seem to be the favorites right now, with hook-form gaining steam.
Suspense
React’s long-awaited Suspense feature is… still awaited. It’s in React right now, and you can try it out, but it’s in experimental mode and it’s not advisable to build production code with it. The API can still change.
Server Components
The latest advancement is components that render on the server, coupled with a server-side framework around React. These are still experimental too. Very cool though, and I suspect will change the ecosystem quite a bit. Check out the official announcement and demo video from the React team to learn more.
Now Go Build Something!
That about wraps this up. I’m sure I missed some things though. Feel free to chime in with your favorites in the comments!