Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Wed, 15 Oct 2025 23:15:09 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Introducing Zustand (State Management) https://frontendmasters.com/blog/introducing-zustand/ https://frontendmasters.com/blog/introducing-zustand/#comments Mon, 21 Jul 2025 19:56:35 +0000 https://frontendmasters.com/blog/?p=6584 Zustand is a minimal, but fun and effective state management library. It’s somewhat weird for me to write an introductory blog post on a tool that’s over 5 years old and pretty popular. But it’s popular for a reason, and there are almost certainly more developers who aren’t familiar with it than are. So if you’re in the former group, hopefully this post will be the concise and impactful introduction you didn’t know you needed.

The code for everything in this post is on my GitHub repo.

Getting Started

We’ll look at a toy task management app that does minimal work so we can focus on state management. It shows a (static) list of tasks, a button to add a new task, a heading showing the number of tasks, and a component to change the UI view between three options.

Moreover, the same app was written 3 times, once using vanilla React context for state, once using Zustand simply but non-idiomatically, and then a third version using Zustand more properly, so we can see some of the performance benefits it offers.

Each of the three apps is identical, except for the label above the Add New Task button.

Each app is broken down more or less identically as so.

function App() {
  console.log("Rendering App");

  return (
    <div className="m-5 p-5 flex flex-col gap-2">
      <VanillaLabel />
      <AddNewTask />
      <TasksCount />
      <TasksHeader />
      <Filter />
      <TasksBody />
    </div>
  );
}

It’s probably more components than needed, but it’ll help us inspect render performance.

The state we need

Our state payload for this app will include an array of tasks, a method to update the tasks, the current UI view being displayed, a function to update it, and a current filter, with, of course, a method to update it.

Those values can all be declared as various pieces of state, and then passed down the component tree as needed. This is simple and it works, but the excessive amount of prop passing, often referred to as “prop drilling,” can get annoying pretty quickly. There are many ways to avoid this, from state management libraries like Zustand, Redux, and MobX, to the regular old React context.

In this post, we’ll first explore what this looks like using React context, and then we’ll examine how Zustand can simplify things while improving performance in the process.

The Vanilla Version

There’s a very good argument to be made that React’s context feature was not designed to be a state management library, but that hasn’t stopped many devs from trying. To avoid excessive prop drilling while minimizing external dependencies, developers will often store the state required for a specific part of their UI in context and access it lower in the component tree as needed.

Our app has its entire state stored like this, but that’s just a product of how unrealistically small it is.

Let’s get started. First, we have to declare our context

const TasksContext = createContext<TasksState>(null as any);

Then we need a component that renders a Provider for that context, while declaring, and then passing in the actual state

export const TasksProvider = ({ children }: { children: ReactNode }) => {
  console.log("Rendering TasksProvider");

  const [tasks, setTasks] = useState<Task[]>(dummyTasks);
  const [currentView, setCurrentView] = useState<TasksView>("list");
  const [currentFilter, setCurrentFilter] = useState<string>("");

  const value: TasksState = {
    tasks,
    setTasks,
    currentView,
    setCurrentView,
    currentFilter,
    setCurrentFilter,
  };

  return <TasksContext.Provider value={value}>{children}</TasksContext.Provider>;
};

The logging console.log("Rendering TasksProvider"); is present in every component in all versions of this app, so we can inspect re-renders.

Notice how we have to declare each piece of state with useState (or useReducer)

const [tasks, setTasks] = useState<Task[]>(dummyTasks);
const [currentView, setCurrentView] = useState<TasksView>("list");
const [currentFilter, setCurrentFilter] = useState<string>("");

and then splice it together in our big state payload, and then render our context provider

const value: TasksState = {
  tasks,
  setTasks,
  currentView,
  setCurrentView,
  currentFilter,
  setCurrentFilter,
};

return <TasksContext.Provider value={value}>{children}</TasksContext.Provider>;

To get the current context value in a component that wants to use it, we call the useContext hook, and pass in the context object we declared above. To simplify this, it’s not uncommon to build a simple hook for just this purpose.

export const useTasksContext = () => {
  return useContext(TasksContext);
};

Now components can grab whatever slice of state they need.

const { currentView, tasks, currentFilter } = useTasksContext();

What’s the problem?

This code is fine. It’s simple enough. And it works. I’ll be honest, though, as someone who works with code like this a lot, the boilerplate can become annoying pretty quickly. We have to declare each piece of state with the normal React primitives (useState, useReducer), and then also integrate it into our context payload (and typings). It’s not the worst thing to deal with; it’s just annoying.

Another downside of this code is that all consumers of this context will always rerender anytime any part of the context changes, even if that particular component is not using the part of the context that just changed. We can see that with the logging that’s in these components.

For example, changing the current UI view rerenders everything, even though only the task header, and task body read that state

Introducing Zustand

Zustand is a minimal but powerful state management library. To create state, Zustand gives you a create method

import { create } from "zustand";

It’s easier to show this than to describe it.

export const useTasksStore = create<TasksState>(set => ({
  tasks,
  setTasks: (arg: Task[] | ((tasks: Task[]) => Task[])) => {
    set(state => {
      return {
        tasks: typeof arg === "function" ? arg(state.tasks) : arg,
      };
    });
  },
  currentView: "list",
  setCurrentView: (newView: TasksView) => set({ currentView: newView }),
  currentFilter: "",
  setCurrentFilter: (newFilter: string) => set({ currentFilter: newFilter }),
}));

We pass a function to create and return our state. Just like that. Simple and humble. The function we pass also takes an argument, which I’ve called set. The result of the create function, which I’ve named useTasksStore here, will be a React hook that you use to read your state.

Updating state

Updating our state couldn’t be simpler. The set function we see above is how we do that. Notice our updating functions like this:

setCurrentView: (newView: TasksView) => set({ currentView: newView }),

By default set will take what we return, and integrate it into the state that’s already there. So we can return the pieces that have changed, and Zustand will handle the update.

Naturally, there’s an override: if we pass true for the second argument to set, then what we return will overwrite the existing state in its entirety.

clear: () => set({}, true);

