How to Update State when Props Change in React
Jasser Mark Arioste
Hello, hustlers! In this tutorial, you'll learn how to update the state when props change.
The Problem #
Sometimes, we have a local component state that depends on the props however the local state is not updated when the props change.
Consider the following code:
import classNames from "classnames"; import { useEffect, useState } from "react"; export default function ParentComponent() { const [scrollYPos, setScrollYPos] = useState(0); useEffect(() => { window.addEventListener("scroll", () => { setScrollYPos(window.scrollY); }); }, []); return ( <div className="h-[2000px] container mx-auto"> <Navbar scrollYPos={scrollYPos} /> </div> ); } type NavbarProps = { scrollYPos: number; }; export function Navbar({ scrollYPos }: NavbarProps) { const [scrolled] = useState(scrollYPos >= 100); return ( <div className={classNames({ "fixed top-0 w-screen bg-indigo-500 text-white": scrolled, "w-full bg-transparent text-indigo-500": !scrolled, "border-b p-2": true, })} > Navbar </div> ); }
123456789101112131415161718192021222324252627282930313233343536
Explanation:
Here we have the Navbar
component that depends on the scrollYPos
props. It has a local component state scrolled
that decides if the Navbar will be fixed to the top of the screen.
In the ParentComponent
, the scrollYPos
state changes when the user scrolls. Now, you might think that the <Navbar/>
component will re-initialize the useState
hook on line 23 but this is not the case. The scroll position remains the same.
Below is the current output. Once the user scrolls, nothing happens to the navbar.
Solution #
The solution is very simple. There are two choices you can make:
- Use the
useEffect
hook to listen if thescrollYPos
prop changes. - Remove the
useState
and rely on a derived state. This is usually the cleaner approach but if you can't use it, you can still use option 1. This is my preferred option - Re-render the
Navbar
component using thekey
prop.
Approach 1- Using useEffect hook #
We can use the useEffect
hook to listen to changes to props
and update our state this way. For example:
// ... omitted for brevity type NavbarProps = { scrollYPos: number; }; export function Navbar({ scrollYPos }: NavbarProps) { const [scrolled, setScrolled] = useState(scrollYPos >= 1); useEffect(() => { setScrolled(scrollYPos >= 1); }, [scrollYPos]); // add scrollYPos as a dependency return ( <div className={classNames({ "fixed top-0 w-screen bg-indigo-500 text-white": scrolled, "w-full bg-transparent text-indigo-500": !scrolled, "border-b p-2": true, })} > Navbar </div> ); }
1234567891011121314151617181920212223
Now, we can see that the Navbar
changes when the user scrolls down the page:
Approach 2 - Computing the Derived State - A Cleaner approach #
A cleaner approach would be to get rid of the useState
and useEffect
and calculate the scrolled
state immediately. This is useful if you don't need a local state in your component.
For example:
// omitted for brevity type NavbarProps = { scrollYPos: number; }; export function Navbar({ scrollYPos }: NavbarProps) { // const [, setScrolled] = useState(scrollYPos >= 1); // useEffect(() => { // setScrolled(scrollYPos >= 1); // }, [scrollYPos]); const scrolled = scrollYPos >= 1; return ( <div className={classNames({ "fixed top-0 w-screen bg-indigo-500 text-white": scrolled, "w-full bg-transparent text-indigo-500": !scrolled, "border-b p-2": true, })} > Navbar </div> ); }
12345678910111213141516171819202122232425
Approach 3 - Using key
prop in the ParentComponent
#
The key
prop can also be used by all components to trigger a complete re-initialization or reset of the component. This technique is useful if you want to reset components like forms.
Here's an example of using they key
prop:
import classNames from "classnames"; import { useEffect, useState } from "react"; export default function ParentComponent() { const [scrollYPos, setScrollYPos] = useState(0); useEffect(() => { window.addEventListener("scroll", () => { setScrollYPos(window.scrollY); }); }, []); return ( <div className="h-[2000px] container mx-auto"> <Navbar scrollYPos={scrollYPos} key={scrollYPos} /> </div> ); } type NavbarProps = { scrollYPos: number; }; export function Navbar({ scrollYPos }: NavbarProps) { const [scrolled] = useState(scrollYPos >= 1); return ( <div className={classNames({ "fixed top-0 w-screen bg-indigo-500 text-white": scrolled, "w-full bg-transparent text-indigo-500": !scrolled, "border-b p-2": true, })} > Navbar </div> ); }
123456789101112131415161718192021222324252627282930313233343536
Here, we make sure to pass a different key to trigger a reset of the Navbar
component. On line 23, we still have a useState hook but we have to don't do anything.
Conclusion #
You learned three different ways to update the state when the props
changes for React functional components. Make sure you use the second approach whenever possible, and make your best judgment on whether to use the first or third approach.
If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on GitHub.
Resources #
Credits: Image by Ingo Jakubke from Pixabay