- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
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 Info
Table of Contents
-
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
applicationdirectory 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. -
Challenge
Step 2: Replace forwardRef with Context providers
Here, you'll complete the pre-provided
FormFieldContext, then refactorTextFieldandSelectto expose their underlying DOM refs through that context instead of throughforwardRef. Note that the direction of sharing changes:forwardRefexposes 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 onforwardRefat 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: wrappingchildrenso any descendant that reads the context receives the given value. For example, given a context created asconst 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 forwardRefforwardReflets 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 createconst boxRef = useRef(null), render<div ref={boxRef} />, and wrap it in<BoxRefContext value={boxRef}>{children}</BoxRefContext>, letting anything rendered inside readboxRefthrough a hook likeuseBoxRef(). ### Applying the pattern to a second host elementThe context-sharing pattern from
TextFieldisn'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. -
Challenge
Step 3: Remove manual memoization
TextFieldandSelectnow share their DOM refs throughFormFieldContextinstead offorwardRef, one piece of the legacy pattern retired. Here, you'll audit the memoization inFilterableList: you'll remove theuseMemoaround its cheap filter calculation and theuseCallbackaround its selection handler, both of which become unnecessary once the team's React Compiler migration lands.Note that
useMemoanduseCallbackare 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,FilterableListwill carry only the memoization it actually needs, and the compiler will handle the rest automatically.
When a computation doesn't need useMemo
useMemotrades 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 inuseMemoadds 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 useCallbackuseCallbackexists 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 callRanking 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
useMemofor: 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 pluginThe 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-pluginmight 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. -
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 aSearchPanel, 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 calluseFormField()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 callconst ref = useSomeContext()and, inside auseEffect, readref.currentto 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 componentsTextFieldandFilterableListdon'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.SearchPanelis that parent: it can hold the query in its own state, pass the input's current value and change handler toTextField, and pass the same query down toFilterableList. The same pattern applies to the selected item:FilterableListreports a selection upward through itsonSelectprop, andSearchPanelstores and displays whatever it's given. Nice work. You've modernized the entire component library:TextFieldandSelectnow share their DOM refs through a Context provider instead offorwardRef, andFilterableListcarries 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
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.