The above would wipe our state, and replace it with an empty object; use this cautiously!

Reading our state

To read our state in the components which need it, we call the hook that was returned from create, which would be useTasksStore from above. We could read our state in the same way we read our context above

This is not the best way to use Zustand. Keep reading for a better way to use this API.

const { currentView, tasks, currentFilter } = useTasksStore();

This will work and behave exactly like our context example before.

This means changing the current UI view will again re-render all components that read anything from the Zustand store, whether related to this piece of state, or not.

The Correct Way to Read State

It’s easy to miss in the docs the first time you read them, but when reading from your Zustand store, you shouldn’t do this:

const { yourFields } = useTasksStore();

Zustand is well optimized, and will cause the component with the call to useTasksStore to only re-render when the result of the hook call changes. By default, it returns an object with your entire state. And when you change any piece of your state, the surrounding object will have to be recreated by Zustand, and will no longer match.

Instead, you should pass a selector argument into useTasksStore, in order to select the piece of state you want. The simplest usage would look like this

const currentView = useTasksStore(state => state.currentView);
const tasks = useTasksStore(state => state.tasks);
const currentFilter = useTasksStore(state => state.currentFilter);

Now our call returns only the currentView value in the first line, or our tasks array, or currentFilter in our second and third lines, respectively.

The value returned for currentView will only be different if you’ve changed that state value, and so on with tasks, and currentFilter. That means if none of these values have changed, then this component will not rerender, even if other values in our Zustand store have changed.

If you don’t like having those multiple calls, you’re free to use Zustand’s useShallow helper

import { useShallow } from "zustand/react/shallow";

// ...
const { tasks, setTasks } = useTasksStore(
  useShallow(state => ({
    tasks: state.tasks,
    setTasks: state.setTasks,
  }))
);

The useShallow hook lets us return an object with the state we want, and will trigger a rerender only if a shallow check on the properties in this object change.

If you want to save a few lines of code, you’re also free to return an array with useShallow.

const [tasks, setTasks] = useTasksStore(useShallow(state => [state.tasks, state.setTasks]));

This does the same thing.

The Zustand-optimized version of the app only uses the useTasksStore hook with a selector function, which means we can observe our improved re-rendering.

Changing the current UI view will only rerender the components that use the ui view part of the state.

Console log showing rendering messages for TasksHeader, TasksBody, and TasksDetailed components.

For a trivial app like this, it probably won’t matter, but for a large app at scale, this can be beneficial, especially for users on slower devices.

Odds & Ends

The full Zustand docs are here. Zustand has a delightfully small surface area, so I’d urge you to just read the docs if you’re curious.

That being said, there are a few features worth noting here.

Async friendly

Zustand doesn’t care where or when the set function is called. You’re free to have async methods in your store, which call set after a fetch.

The docs offer this example:

const useFishStore = create(set => ({
  fishies: {},
  fetch: async pond => {
    const response = await fetch(pond);
    set({ fishies: await response.json() });
  },
}));

Reading state inside your store, but outside of set

We already know that we can call set(oldState => newState), but what if we need (or just want) to read the current state inside one of our actions, unrelated to an update?

It turns out create also has a second argument, get, that you can use for this very purpose

export const useTasksStore = create<TasksState>((set, get) => ({

And now you can do something like this

logOddTasks: () => {
  const oddTasks = get().tasks.filter((_, index) => index % 2 === 0);
  console.log({ oddTasks: oddTasks });
},

The first line grabs a piece of state, completely detached from any updates.

Reading state outside of React components

Zustand gives you back a React hook from create. But what if you want to read your state outside of a React component? Zustand attaches a getState() method directly onto your hook, which you can call anywhere.

useEffect(() => {
  setTimeout(() => {
    console.log("Can't call a hook here");
    const tasks = useTasksStore.getState().tasks;
    console.log({ tasks });
  }, 1000);
}, []);

Pushing further

Zustand also supports manual, fine-grained subscriptions; bindings for vanilla JavaScript, with no React at all; and integrates well with immutable helpers like Immer. It also has some other, more advanced goodies that we won’t try to cover here. Check out the docs if this post has sparked your interest!

Concluding Thoughts

Zustand is a wonderfully simple, frankly fun library to use to manage state management in React. And as an added bonus, it can also improve your render performance.

]]>
https://frontendmasters.com/blog/introducing-zustand/feed/ 11 6584
RedwoodSDK https://frontendmasters.com/blog/redwoodsdk/ https://frontendmasters.com/blog/redwoodsdk/#respond Fri, 02 May 2025 19:05:31 +0000 https://frontendmasters.com/blog/?p=5777 A bit of a pivot from the Redwood gang, splitting RedwoodJS into Redwood GraphQL …

To minimize disruption and provide clarity going forward, we’re renaming the existing RedwoodJS framework to Redwood GraphQL, reflecting its strength as a mature, stable framework built around GraphQL.

… and the newfangled RedwoodSDK. Redwood has always been ultra opinionated (I remember making a slide about it once), and so this feels in-line to me. It’s now so opinionated it adds where you can host it to the mix. But I get it — Cloudflare’s stuff is pretty unique/cool/powerful/opinionated in itself, so it’s a good match. I was also just bemoaning React Server Components slow uptick, and now eating my words as now another major-ish framework is supporting them.

]]>
https://frontendmasters.com/blog/redwoodsdk/feed/ 0 5777
React Internals: Which useEffect runs first? https://frontendmasters.com/blog/react-internals-which-useeffect-runs-first/ https://frontendmasters.com/blog/react-internals-which-useeffect-runs-first/#comments Mon, 28 Apr 2025 21:54:15 +0000 https://frontendmasters.com/blog/?p=5672 useEffect is one of the most commonly used hooks in the React community. Regardless of how much experience you have with React, you’ve probably used it before.

But have you ever run into situations where useEffect hooks run in an unexpected order when multiple layers of components are involved?

Let’s start with a quick quiz. What’s the correct order of these console.log statements in the console?

function Parent({ children }) {
  console.log("Parent is rendered");
  useEffect(() => {
    console.log("Parent committed effect");
  }, []);

  return <div>{children}</div>;
}

