Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Guided: Upgrading to React 19

In this lab, you'll modernize a legacy set of React components by replacing forwardRef wrappers with the current Context provider pattern and auditing manual memoization for the React Compiler, removing what the compiler makes unnecessary while keeping deliberate memoization for genuinely expensive work. By the end, you'll be able to recognize outdated ref-forwarding and memoization patterns, replace them with idiomatic, compiler-friendly React code, and enable the React Compiler itself.

Lab platform
Lab Info
Level
Intermediate
Last updated
Sep 03, 2026
Duration
30m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Introduction

    Welcome to the Guided: Upgrading to React 19 CodeLab.


    Every long-lived React codebase eventually accumulates patterns that were once best practice and are now just extra code to maintain. Recognizing which patterns have aged out and replacing them is a skill you'll use on nearly every real-world React codebase you touch.

    In this lab, you'll modernize a small internal component library: you'll replace forwardRef-based ref forwarding with React's Context provider pattern, decide which of the library's memoized calculations the React Compiler now makes unnecessary, and enable the compiler itself in the project's build configuration. You'll put every refactored piece to the test by composing them into a working search panel, verified by the library's existing test suite. To begin, you'll get familiar with the two legacy patterns this lab retires: how the components currently forward refs, and which of their memoized calculations are genuinely expensive versus merely leftover caution.

    The application directory already contains the component library and its existing Vitest test suite, ready for you to modernize.


    warning> If VS Code shows a Do you trust the authors of the files in this folder? prompt, click Yes, I trust the authors. Trusting the workspace is required for VS Code to run and debug the code in this lab.

    info> Feeling stuck? Check out the matching solution/stepN/ folder for the step you're on to see a working implementation. Give it a try on your own first. The solution folder is your safety net, not your starting point.

  2. Challenge

    Step 2: Replace forwardRef with Context providers

    Here, you'll complete the pre-provided FormFieldContext, then refactor TextField and Select to expose their underlying DOM refs through that context instead of through forwardRef. Note that the direction of sharing changes: forwardRef exposes a component's ref upward to its parent, while the Context pattern shares it downward with descendants. By the end of this step, both components will share their refs the same way, without relying on forwardRef at all.


    Understanding the modern Context provider syntax

    React lets you share data down a component tree using createContext. Historically, sharing a value meant wrapping descendants in <SomeContext.Provider value={...}>; React now also lets you render the context object directly as <SomeContext value={...}>, which is the preferred form going forward. Both accomplish the same thing: wrapping children so any descendant that reads the context receives the given value. For example, given a context created as const ThemeContext = createContext(null), its provider component would render <ThemeContext value={theme}>{children}</ThemeContext> instead of the older <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>. ### Understanding ref sharing through Context instead of forwardRef

    forwardRef lets a parent component pass a ref down into a child so the parent can reach the child's underlying DOM node. Once a component wraps its own element in a context provider instead, sharing works in the opposite direction: the component creates its own ref, attaches it to the element, and supplies that ref as the context value so any descendant can read it instead of a parent. For example, a component might create const boxRef = useRef(null), render <div ref={boxRef} />, and wrap it in <BoxRefContext value={boxRef}>{children}</BoxRefContext>, letting anything rendered inside read boxRef through a hook like useBoxRef(). ### Applying the pattern to a second host element

    The context-sharing pattern from TextField isn't specific to <input> elements: any DOM element accepts a ref the same way, so a <select> element can supply its own ref through a context provider using the identical shape. Recognizing that ref-forwarding alternatives generalize across element types, rather than treating each component as a special case, is what makes this refactor scale across a whole component library.

  3. Challenge

    Step 3: Remove manual memoization

    TextField and Select now share their DOM refs through FormFieldContext instead of forwardRef, one piece of the legacy pattern retired. Here, you'll audit the memoization in FilterableList: you'll remove the useMemo around its cheap filter calculation and the useCallback around its selection handler, both of which become unnecessary once the team's React Compiler migration lands.

    Note that useMemo and useCallback are not deprecated: the React docs keep them as the right tool for precise control over genuinely expensive work, and you'll apply exactly that judgment by deliberately memoizing the list's costly relevance ranking. To close the step, you'll land the migration yourself by registering the compiler in the project's build configuration. By the end of this step, FilterableList will carry only the memoization it actually needs, and the compiler will handle the rest automatically.


    When a computation doesn't need useMemo

    useMemo trades some memory and a dependency comparison on every render for skipping a recomputation, which only pays off when the computation itself is expensive enough to matter. Filtering a short list by a substring match runs in a fraction of a millisecond, so wrapping it in useMemo adds bookkeeping overhead without meaningfully speeding anything up. Prefer a plain expression, computed fresh on each render, whenever the underlying work is this cheap. ### When an inline handler doesn't need useCallback

    useCallback exists to keep a function's identity stable across renders, which matters when that identity is a dependency somewhere else, such as in another hook's dependency array or a memoized child component. The selection handler here is only ever invoked directly from a click, and nothing downstream depends on its reference staying the same between renders. Prefer a plain inline function whenever a handler's identity isn't itself load-bearing. ### Recognizing when memoization is still the right call

    Ranking a list by relevance is real, non-trivial computation, unlike the cheap filtering or handler wrapping you removed in the last two tasks: it's the kind of work worth skipping on renders where its inputs haven't changed. This is exactly the case the React docs still reserve useMemo for: precise, deliberate control over a genuinely expensive calculation, not a default wrapped around everything. Keying the memoized computation on the values it actually reads keeps it correct as those values change. ### Registering the React Compiler as a build plugin

    The React Compiler isn't something you import into a component: it's a Babel plugin that transforms your components' code during the build, automatically adding the memoization your last three tasks handled by hand for the parts you removed. Registering it means adding its plugin identifier to the list of Babel plugins your bundler's React plugin already applies. For example, enabling a hypothetical some-babel-plugin might look like adding it to a plugins array: const babelPlugins = ['some-babel-plugin'], which a bundler's React integration then reads when it builds your code.

  4. Challenge

    Step 4: Compose and verify the modernized components

    Every component now uses the modern patterns: context-shared refs instead of forwardRef, and only deliberate, justified memoization. Here, you'll prove the pieces work together by composing them into a SearchPanel, first wiring focus through the context ref and then connecting the query and selection flow end to end.


    Reading a shared ref from a small consumer component

    Anything rendered inside a FormFieldProvider's subtree can call useFormField() to read the ref it's sharing, including a small component whose only job is to act on that ref rather than render visible content itself. For example, a component might call const ref = useSomeContext() and, inside a useEffect, read ref.current to trigger a side effect like focusing an element, since a ref only points at its DOM node after the initial render has committed. You'll write two such components: one that focuses an input on mount, and one that reads and displays an element's tag name. ### Lifting state to coordinate sibling components

    TextField and FilterableList don't know about each other, so whatever connects them, the current query text, has to live in their shared parent and flow down as props. SearchPanel is that parent: it can hold the query in its own state, pass the input's current value and change handler to TextField, and pass the same query down to FilterableList. The same pattern applies to the selected item: FilterableList reports a selection upward through its onSelect prop, and SearchPanel stores and displays whatever it's given. Nice work. You've modernized the entire component library: TextField and Select now share their DOM refs through a Context provider instead of forwardRef, and FilterableList carries only the memoization its ranking computation actually needs, with the React Compiler registered to handle the rest automatically.

    Along the way, you exercised the judgment at the center of this kind of migration: recognizing when a memoization hook is dead weight versus when it's still the right tool for precise control, and knowing which of React's Context patterns to reach for when a component needs to share more than props allow.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight