Over the past year, I noticed I was deleting more code than I was writing. Not because I was cleaning up old mistakes, but because React had quietly started doing things I used to do myself.
At first, I thought it was just me getting more comfortable with the codebase, but then I realized React itself had changed, and a lot of what I was writing by hand had a simpler, built-in way of doing it now.
These are five of those patterns, which I was writing before, and what replaced them after React 19.
1. Stopped Writing useMemo and useCallback
For a long time, these two were almost a reflex. Open a component, see a function being passed as a prop, wrap it in useCallback. See a filtered list being computed, wrap it in useMemo. It was just what you did. Not always because you had profiled something and found a problem, but because it felt responsible.
The React Compiler changed that. It runs at build time, reads your component, and handles the memoization automatically. You write the component the normal way, and the compiler figures out what needs to be cached and what does not.
Here's what that change looks like:
Before
const filteredList = useMemo(() => {
return items.filter(item => item.active);
}, [items]);
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);
After
const filteredList = items.filter(item => item.active);
const handleSelect = (id) => {
setSelected(id);
};
Same behavior. Fewer decisions to make manually.
The thing worth saying honestly is that writing dependency arrays wasn't just extra work; it forced you to think about what a function actually depended on, what changed between renders, and what did not. That thinking built something real over time. The compiler does not take that understanding away. It just stops asking you to prove it on every component.
When you enable it, you will find yourself going through old code and deleting a lot of these.
2. Stopped Writing forwardRef
If you have built any kind of reusable input, a custom text field, a dropdown, or a date picker, you have written forwardRef. The reason was straightforward: by default, React does not let a parent component access the DOM node inside a child component. If you needed to do something like focus an input from a button click outside that component, you had to explicitly opt in by wrapping the whole component in forwardRef.
It worked, but it always felt like an extra step for something that should have been simple.
// The old way
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput({ label }, ref) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
});
That forwardRef wrapper, the second ref argument after props, the import, all of it just to pass a ref through. In React 19, ref is now just a prop like any other. You write the component the normal way and accept ref directly.
// Now
function MyInput({ label, ref }) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
Same behavior. No wrapper, no separate argument, no extra import.
forwardRef is deprecated now. The React team has also mentioned a codemod to automatically update existing components, so you will not have to go through every file manually. But if you are starting something new today, there is no reason to use forwardRef at all.
3. Stopped Writing <Context.Provider>
This one is small, but you notice it every single time. If you use Context in your app, you wrap things in <SomeContext.Provider value={...}>. Every time. It is just how it worked.
React 19 lets you render the context object directly as the provider.
Before
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
After
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext value="dark">
<Page />
</ThemeContext>
);
}
That is the entire change. .Provider is no longer necessary and will be deprecated in a future version.
4. Stopped Managing Form State with Three useState Calls
Every form that talked to an API had the same setup. You needed to know if the request was running, you needed to store the result if it succeeded, and you needed to store the error if it did not. That meant three separate pieces of state before you even wrote the submission logic.
// The old wiring
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
const [result, setResult] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
setError(null);
try {
const data = await submitForm(formData);
setResult(data);
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
}
It worked, but you wrote some version of this for every form in every project. The shape was always the same: three states, a try/catch, manually toggling pending on and off.
useActionState collapses all of that. You give it an async action function and an initial state, and it gives you back the current state, a dispatch function to trigger the action, and an isPending flag. Everything the form needed is just coming from one place now.
// With useActionState
import { useActionState } from 'react';
async function submitAction(prevState, formData) {
try {
const data = await submitForm(formData);
return { data, error: null };
} catch (err) {
return { data: null, error: err.message };
}
}
function MyForm() {
const [state, dispatch, isPending] = useActionState(submitAction, {
data: null,
error: null,
});
return (
<form action={dispatch}>
<input name="email" type="email" />
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
{state.error && <p>{state.error}</p>}
</form>
);
}
A few things worth pointing out here. The action function receives the previous state as its first argument and the submitted FormData as the second. You return the new state from it directly, no setState calls inside the action. And when you pass dispatch to the action prop on a <form>, React automatically handles the transition, so you do not need to wrap anything in startTransition yourself.
What changed is not just the amount of code. It is that the logic is now in one place. Before, you had three separate state variables that all needed to update together at the right time. You may miss one, and the UI shows the wrong state. With useActionState, that is just taken care of. You write the action, it runs, and you get the result.
5. Stopped Installing react-helmet for Page Metadata
react-helmet was one of those installs that just happened on every project. You needed different page titles on different routes, maybe some meta descriptions, and Open Graph tags for sharing. The browser <head> is outside your component tree, so React had no native way to touch it. react-helmet solved that by intercepting those tags and placing them in the right spot.
But now, React 19 handles this natively. You can drop <title>, <meta>, and <link> tags directly inside any component, and React hoists them to <head> automatically.
Before
import { Helmet } from 'react-helmet';
function BlogPost({ post }) {
return (
<>
<Helmet>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
</Helmet>
<article>{post.content}</article>
</>
);
}
After
function BlogPost({ post }) {
return (
<>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<article>{post.content}</article>
</>
);
}
React also deduplicates these automatically. If multiple components render a <title> at the same time, React keeps the last one, which is exactly what you want when navigating between pages.
Note: for more complex SEO needs in a framework like Next.js, the Metadata API it provides is still the right tool. But for a straightforward React app where you just need per-page titles and basic meta tags, you no longer need a library for that.
Closing
These changes made React easier to understand. They removed obstacles between your intention and the code. Now, to pass a ref, you don't need a wrapper. For a form to track its state, you don't need three variables. To set a page title, you just write the tag.
The upgrade is worth it, not because of any single feature but because of how all five of these add up. Less boilerplate means less to explain, less to maintain, and less to get wrong. And these five are just the ones that changed my day-to-day the most. React 19 also brought use() for reading resources in render, improvements to Suspense, better error reporting, and a lot more worth exploring once you are settled in.
If you are already on React 19, which of these have you adopted? And if there is one you are still holding off on, drop it in the comments. I would love to know.
That's it for this article. I'll see you in the next one. Bye 👋

Top comments (0)