function Child() {
  console.log("Child is rendered");
  useEffect(() => {
    console.log("Child committed effect");
  }, []);

  return <p>Child</p>;
}

export default function App() {
  return (
    <Parent>
      <Child />
    </Parent>
  );
}
Answer
// initial render
Parent is rendered
Child is rendered

// useEffects
Child committed effect
Parent committed effect

If you got it right—nice job! If not, no worries—most React devs get this wrong too. In fact, this isn’t something that’s clearly documented or explained on the official React website.

Let’s explore why children components are rendered last but their effects are committed first. We’ll dive into how and when React renders components and commits effects (useEffect). We’ll touch on a few React internal concepts like the React Fiber architecture and its traversal algorithm.

Frontend Masters offers lots of courses on React, like Complete Intro to React and Intermediate React by Brian Holt.

Overview of React Internals

According to React official documentation, the entire React’s component lifecycle can be roughly divided into 3 phases: Trigger → Render → Commit

Triggering a render

  • The component’s initial render, or state updates with setState.
  • A state update is put in a queue and scheduled to be processed by the React Scheduler.

Rendering

  • React calls the component and works on the state update.
  • React reconciles and marks it as “dirty” for commit phase.
  • Create new DOM node internally.

Committing to the DOM

  • Apply actual DOM manipulation.
  • Runs effects (useEffect, useLayoutEffect).

React Fiber Tree

Before diving into the traversal algorithm, we need to understand the React Fiber architecture. I’ll try to keep this introduction beginner-friendly.

Internally, React uses a tree-like data structure called fiber tree to represent the component hierarchy and track updates.

From the diagram above, we can tell that fiber tree is not exactly a one-to-one mapping of DOM tree. It includes additional information that help React manage rendering more efficiently.

Each node in this tree is called a fiber node. There are different kinds of fiber nodes such as HostComponent which refers to a native DOM element, like <div> or <p> in the diagram. FiberRootNode is the root node and will point to a different HostRoot node during each new render.

Every fiber node contains properties like props, state, and most importantly:

  1. child – The child of the fiber.
  2. sibling – The sibling of the fiber.
  3. return – The return value of the fiber is the parent fiber.

These information allows React to form a tree.

Every time there is a state update, React will construct a new fiber tree and compare against the old tree internally.

If you’re interested in the detail, please check out JSer’s blog or his super cool project React Internal Explorer!

How Fiber Tree Is Traversed

Generally, React reuses the same traversal algorithm in many use cases.

The animation above shows how React walks the fiber tree. Notice that each node is stepped twice. The rule is simple:

  1. Traverse downwards.
  2. In each fiber node, React checks
    1. If there’s a child, move to the child.
    2. If there’s no child, step again the current node. Then,
      1. If there’s a sibling, move to the sibling.
      2. If there’s no sibling, move up to its parent.

This traversal algorithm ensures each node is stepped twice.

Now, let’s revisit the quiz above.

Render Phase

React traverses the fiber tree and recursively performs two steps on each fiber node:

  • In the first step, React calls the component — this is where console.log statement is executed. React reconciles and marks the fiber as “dirty” if state or props have changed, preparing it for the commit phase.
  • In the second step, React constructs the new DOM node.

In the React source code, the process is named workLoop. The first step is beginWork(). The second step is completeWork().

At the end of Render phase, a new fiber tree with the updated DOM nodes is generated. At this point, nothing has been committed to the real DOM yet. The actual DOM mutations will happen in the Commit phase.

Commit Phase

The commit phase is where actual DOM mutations and effect flushing (useEffect) take place. The traversal pattern remains the same, but DOM mutations and effect flushing are handled in separate walks.

In this section, we’ll skip DOM mutations and focus on the effect flushing walk.

Committing effects

React uses the same traversal algorithm. However, instead of checking whether a node has a child, it checks whether it has a subtree — which makes sense, because only React components can contain useEffect hooks. A DOM node like <p> won’t contain any React hooks.

Nothing happens in the first step, but in the second step, it commits effects.

This depth-first traversal explains why child effects are run before parent effects. This is the root cause.

In the React source code, the recursive function for committing effects is named recursivelyTraversePassiveMountEffect.

Now let’s check out another quiz example. The result should make more sense to you now.

function Parent({ children }) {
  console.log("Parent is rendered");
  useEffect(() => {
    console.log("Parent committed effect");
  }, []);

  return <div>{children}</div>;
}

function Child() {
  console.log("Child is rendered");
  useEffect(() => {
    console.log("Child committed effect");
  }, []);

  return <p>Child</p>;
}

function ParentSibling() {
  console.log("ParentSibling is rendered");
  useEffect(() => {
    console.log("ParentSibling committed effect");
  }, []);

  return <p>Parent's Sibling</p>;
}

export default function App() {
  return (
    <>
      <Parent>
        <Child />
      </Parent>
      <ParentSibling />
    </>
  );
}
Answer
// Initial render
Parent is rendered
Child is rendered
ParentSibling is rendered

// useEffects
Child committed effect
Parent committed effect
ParentSibling committed effect

During the commit phase:

Now, it should be self-explanatory why child effects are flushed before their parents during the commit phase.

Understanding how and when React commits useEffect hooks can help you avoid subtle bugs and unexpected behaviors—especially when working with complex component structures.

