Modern React 6: Lifting state and controlled inputs
This is part 6 of the Modern React development series.
State starts local, then moves when more than one component needs the same fact. Lifting state gives one parent ownership of that fact, and controlled inputs make form fields render from state instead of carrying separate browser-only memory.
Concept
Lifting state means moving state to the closest common parent of the components that need it. A controlled input is a form control whose value comes from React state and reports edits through an event handler.
Terms
- Lifted state: State owned by a parent so multiple children can read or change it.
- Controlled input: An input whose
valueorcheckedprop is set by React state. - Uncontrolled input: An input whose current value is kept by the browser until code reads it.
- Closest common parent: The nearest component above all components that need a shared value.
Mental model
Treat the parent as the notebook and children as pens and readers. The child can report a new value, but the parent keeps the canonical note and passes the current value back down.
How it is used
Lift state for search fields that filter a sibling list, tabs that choose which panel is visible, forms that enable a submit button, and shared counters. Use controlled inputs when the current value affects rendering while the user types.
How to use it
- Find the components that need to read or update the same value.
- Move the state to their closest common parent.
- Pass the current value down as a prop.
- Pass an event callback down so children can request updates.
- Use
valueplusonChangefor text inputs andcheckedplusonChangefor checkboxes.
Example: Shared search query
import { useState } from "react";import { ProductList } from "./ProductList";
type Product = { id: string; name: string };
export function ProductSearch({ products }: { products: Product[] }) { const [query, setQuery] = useState(""); const visible = products.filter((product) => product.name.toLowerCase().includes(query.toLowerCase()), );
return ( <> <SearchField value={query} onChange={setQuery} /> <ProductList products={visible} /> </> );}
function SearchField({ value, onChange,}: { value: string; onChange: (value: string) => void;}) { return ( <input aria-label="Search products" value={value} onChange={(event) => onChange(event.target.value)} /> );}- Trail shoes
- Rain shell
The parent owns the query because both the input and the list depend on it.
Example: Controlled checkbox
import { useState, type ChangeEvent } from "react";
function InventoryFilter() { const [inStockOnly, setInStockOnly] = useState(true);
return ( <InStockOnly checked={inStockOnly} onChange={setInStockOnly} /> );}
function InStockOnly({ checked, onChange,}: { checked: boolean; onChange: (checked: boolean) => void;}) { function handleChange(event: ChangeEvent<HTMLInputElement>) { onChange(event.target.checked); }
return ( <label> <input type="checkbox" checked={checked} onChange={handleChange} /> In stock only </label> );}The parent owns the boolean. The child renders the checkbox from checked and reports the next boolean through onChange. Checkboxes use checked, not value, because the rendered state is binary.
Details to watch
- Typing responsiveness: The state update for a text input should stay urgent. Do not wrap the input’s own value update in a Transition.
- Default values: Use
defaultValueordefaultCheckedfor uncontrolled inputs. Usevalueorcheckedfor controlled ones. - Ownership: Do not lift state higher than the components that need it. High state causes extra coupling.
- Parsing: Input values are strings. Convert to numbers, dates, or structured values at the boundary that needs that type.
Series navigation
- Previous: Part 5: State shape and derived values
- Next: Part 7: Reducers for multi-step state
- Series index: Modern React development