React 19.2 marks the third major release within a year, bringing significant new features and improvements to the React ecosystem.
This release introduces new features and improvements to the React ecosystem:
- The
<Activity>
component to better manage the visibility of a tree - The
useEffectEvent
hook to create a stable function that always has access to the latest props and state within auseEffect
hook - Partial Pre-rendering
- Enhanced Chrome DevTools integration
In general, there are no breaking changes in this release, but there are some new EsLint rules that are stricter and may help catch some bugs and anti-patterns, and may require some refactoring.
The official React documentation has a comprehensive list of all the new features and improvements in React 19.2. I highly recommend reading it.
In this article, I want to focus on the most important changes that affected the code in Makerkit.
EsLint rules: new strict rules about immutability and purity of React Hooks usage
This release also comes with an updated EsLint ruleset that is more strict and may help catch some bugs. However, it will surface some popular anti-patterns that were previously allowed, such as updating the state in a useEffect
hook.
I will document here some of the changes that affected the code in Makerkit and how to fix them.
Activity Component
The <Activity>
component allows developers to control the visibility of a tree based on the mode
prop.
You can pretty much change your code from ternary operators and conditionals to the <Activity>
component.
Before React 19.2:
{currentTab === 'home' && <HomePage />}
With React 19.2:
<Activity mode={currentTab === 'home' ? 'visible' : 'hidden'}> <HomePage /></Activity>
The difference between the two ways is that the <Activity>
component will preserve the state of the component when it is hidden, while the ternary operator will not.
Depending on your use case, you may prefer one over the other, so they're not always interchangeable.
Here are the times when you want to use the <Activity>
component:
- pre-rendering a component that the user is likely to navigate to but is not currently visible
- you want to preserve the state of a component when it is hidden (ex. a form that is not submitted when the user navigates away)
For a comprehensive documentation about the <Activity>
component, check out the official React documentation.
useEffectEvent Hook
The useEffectEvent
hook allows you to create a stable function that always has access to the latest props and state, and is supposed to be used within a useEffect
hook.
This is very useful when you need to execute an event handler function within a useEffect
hook that depends on the latest props and state. In this way, you don't need to pass these dependencies to the useEffect
hook (which trigger the re-execution of the useEffect hook when they change) which prevents unnecessary re-executions of the useEffect
hook. The linter won't bug you about the missing dependencies and you can still access the latest props and state within the event handler function.
In Makerkit, I migrated quite a few functions to use the useEffectEvent
hook - which allowed me to remove a lot of dependencies from the useEffect
hook, and make the code more readable and maintainable.
Homework: locate the useEffect
hooks in your codebase and try to see if they use unnecessary dependencies. If you do, try to migrate them to use the useEffectEvent
hook to reduce the number of re-renders and make your code more readable, performant and less error-prone.
ESLint Plugin v7
The latest version of the EsLint plugins for React introduce some strict rules about immutability and purity of React Hooks usage.
Makerkit uses strict EsLint and Typescript rules, so we make sure to follow these rules to the letter. Our high standards meant we couldn't just disable the rules - we had to refactor the code to follow the rules and the best practices in React.
In general, following these rules will make your code more stable, predictable and ready to be used with the upcoming React Compiler, which has recently been moved out of the experimental phase and stabilized for production use.
Setting state in the useEffect hook
If you've worked with React for a while, you may be used to settings state using props values:
function MyComponent({ value }) { const [state, setState] = useState(value); // ❌ This is not allowed by the EsLint rules useEffect(() => { setState(value); }, [value]);}
This was an anti-pattern for a long time, but it was allowed by the EsLint rules. Now, it's not allowed anymore, so your rules may break if you're using this pattern.
React suggests updating the state in the render function instead:
function MyComponent({ value }) { const [state, setState] = useState(value); // ✅ This is allowed by the EsLint rules if (value !== state) { setState(value); } return <div>{state}</div>;}
There are many other scenarios where you may want to set state in the useEffect hook. You may not need an effect is a great article that covers many of these scenarios and provides a lot of guidance on when to use the useEffect
hook and when not to use it.
Scenario: using useFetcher in React Router 7
For example, this required me to update a lot of code in the React Router 7 whereas it's suggested to use useEffect
to react to the data fetching state. useFetcher
famously does not return a Promise when the request executes, which can make things a bit more complex when, for example, you want to do something at the end of the request - such as displaying a notification, closing a dialog, etc.
Before, we would use useFetcher
to fetch data and use useEffect
to react to the data fetching state:
function MyComponent() { const fetcher = useFetcher(); const [isSuccess, setIsSuccess] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { if (fetcher.data?.success) { setIsSuccess(true); } else if (fetcher.data?.error) { setIsError(true); } }, [fetcher.data]); return ( <div> <If condition={isSuccess}> <div>Success</div> </If> <If condition={isError}> <div>Error</div> </If> <fetcher.Form method="post"> <input type="text" name="name" /> <button type="submit">Submit</button> </fetcher.Form> </div> );}
The issue is we're setting the state in the useEffect
hook, which is not allowed by the EsLint rules - and is a poor practice in general which may lead to infinite re-renders.
The fix is to simply use the state in the render function:
function MyComponent() { const fetcher = useFetcher(); const isSuccess = fetcher.data?.success; const isError = fetcher.data && !fetcher.data.success; return ( <div> <If condition={isSuccess}> <div>Success</div> </If> <If condition={isError}> <div>Error</div> </If> <fetcher.Form method="post"> <input type="text" name="name" /> <button type="submit">Submit</button> </fetcher.Form> </div> );}
This still doesn't solve the issue where we want to display a notification at the end of the request. In this case, I suggest to split the component into two: one is a form, and the other is a container that receives notifications from the form.
function MyForm({ onSuccess, onError,}) { const fetcher = useFetcher(); useEffect(() => { if (fetcher.data) { if (fetcher.data.success) { onSuccess?.(); } else { onError?.(); } } }, [fetcher.data, onSuccess, onError]); return <fetcher.Form method="post"> <input type="text" name="name" /> <button type="submit">Submit</button> </fetcher.Form>}
The container component can then receive the notifications from the form and display them, using the useCallback
hook to memoize the functions:
import { useCallback } from 'react';import { toast } from 'sonner';function MyContainer({ onSuccess, onError,}) { const onSuccess = useCallback(() => { toast.success('Success'); }, []); const onError = useCallback(() => { toast.error('Error'); }, []); return ( <MyForm onSuccess={onSuccess} onError={onError} /> );}
Reading or writing to references in the render function
Reading or writing the value of a reference in the render function is not allowed by the EsLint rules and is generally considered a bad practice.
function MyComponent() { const ref = useRef(0); // ❌ This is not allowed by the EsLint rules return <div>{ref.current}</div>;}
React suggests to use useState
instead:
function MyComponent() { const [state, setState] = useState(0); return <div>{state}</div>;}
Using a ref allows you to:
- Persist data across re-renders - unlike regular variables, which reset every time your component renders.
- Avoid unnecessary re-renders - updating a ref’s value doesn’t cause your component to re-render, unlike updating state.
- Keep data isolated per component instance - each component gets its own ref, unlike external variables that are shared globally.
However, because changing a ref doesn’t trigger a re-render, refs aren’t suitable for values that need to appear on the screen. Use useState instead for anything that affects what your component renders and for reading or writing to data in the render function.
Disabling the EsLint rules
If you feel like the EsLint rules are too strict, you can disable them on a per-line basis:
// eslint-disable-next-line react-hooks/set-state-in-effect
Or you can disable it globally by adding it to the main eslint config file:
tooling/eslint/base.js
rules: { 'react-hooks/set-state-in-effect': 'off',},
Conclusion
React 19.2 is a refinement release that rewards disciplined developers - not a flashy overhaul, but a tightening of how React wants you to think about effects, state, and visibility.
The new <Activity>
component and useEffectEvent
hook move React closer to a declarative, predictable model where components express intent, not mechanics. The stricter ESLint rules push teams toward more deterministic and pure code - enforcing practices that long-time React devs have known intuitively but often ignored for convenience.
If you maintain a large React codebase, take this update as an opportunity to:
- Audit your effects - remove unnecessary ones and replace them with
useEffectEvent
where possible. - Revisit your state logic - ensure that you’re not mutating values or using effects to mirror props.
- Embrace
<Activity>
for smarter pre-rendering and preserved component state.
React 19.2 doesn’t just modernize the API surface - it nudges the ecosystem toward a more functional, predictable, and resilient architecture. Upgrade deliberately, refactor cleanly, and your codebase will come out more maintainable, faster, and future-ready.
The Makerkit SaaS Boilerplate is already using React 19.2 and all the new features and improvements. Make sure to upgrade to the latest version to get the benefits of the new features and improvements!