Welcome to React internals!

]]>
https://frontendmasters.com/blog/react-internals-which-useeffect-runs-first/feed/ 12 5672
Creating a Keyboard Shortcut Hook in React https://frontendmasters.com/blog/creating-a-keyboard-shortcut-hook-in-react/ https://frontendmasters.com/blog/creating-a-keyboard-shortcut-hook-in-react/#respond Thu, 06 Mar 2025 18:04:55 +0000 https://frontendmasters.com/blog/?p=5287 I enjoyed this very straightforward, well-presented, useful article on how to create a keyboard shortcut hook in React from Tania Rascia. It’s part basic implementation, part designing an API pattern that feels right, part dealing with React hook eccentricities, and part dealing with edge cases. In the end, a pretty readable 86 lines of code.

]]>
https://frontendmasters.com/blog/creating-a-keyboard-shortcut-hook-in-react/feed/ 0 5287
React 19 and Web Component Examples https://frontendmasters.com/blog/react-19-and-web-component-examples/ https://frontendmasters.com/blog/react-19-and-web-component-examples/#respond Mon, 16 Dec 2024 16:37:38 +0000 https://frontendmasters.com/blog/?p=4800 There is lots of news of React 19 and going stable and now supporting Web Components. Or… “custom elements” I should say, as that refers to the HTML expression of them as <dashed-elements>, which is where the trouble laid. Apparently it was hard for React to know, in JSX, if props should be treated as a property or an attribute. So they’ve just decided this is how it will work:

  • Server Side Rendering: props passed to a custom element will render as attributes if their type is a primitive value like stringnumber, or the value is true. Props with non-primitive types like objectsymbolfunction, or value false will be omitted.
  • Client Side Rendering: props that match a property on the Custom Element instance will be assigned as properties, otherwise they will be assigned as attributes.

That’s enough to pass all the tests at Custom Elements Everywhere, which tracks such things. (And apparently every single framework is now 100% fine. Cool.)

This got me thinking about what this actually means and how I might make use of it. I use both React and Web Components sometimes, but only rarely do I combine them, and the last time I did I had more issues with the Shadow DOM than I did with React doing anything funky.

So here I’ve made a LitElement and rendered it within a React component:

What I was thinking there was… what if I make a <designsystem-button> and need a click handler on it? Turns out that’s not really a problem. You can just slap a React onClick right on it and it’s fine.

<MyCard>
  <p>blah blah</p>
  <!-- this is fine -->
  <designsystem-button onClick={() => {}}></designsystem-button>
</MyCard>

That wasn’t a problem anyway, apparently.

What is a problem is if I want to send in a function from React-land for the Web Component to call. You’d think we could send the function in how LitElement generally wants you to do that like:

<!-- nope -->
<designsystem-button .mySpecialEvent=${specialEvent}>

But nope, JSX really doesn’t like that “dot syntax” and won’t compile.

So you gotta send it in more like this:

<designsystem-button onSpecialEvent={() => mySpecialEvent()}

Then in order to “call” that event, you “dispatch” and event named properly. Like:

this.dispatchEvent(new CustomEvent("SpecialEvent", { bubbles: true }));

Here’s that with a “raw” Web Component:

I took that idea from Jared White’s article Oh Happy Day! React Finally Speaks Web Components. Where he’s got some other examples. Another is passing in a “complex object” which is one of those things that would have been impossible in React 18 and under apparently, and now we can do:

]]>
https://frontendmasters.com/blog/react-19-and-web-component-examples/feed/ 0 4800
Combining React Server Components with react-query for Easy Data Management https://frontendmasters.com/blog/combining-react-server-components-with-react-query-for-easy-data-management/ https://frontendmasters.com/blog/combining-react-server-components-with-react-query-for-easy-data-management/#comments Fri, 24 May 2024 15:27:11 +0000 https://frontendmasters.com/blog/?p=2378 React Server Components (RSC) are an exciting innovation in web development. In this post we’ll briefly introduce them, show what their purpose and benefits are, as well as their limitations. We’ll wrap up by showing how to pair them with react-query to help solve those limitations. Let’s get started!

Why RSC?

React Server Components, as the name implies, execute on the server—and the server alone. To see why this is significant, let’s take a whirlwind tour of how web development evolved over the last 10 or so years.

Prior to RSC, JavaScript frameworks (React, Svelte, Vue, Solid, etc) provided you with a component model for building your application. These components were capable of running on the server, but only as a synchronous operation for stringifying your components’ HTML to send down to the browser so it could server render your app. Your app would then render in the browser, again, at which point it would become interactive. With this model, the only way to load data was as a side-effect on the client. Waiting until your app reached your user’s browser before beginning to load data was slow and inefficient.

To solve this inefficiency, meta-frameworks like Next, SvelteKit, Remix, Nuxt, SolidStart, etc were created. These meta-frameworks provided various ways to load data, server-side, with that data being injected by the meta-framework into your component tree. This code was non-portable, and usually a little awkward. You’d have to define some sort of loader function that was associated with a given route, load data, and then expect it to show up in your component tree based on the rules of whatever meta-framework you were using.

This worked, but it wasn’t without issue. In addition to being framework-specific, composition also suffered; where typically components are explicitly passed props by whichever component renders them, now there are implicit props passed by the meta-framework, based on what you return from your loader. Nor was this setup the most flexible. A given page needs to know what data it needs up front, and request it all from the loader. With client-rendered SPAs we could just render whatever components we need, and let them fetch whatever data they need. This was awful for performance, but amazing for convenience.

RSC bridges that gap and gives us the best of both worlds. We get to ad hoc request whatever data we need from whichever component we’re rendering, but have that code execute on the server, without needing to wait for a round trip to the browser. Best of all, RSC also supports streaming, or more precisely, out-of-order streaming. If some of our data are slow to load, we can send the rest of the page, and push those data down to the browser, from the server, whenever they happen to be ready.

How do I use them?

At time of writing RSC are mostly only supported in Next.js, although the minimal framework Waku also supports it. Remix and TanStack Router are currently working on implementations, so stay tuned. I’ll show a very brief overview of what they look like in Next; consult those other frameworks when they ship. The ideas will be the same, even if the implementations differ slightly.

In Next, when using the new “app directory” (it’s literally a folder called “app” that you define your various routes in), pages are RSC by default. Any components imported by these pages are also RSC, as well as components imported by those components, and so on. When you’re ready to exit server components and switch to “client components,” you put the "use client" pragma at the top of a component. Now that component, and everything that component imports are client components. Check the Next docs for more info.

How do React Server Components work?

React Server Components are just like regular React Components, but with a few differences. For starters, they can be async functions. The fact that you can await asynchronous operations right in the component makes them well suited for requesting data. Note that asynchronous client components are a thing coming soon to React, so this differentiation won’t exist for too long. The other big difference is that these components run only on the server. Client components (i.e. regular components) run on the server, and then re-run on the client in order to “hydrate.” That’s how frameworks like Next and Remix have always worked. But server components run only on the server.

Server components have no hydration, since, again, they only execute on the server. That means you can do things like connect directly to a database, or use Server-only api’s. But it also means there are many things you can’t do in RSCs: you cannot use effects or state, you cannot set up event handlers, or use browser-specific APIs like localStorage. If you violate any of those rules you’ll get errors.

For a more thorough introduction to RSC, check the Next docs for the app directory, or depending on when you read this, the Remix or TanStack Router docs. But to keep this post a reasonable length, let’s keep the details in the docs, and see how we use them.

Let’s put together a very basic proof of concept demo app with RSC, see how data mutations work, and some of their limitations. We’ll then take that same app (still using RSC) and see how it looks with react-query.

The demo app

As I’ve done before, let’s put together a very basic, very ugly web page for searching some books, and also updating the titles of them. We’ll also show some other data on this page: the various subjects, and tags we have, which in theory we could apply to our books (if this were a real web app, instead of a demo).

The point is to show how RSC and react-query work, not make anything useful or beautiful, so temper your expectations 🙂 Here’s what it looks like:

The page has a search input which puts our search term into the url to filter the books shown. Each book also has an input attached to it for us to update that book’s title. Note the nav links at the top, for the RSC and RSC + react-query versions. While the pages look and behave identically as far as the user can see, the implementations are different, which we’ll get into.

The data is all static, but the books are put into a SQLite database, so we can update the data. The binary for the SQLite db should be in that repo, but you can always re-create it (and reset any updates you’ve made) by running npm run create-db.

Let’s dive in.

A note on caching

At time of writing, Next is about to release a new version with radically different caching APIs and defaults. We won’t cover any of that for this post. For the demo, I’ve disabled all caching. Each call to a page, or API endpoint will always run fresh from the server. The client cache will still work, so if you click between the two pages, Next will cache and display what you just saw, client-side. But refreshing the page will always recreate everything from the server.

Loading the data

There are API endpoints inside of the api folder for loading data and for updating the books. I’ve added artificial delays of a few hundred ms for each of these endpoints, since they’re either loading static data, or running simple queries from SQLite. There’s also console logging for these data, so you can see what’s loading when. This will be useful in a bit.

Here’s what the terminal console shows for a typical page load in either the RSC or RSC + react-query version.

Let’s look at the RSC version

RSC Version

export default function RSC(props: { searchParams: any }) {
  const search = props.searchParams.search || "";

  return (
    <section className="p-5">
      <h1 className="text-lg leading-none font-bold">Books page in RSC</h1>
      <Suspense fallback={<h1>Loading...</h1>}>
        <div className="flex flex-col gap-2 p-5">
          <BookSearchForm />
          <div className="flex">
            <div className="flex-[2] min-w-0">
              <Books search={search} />
            </div>
            <div className="flex-1 flex flex-col gap-8">
              <Subjects />
              <Tags />
            </div>
          </div>
        </div>
      </Suspense>
    </section>
  );
}

We have a simple page header. Then we see a Suspense boundary. This is how out-of-order streaming works with Next and RSC. Everything above the Suspense boundary will render immediately, and the Loading... message will show until all the various data in the various components below have finished loading. React knows what’s pending based on what you’ve awaited. The BooksSubjects and Tags components all have fetches inside of them, which are awaited. We’ll look at one of them momentarily, but first note that, even though three different components are all requesting data, React will run them in parallel. Sibling nodes in the component tree can, and do load data in parallel.

But if you ever have a parent / child component which both load data, then the child component will not (cannot) even start util the parent is finished loading. If the child data fetch depends on the parent’s loaded data, then this is unavoidable (you’d have to modify your backend to fix it), but if the data do not depend on each other, then you would solve this waterfall by just loading the data higher up in the component tree, and passing the various pieces down.

Loading data

Let’s see the Books component”

import { FC } from "react";
import { BooksList } from "../components/BooksList";
import { BookEdit } from "../components/BookEditRSC";

export const Books: FC<{ search: string }> = async ({ search }) => {
  const booksResp = await fetch(`http://localhost:3000/api/books?search=${search}`, {
    next: {
      tags: ["books-query"],
    },
  });
  const { books } = await booksResp.json();

  return (
    <div>
      <BooksList books={books} BookEdit={BookEdit} />
    </div>
  );
};

We fetch and await our data right in the component! This was completely impossible before RSC. We then then pass it down into the BooksList component. I separated this out so I could re-use the main BookList component with both versions. The BookEdit prop I’m passing in is a React component that renders the textbox to update the title, and performs the update. This will differ between the RSC, and react-query version. More on that in a bit.

The next property in the fetch is Next-specific, and will be used to invalidate our data in just a moment. The experienced Next devs might spot a problem here, which we’ll get into very soon.

So you’ve loaded data, now what?

We have a page with three different RSCs which load and render data. Now what? If our page was just static content we’d be done. We loaded data, and displayed it. If that’s your use case, you’re done. RSCs are perfect for you, and you won’t need the rest of this post.

But what if you want to let your user interact with, and update your data?

Updating your data with Server Actions

To mutate data with RSC you use something called Server Actions. Check the docs for specifics, but here’s what our server action looks like

"use server";

import { revalidateTag } from "next/cache";

export const saveBook = async (id: number, title: string) => {
  await fetch("http://localhost:3000/api/books/update", {
    method: "POST",
    body: JSON.stringify({
      id,
      title,
    }),
  });
  revalidateTag("books-query");
};

Note the "use server" pragma at the top. That means the function we export is now a server action. saveBook takes an id, and a title; it posts to an endpoint to update our book in SQLite, and then calls revalidateTag with the same tag we passed to our fetch, before.

In real life, we wouldn’t even need the books/update endpoint. We’d just do the work right in the server action. But we’ll be re-using that endpoint in a bit, when we update data without server actions, and it’s nice to keep these code samples clean. The books/update endpoint opens up SQLite, and executes an UPDATE.

