RE

State Management

React · 6 entries

Lifting State Up

syntax
// Move shared state to the nearest common ancestor
function Parent() {
  const [shared, setShared] = useState(init);
  return (
    <>
      <ChildA value={shared} />
      <ChildB onChange={setShared} />
    </>
  );
}
example
function TemperatureCalculator() {
  const [temp, setTemp] = useState('');
  const [scale, setScale] = useState('c');

  const celsius = scale === 'f' ? ((Number(temp) - 32) * 5/9).toFixed(1) : temp;
  const fahrenheit = scale === 'c' ? ((Number(temp) * 9/5) + 32).toFixed(1) : temp;

  return (
    <div>
      <TemperatureInput
        label="Celsius"
        value={celsius}
        onChange={val => { setTemp(val); setScale('c'); }}
      />
      <TemperatureInput
        label="Fahrenheit"
        value={fahrenheit}
        onChange={val => { setTemp(val); setScale('f'); }}
      />
    </div>
  );
}

function TemperatureInput({ label, value, onChange }) {
  return (
    <label>
      {label}:
      <input value={value} onChange={e => onChange(e.target.value)} />
    </label>
  );
}

Note When two components need the same data, move the state to their closest shared parent and pass it down as props. This is the fundamental React data flow pattern.

Prop Drilling Solutions

syntax
// 1. Context
// 2. Composition (pass components, not data)
// 3. Component composition with children
example
// PROBLEM: drilling "user" through many layers
// <App user={user}> -> <Layout user> -> <Header user> -> <Avatar user>

// SOLUTION 1: Context
const UserContext = createContext(null);
// provide at top, consume at Avatar

// SOLUTION 2: Composition -- pass the rendered element
function App({ user }) {
  const avatar = <Avatar user={user} />;
  return <Layout header={<Header avatar={avatar} />} />;
}

// Now Layout and Header don't need to know about "user" at all
function Layout({ header }) {
  return (
    <div>
      <nav>{header}</nav>
      <main>...</main>
    </div>
  );
}

function Header({ avatar }) {
  return <div className="header">{avatar}</div>;
}

Note Before reaching for context, try component composition. Passing pre-rendered elements as props skips intermediate layers entirely and keeps the data flow explicit. Context is better for truly global data (theme, auth, locale).

Context for Global State

syntax
// Create -> Provide -> Consume
const Ctx = createContext(defaultValue);
<Ctx.Provider value={value}>...
const value = useContext(Ctx);
example
// Locale context for internationalization
const LocaleContext = createContext('en');

function LocaleProvider({ children }) {
  const [locale, setLocale] = useState('en');

  const translations = useMemo(
    () => loadTranslations(locale),
    [locale]
  );

  const value = useMemo(
    () => ({ locale, setLocale, t: (key) => translations[key] || key }),
    [locale, translations]
  );

  return (
    <LocaleContext.Provider value={value}>
      {children}
    </LocaleContext.Provider>
  );
}

function useLocale() {
  return useContext(LocaleContext);
}

// Usage in any component
function WelcomeBanner() {
  const { t, locale } = useLocale();
  return <h1>{t('welcome_message')} ({locale})</h1>;
}

Note Context is ideal for low-frequency updates like theme, locale, and auth. For high-frequency updates (e.g., mouse position), context can cause excessive re-renders -- consider useSyncExternalStore or a dedicated state library.

When to Use External State

syntax
// Consider external state management when:
// - Many components read/write the same state
// - State updates are frequent and complex
// - You need middleware (logging, persistence, devtools)
// - Server state caching is needed (React Query, SWR)
example
// Signs you might need a state library:

// 1. Context re-renders are a bottleneck
// function App() {
//   return (
//     <StoreProvider>  {/* every consumer re-renders on any change */}
//       <BigComponentTree />
//     </StoreProvider>
//   );
// }

// 2. You need selectors (subscribe to a slice)
// const count = useStore(state => state.count);

// 3. You need server state cache with stale-while-revalidate
// const { data, isLoading } = useQuery('users', fetchUsers);

// Popular choices:
// - Zustand: lightweight, hook-based, selectors
// - Jotai: atomic state, bottom-up approach
// - React Query / TanStack Query: server state management
// - Redux Toolkit: complex client state with devtools

Note Start with useState and useReducer + context. Only add an external library when you hit a concrete pain point: performance issues from context re-renders, complex async logic, or the need for devtools and middleware.

Derived State

syntax
// Compute from existing state during render
const derivedValue = computeFrom(stateA, stateB);
// Do NOT store derived values in separate state
example
function ShoppingCart({ items }) {
  // GOOD: derive during render
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  const tax = subtotal * 0.08;
  const total = subtotal + tax;
  const itemCount = items.reduce((sum, item) => sum + item.qty, 0);

  return (
    <div>
      <p>{itemCount} items</p>
      <p>Subtotal: ${subtotal.toFixed(2)}</p>
      <p>Tax: ${tax.toFixed(2)}</p>
      <p>Total: ${total.toFixed(2)}</p>
    </div>
  );

  // BAD: storing derived data in state
  // const [total, setTotal] = useState(0);
  // useEffect(() => setTotal(subtotal + tax), [subtotal, tax]);
}

Note If you can compute a value from existing state or props, derive it inline during render instead of syncing it with useState + useEffect. Derived state in useEffect causes an extra render cycle and introduces potential bugs.

State Colocation

syntax
// Keep state as close to where it is used as possible
// Move state DOWN if only one child needs it
// Move state UP only when siblings need to share it
example
// BAD: state lives too high
function App() {
  const [searchQuery, setSearchQuery] = useState('');
  return (
    <Layout>
      <Header />
      <SearchPanel query={searchQuery} setQuery={setSearchQuery} />
      <Footer />
    </Layout>
  );
}

// GOOD: state lives in the component that uses it
function App() {
  return (
    <Layout>
      <Header />
      <SearchPanel />
      <Footer />
    </Layout>
  );
}

function SearchPanel() {
  const [searchQuery, setSearchQuery] = useState('');
  const results = useSearchResults(searchQuery);
  return (
    <div>
      <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />
      <ResultsList results={results} />
    </div>
  );
}

Note Colocating state reduces unnecessary re-renders and makes components easier to understand. Only lift state when it genuinely needs to be shared. Global state for local concerns is an anti-pattern that hurts performance and readability.