React

Lifting State Up in React

Picture of me, Omari
Omari Thompson-Edwards
Mon Feb 19 2024 4 min

Communication between components is an issue you'll be handling a lot with React. Communicating between parents and children is fine, we can use props or context. Communicating between children however can get tricky. 
This is where "lifting state up" comes into play. To lift state, remove state from the children, move it to their closest common parent, and then pass it down to them via props. 

Let's explore an example, by building an email input:

function EmailInput() {
    const [email, setEmail] = useState('');
    const handleChange: ChangeEventHandler<HTMLInputElement> = (e) =>
        setEmail(e.target.value);
    return (
        <label className="flex gap-4 items-center justify-center">
            Email:
            <input
                type="Email"
                className="text-dark"
                value={email}
                onChange={handleChange}
            />
        </label>
    );
}

Great, we're done! A simple state value to track our value, with an event handler to handle the change.

We'll put this together in a simple form component:

function EmailForm() {
    return (
        <form>
            <EmailInput />
            
        </form>
    );
}

Thanks for reading this article, roll credits!

...Alright maybe we're not done, having a "confirm email" input is a pretty common pattern, but that's fine, right?

We'll turn the label into a prop, so we can pass that down, and give it a default value to avoid having to repeat it:

type EmailInputProps = {
    label?: string;
};

function EmailInput({ label = 'Email' }: EmailInputProps) {
    const [email, setEmail] = useState('');
    const handleChange: ChangeEventHandler<HTMLInputElement> = (e) =>
        setEmail(e.target.value);
    return (
        <label className="flex items-center justify-end gap-4">
            {label}:
            <input
                type="Email"
                className="text-dark"
                value={email}
                onChange={handleChange}
            />
        </label>
    );
}

And then just add another component to our form:

function EmailForm() {
    return (
        <form className="flex w-96 flex-col justify-end">
            <EmailInput />
            <EmailInput label="Confirm Email" />
        </form>
    );
}

Great, now all we have to do is check that both the emails are the same.!

Here's where we run into a problem.

Here's what our component tree looks like at the moment:

Props.drawio.svg

If both our email inputs handle their own state, how can we perform any actions that need both values?

We need to lift the state up.

Firstly let's move the email value from the internal state to props:

type EmailInputProps = {
    email: string;
    label?: string;
};

function EmailInput({ email, label = 'Email' }: EmailInputProps) {
    /*const handleChange: ChangeEventHandler<HTMLInputElement> = (e) =>
        setEmail(e.target.value);*/
    return (
        <label className="flex items-center justify-end gap-4">
            {label}:
            <input type="Email" className="text-dark" value={email} />
        </label>
    );
}

We've lost the ability to update the input, but we'll fix that later.

So now in our form component, we have some blanks to fill in:

function EmailForm() {
    return (
        <form className="flex w-96 flex-col justify-end">
            <EmailInput email={}/>
            <EmailInput label="Confirm Email" email={}/>
        </form>
    );
}

Simple, we'll lift up the state hooks from before into the parent component:

function EmailForm() {
    const [email, setEmail] = useState('');
    const [confirmEmail, setConfirmEmail] = useState('');
    return (
        <form className="flex w-96 flex-col justify-end">
            <EmailInput email={email} />
            <EmailInput label="Confirm Email" email={confirmEmail} />
        </form>
    );
}

How do we handle changing the values? Lift up the event handlers as well! We convert the hardcoded function into props:

type EmailInputProps = {
    email: string;
    label?: string;
    onInputChange: ChangeEventHandler<HTMLInputElement>;
};

function EmailInput({
    email,
    onInputChange,
    label = 'Email',
}: EmailInputProps) {
    /**/
    return (
        <label className="flex items-center justify-end gap-4">
            {label}:
            <input
                type="Email"
                className="text-dark"
                value={email}
                onChange={onInputChange}
            />
        </label>
    );
}

And then just define the event handlers in the parent, and pass the values down:

function EmailForm() {
    const [email, setEmail] = useState('');
    const [confirmEmail, setConfirmEmail] = useState('');
    const handleChange: ChangeEventHandler<HTMLInputElement> = (e) =>
        setEmail(e.target.value);

    const handleConfirmChange: ChangeEventHandler<HTMLInputElement> = (e) =>
        setConfirmEmail(e.target.value);

    return (
        <form className="flex w-96 flex-col justify-end">
            <EmailInput email={email} onInputChange={handleChange} />
            <EmailInput
                label="Confirm Email"
                email={confirmEmail}
                onInputChange={handleConfirmChange}
            />
        </form>
    );
}

Great!

Now that we have access to the state of both email inputs from one parent component, we can finally check if the confirmation email is the same as the first email  

function EmailForm() {
    const [email, setEmail] = useState('');
    const [confirmEmail, setConfirmEmail] = useState('');
    const handleChange: ChangeEventHandler<HTMLInputElement> = (e) =>
        setEmail(e.target.value);

    const handleConfirmChange: ChangeEventHandler<HTMLInputElement> = (e) =>
        setConfirmEmail(e.target.value);

    const emailsAreDifferent = email !== confirmEmail;
    return (
        <form className="flex w-96 flex-col justify-end">
            <EmailInput email={email} onInputChange={handleChange} />
            <EmailInput
                label="Confirm Email"
                email={confirmEmail}
                onInputChange={handleConfirmChange}
            />
            {emailsAreDifferent && (
                <div className="bg-red-400">Emails must be identical</div>
            )}
        </form>
    );
}

Conclusion.

So what have we done here? React uses one-way data flow. This means that you can pass values down the component tree from a parent to a child, either through props or context, but not the other way around, or directly between children. Whenever you want to pass data between children, in our example to conditionally render an error message, you need to lift the state up.

read more.

Me
Hey, I'm Omari 👋

I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, feel free to reach out.