Let’s see the BookEdit component we use with RSC:

"use client";

import { FC, useRef, useTransition } from "react";
import { saveBook } from "../serverActions";
import { BookEditProps } from "../types";

export const BookEdit: FC<BookEditProps> = (props) => {
  const { book } = props;
  const titleRef = useRef<HTMLInputElement>(null);
  const [saving, startSaving] = useTransition();

  function doSave() {
    startSaving(async () => {
      await saveBook(book.id, titleRef.current!.value);
    });
  }

  return (
    <div className="flex gap-2">
      <input className="border rounded border-gray-600 p-1" ref={titleRef} defaultValue={book.title} />
      <button className="rounded border border-gray-600 p-1 bg-blue-300" disabled={saving} onClick={doSave}>
        {saving ? "Saving..." : "Save"}
      </button>
    </div>
  );
};

It’s a client component. We import the server action, and then just call it in a button’s event handler, wrapped in a transition so we can have saving state.

Stop and consider just how radical this is, and what React and Next are doing under the covers. All we did was create a vanilla function. We then imported that function, and called it from a button’s event handler. But under the covers a network request is made to an endpoint that’s synthesized for us. And then the revalidateTag tells Next what’s changed, so our RSC can re-run, re-request data, and send down updated markup.

Not only that, but all this happens in one round trip with the server.

This is an incredible engineering achievement, and it works! If you update one of the titles, and click save, you’ll see updated data show up in a moment (the update has an artificial delay since we’re only updating in a local SQLite instance)

What’s the catch?

This seems too good to be true. What’s the catch? Well, let’s see what the terminal shows when we update a book:

Ummmm, why is all of our data re-loading? We only called revalidateTag on our books, not our subjects or tags. The problem is that revalidateTag doesn’t tell Next what to reload, it tells it what to eject from its cache. The fact is, Next needs to reload everything for the current page when we call revalidateTag. This makes sense when you think about what’s really happening. These server components are not stateful; they run on the server, but they don’t live on the server. The request executes on our server, those RSCs render, and send down the markup, and that’s that. The component tree does not live on indefinitely on the server; our servers wouldn’t scale very well if they did!

So how do we solve this? For a use case like this, the solution would be to not turn off caching. We’d lean on Next’s caching mechanisms, whatever they look like when you happen to read this. We’d cache each of these data with different tags, and invalidate the tag related to the data we just updated.

The whole RSC tree will still re-render when we do that, but the requests for cached data would run quickly. Personally, I’m of the view that caching should be a performance tweak you add, as needed; it should not be a sine qua non for avoiding slow updates.

Unfortunately, there’s yet another problem with server actions: they run serially. Only one server action can be in flight at a time; they’ll queue if you try to violate this constraint.

This sounds genuinely unbelievable; but it’s true. If we artificially slow down our update a LOT, and then quickly click 5 different save buttons, we’ll see horrifying things in our network tab. If the extreme slowdown on the update endpoint seems unfair on my part, remember: you should never, ever assume your network will be fast, or even reliable. Occasional, slow network requests are inevitable, and server actions will do the worst possible thing under those circumstances.

This is a known issue, and will presumably be fixed at some point. But the re-loading without caching issue is unavoidable with how Next app directory is designed.

Just to be clear, server actions are still, even with these limitations, outstanding (for some use cases). If you have a web page with a form, and a submit button, server actions are outstanding. None of these limitations will matter (assuming your form doesn’t depend on a bunch of different data sources). In fact, server actions go especially well with forms. You can even set the “action” of a form (in Next) directly to a server action. See the docs for more info, as well as on related hooks, like useFormStatus hook.

But back to our app. We don’t have a page with a single form and no data sources. We have lots of little forms, on a page with lots of data sources. Server actions won’t work well here, so let’s see an alternative.

react-query

React Query is probably the most mature, well-maintained data management library in the React ecosystem. Unsurprisngly, it also works well with RSC.

To use react-query we’ll need to install two packages: npm i @tanstack/react-query @tanstack/react-query-next-experimental. Don’t let the experimental in the name scare you; it’s been out for awhile, and works well.

Next we’ll make a Providers component, and render it from our root layout

"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
import { FC, PropsWithChildren, useEffect, useState } from "react";

export const Providers: FC<PropsWithChildren<{}>> = ({ children }) => {
  const [queryClient] = useState(() => new QueryClient());

  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration>
    </QueryClientProvider>
  );
};

Now we’re ready to go.

Loading data with react-query

The long and short of it is that we use the useSuspenseQuery hook from inside client components. Let’s see some code. Here’s the Books component from the react-query version of our app.

"use client";

import { FC } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
import { BooksList } from "../components/BooksList";
import { BookEdit } from "../components/BookEditReactQuery";
import { useSearchParams } from "next/navigation";

export const Books: FC<{}> = () => {
  const params = useSearchParams();
  const search = params.get("search") ?? "";

  const { data } = useSuspenseQuery({
    queryKey: ["books-query", search],
    queryFn: async () => {
      const booksResp = await fetch(`http://localhost:3000/api/books?search=${search}`);
      const { books } = await booksResp.json();

      return { books };
    },
  });

  const { books } = data;

  return (
    <div>
      <BooksList books={books} BookEdit={BookEdit} />
    </div>
  );
};

Don’t let the "use client" pragma fool you. This component still renders on the server, and that fetch also happens on the server during the initial load of the page.

As the url changes, the useSearchParams result changes, and a new query is fired off by our useSuspenseQuery hook, from the browser. This would normally suspend the page, but I wrap the call to router.push in startTransition, so the existing content stays on the screen. Check the repo for details.

Updating data with react-query

We already have the /books/update endpoint for updating a book. How do we tell react-query to re-run whichever queries were attached to that data? The answer is the queryClient.invalidateQueries API. Let’s take a look at the BookEdit component for react-query

"use client";

import { FC, useRef, useTransition } from "react";
import { BookEditProps } from "../types";
import { useQueryClient } from "@tanstack/react-query";

export const BookEdit: FC<BookEditProps> = (props) => {
  const { book } = props;
  const titleRef = useRef<HTMLInputElement>(null);
  const queryClient = useQueryClient();
  const [saving, startSaving] = useTransition();

  const saveBook = async (id: number, newTitle: string) => {
    startSaving(async () => {
      await fetch("/api/books/update", {
        method: "POST",
        body: JSON.stringify({
          id,
          title: newTitle,
        }),
      });

      await queryClient.invalidateQueries({ queryKey: ["books-query"] });
    });
  };

  return (
    <div className="flex gap-2">
      <input className="border rounded border-gray-600 p-1" ref={titleRef} defaultValue={book.title} />
      <button className="rounded border border-gray-600 p-1 bg-blue-300" disabled={saving} onClick={() => saveBook(book.id, titleRef.current!.value)}>
        {saving ? "Saving..." : "Save"}
      </button>
    </div>
  );
};

The saveBook function calls out to the same book updating endpoint as before. We then call invalidateQueries with the first part of the query key, books-query. Remember, the actual queryKey we used in our query hook was queryKey: ["books-query", search]. Calling invalidate queries with the first piece of that key will invalidate everything that’s starts with that key, and will immediately re-fire any of those queries which are still on the page. So if you started out with an empty search, then searched for X, then Y, then Z, and updated a book, this code will clear the cache of all those entries, and then immediately re-run the Z query, and update our UI.

And it works.

What’s the catch?

The downside here is that we need two roundtrips from the browser to the server. The first roundtrip updates our book, and when that finishes, we then, from the browser, call invalidateQueries, which causes react-query to send a new network request for the updated data.

This is a surprisingly small price to pay. Remember, with server actions, calling revalidateTag will cause your entire component tree to re-render, which by extension will re-request all their various data. If you don’t have everything cached (on the server) properly, it’s very easy for this single round trip to take longer than the two round trips react-query needs. I say this from experience. I recently helped a friend / founder build a financial dashboard app. I had react-query set up just like this, and also implemented a server action to update a piece of data. And I had the same data rendered, and updated twice: once in an RSC, and again adjacently in a client component from a useSuspenseQuery hook. I basically fired off a race to see which would update first, certain the server action would, but was shocked to see react-query win. I initially thought I’d done something wrong until I realized what was happening (and hastened to roll back my server action work).

Playing on hard mode

There’s one obnoxious imperfection hiding. Let’s find it, and fix it.

Fixing routing when using react-query

Remember, when we search our books, I’m calling router.push which adds a querystring to the url, which causes useSearchParams() to update, which causes react-query to query new data. Let’s look at the network tab when this happens.

before our books endpoint can be called, it looks like we have other things happening. This is the navigation we caused when we called router.push. Next is basically rendering to a new page. The page we’re already on, except with a new querystring. Next is right to assume it needs to do this, but in practice react-query is handling our data. We don’t actually need, or want Next to render this new page; we just want the url to update, so react-query can request new data. If you’re wondering why it navigates to our new, changed page twice, well, so am I. Apparently, the RSC identifier is being changed, but I have no idea why. If anyone does, please reach out to me.

Next has no solutions for this.

The closest Next can come is to let you use window.history.pushState. That will trigger a client-side url update, similar to what used to be called shallow routing in prior versions of Next. This does in fact work; however, it’s not integrated with transitions for some reason. So when this calls, and our useSuspenseQuery hook updates, our current UI will suspend, and our nearest Suspense boundary will show the fallback. This is awful UI. I’ve reported this bug here; hopefully it gets a fix soon.

Next may not have a solution, but react-query does. If you think about it, we already know what query we need to run, we’re just stuck waiting on Next to finish navigating to an unchanging RSC page. What if we could pre-fetch this new endpoint request, so it’s already running for when Next finally finishes rendering our new (unchanged) page. We can, since react-query has an API just for this. Let’s see how.

Let’s look at the react-query search form component. In particular, the part which triggers a new navigation:

startTransition(() => {
  const search = searchParams.get("search") ?? "";
  queryClient.prefetchQuery({
    queryKey: ["books-query", search],
    queryFn: async () => {
      const booksResp = await fetch(`http://localhost:3000/api/books?search=${search}`);
      const { books } = await booksResp.json();

      return { books };
    },
  });

  router.push(currentPath + (queryString ? "?" : "") + queryString);
});

The call to queryClient.prefetchQueryprefetchQuery takes the same options as useSuspenseQuery, and runs that query, now. Later, when Next is finished, and react-query attempts to run the same query, it’s smart enough to see that the request is already in flight, and so just latches onto that active promise, and uses the result.

Here’s our network chart now:

Now nothing is delaying our endpoint request from firing. And since all data loading is happening in react-query, that navigation to our RSC page (or even two navigations) should be very, very fast.

Removing the duplication

If you’re thinking the duplication between the prefetch and the query itself is gross and fragile, you’re right. So just move it to a helper function. In a real app you’d probably move this boilerplate to something like this:

export const makeBooksSearchQuery = (search: string) => {
  return {
    queryKey: ["books-query", search ?? ""],
    queryFn: async () => {
      const booksResp = await fetch(`http://localhost:3000/api/books?search=${search}`);
      const { books } = await booksResp.json();

      return { books };
    },
  };
};

and then use it:

const { data } = useSuspenseQuery(makeBooksSearchQuery(search));

as needed:

queryClient.prefetchQuery(makeBooksSearchQuery(search));

But for this demo I opted for duplication and simplicity.

Before moving on, let’s take a moment and point out that all of this was only necessary because we had data loading tied to the URL. If we just click a button to set client-side state, and trigger a new data request, none of this would ever be an issue. Next would not route anywhere, and our client-side state update would trigger a new react-query.

What about bundle size?

When we did our react-query implementation, we changed our Books component to be a client component by adding the "use client" pragma. If you’re wondering whether that will cause an increase in our bundle size, you’re right. In the RSC version, that component only ever ran on the server. As a client component, it now has to run in both places, which will increase our bundle size a bit.

Honestly, I wouldn’t worry about it, especially for apps like this, with lots of different data sources that are interactive, and updating. This demo only had a single mutation, but it was just that; a demo. If we were to build this app for real, there’d be many mutation points, each with potentially multiple queries in need of invalidation.

If you’re curious, it’s technically possible to get the best of both worlds. You could load data in an RSC, and then pass that data to the regular useQuery hook via the initialData prop. You can check the docs for more info, but I honestly don’t think it’s worth it. You’d now need to define your data loading (the fetch call) in two places, or manually build an isomorphic fetch helper function to share between them. And then with actual data loading happening in RSCs, any navigations back to the same page (ie for querystrings) would re-fire those queries, when in reality react-query is already running those query udates client side. To fix that so you’d have to be certain to only ever use window.history.pushState like we talked about. The useQuery hook doesn’t suspend, so you wouldn’t need transitions for those URL changes. That’s good since pushState won’t suspend your content, but now you have to manually track all your loading states; if you have three pieces of data you want loaded before revealing a UI (like we did above) you’d have to manually track and aggregate those three loading states. It would work, but I highly doubt the complexity would be worth it. Just live with the very marginal bundle size increase.

Just use client components and let react-query remove the complexity with useSuspenseHook.

Wrapping up

This was a long post, but I hope it was a valuable. Next’s app directory is an incredible piece of infrastructure that let’s us request data on the server, render, and even stream component content from that data, all using the single React component model we’re all used to.

There’s some things to get right, but depending on the type of app you’re building, react-query can simplify things a great deal.

🆕 Update

Since publishing this post it was brought to my attention that these fetch calls from the server will not include cookie info. This is by design in Next, unfortunately. Track this issue for updates.

Unfortunately those cookies are needed in practice, for your auth info to be passed to your data requests on the backend. 

The best workaround here would be to read your cookies in the root RSC, and then pass them to the Providers component we already have, for setting up our react-query provider, to be placed onto context. This, by itself, would expose our secure, likely httpOnly cookies into our client bundle, which is bad. Fortunately there’s a library that allows you to encrypt them in a way that they only ever show up on the server.

You’d read these cookie values in all your client components that use useSuspenseQuery, and pass them along in your fetch calls on the server, and on the client, where those values would be empty, do nothing (and rely on your browser’s fetch to send the cookies along) 

]]>
https://frontendmasters.com/blog/combining-react-server-components-with-react-query-for-easy-data-management/feed/ 6 2378
2023 JavaScript Rising Stars https://frontendmasters.com/blog/2023-javascript-rising-stars/ https://frontendmasters.com/blog/2023-javascript-rising-stars/#respond Wed, 07 Feb 2024 02:32:13 +0000 https://frontendmasters.com/blog/?p=742 JavaScript Rising Stars is interesting to look at year after year because of the simple methodology of measuring how many more GitHub Stars a project gets year after year. A project like React with a massive amount of star-based popularity isn’t guaranteed to top the list, and in fact this year clocks in at #7.

The winner? shadcn/ui, notable for it’s extremely unusual distribution method.

This is NOT a component library. It’s a collection of re-usable components that you can copy and paste into your apps.[You] do not install it as a dependency. It is not available or distributed via npm. Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours.

Clearly, this approach really connects with people. The components are React-only, in TypeScript, and styled with a Tailwind dependency. Impressive the level of popularity even with that thick set of requirements.

]]>
https://frontendmasters.com/blog/2023-javascript-rising-stars/feed/ 0 742
Million.js 3.0 https://frontendmasters.com/blog/million-js-3-0/ https://frontendmasters.com/blog/million-js-3-0/#comments Mon, 15 Jan 2024 21:42:29 +0000 https://frontendmasters.com/blog/?p=509 Million.js caught my eye a few months back because of the big claim it makes: Make React 70% faster. I ended up listening to a podcast with the creator, and the meat of it is: it removes the need for “diffing” the virtual DOM that React uses when re-rendering to find what needs to change, which can be slow. I see the project still has momentum, now reaching 3.0.

Skeptical? Good — it’s your job to be skeptical. If this is so amazing, why doesn’t React itself do it? Potential answer: it requires a compiler. That’s a pretty big directional shift for React and I could see them never wanting to go down that road. Although I say that but I’m even more surprised that React will have server requirements (presumably, with server components, right?) And do I actually need this? How complex does my project need to be before I can actually feel React being slow in diffing? What is my technical debt here? How much of my code base has to change to accommodate this? What is this project dies out, where does that leave me? Is there any entirely un-biased endorsements or critical reviews out there to find?

I can’t answer all this for you. I just bring it up because it’s my goal with Boost to get you thinking like you need to think to become a senior developer, and this is part of how.

]]>
https://frontendmasters.com/blog/million-js-3-0/feed/ 3 509
The Two Reacts https://frontendmasters.com/blog/the-two-reacts/ https://frontendmasters.com/blog/the-two-reacts/#respond Tue, 09 Jan 2024 02:42:17 +0000 https://frontendmasters.com/blog/?p=400 A nice bit of writing from Dan Abramov in which he argues for code that should run on the client, and then again for code that should run on the server. It feels like an elaborate setup to explain React Server Components1 in a part two.

Is there some way we could split components between your computer and mine in a way that preserves what’s great about React? Could we combine and nest components from two different environments? How would that work?

It’s neat that React is getter better SSR support or whatever, and I’m sure it’s very neat with how they share state and such. But mostly I like Dan’s approach here of getting people thinking about where certain types of code is the most appropriate place to run. To be sure: you don’t have to wait for React for that.

I talked to a woman yesterday about the curriculum at the college she oversees, and she said the Advanced Web course they teach has students build the same websites three ways, all client-side, all server-side, and a hybrid. The server-side stuff is PHP, because PHP is a perfectly nice server side language that is perfectly good at generating and returning HTML or data, and still quite popular. But it could be Ruby, Python, Node, Go, or anything else. The language doesn’t matter, is why you are doing it.

  1. Josh has a good explainer on them, and there are other good blog posts around about them. But kinda strangely no official docs other than the announcement blog post from years ago. ↩︎
]]>
https://frontendmasters.com/blog/the-two-reacts/feed/ 0 400