Mastering Composable State in Component UIs

Mastering Composable State in Component UIs

Composable state functions transformed how we write components: instead of classes and lifecycle methods, small functions declare the state and effects a component needs. The primitives are simple; the craft is in composing them.

The core primitives

1. Local state

The most basic primitive adds a piece of state and a setter to a function component:

const [count, setCount] = useLocalState(0);

Every call is independent, so a component can hold several small pieces of state instead of one monolithic object.

2. Effects

Effects run after render and declare their own dependencies:

useEffectOnce(() => {
  document.title = `Count: ${count}`;
}, [count]);

The dependency array is a contract: everything the effect reads must be listed, or it will run against stale values.

3. Shared context

Context passes data down the tree without threading props through every level:

const theme = useSharedContext(ThemeContext);

Reach for it when a value is genuinely ambient — theme, locale, session — not as a general store.

Building your own compositions

The real power arrives when you extract component logic into reusable functions:

function useWindowSize() {
  const [size, setSize] = useLocalState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffectOnce(() => {
    const onResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  return size;
}

The composition owns its cleanup, its subscription, and its state — the component just consumes a value.

Rules that keep you sane

  1. Only call state functions at the top level — never inside conditions or loops.
  2. Only call them from components or other compositions.
  3. Split unrelated concerns into separate effects.
  4. Always return a cleanup from effects that subscribe to anything.
  5. Treat the dependency array as part of the code review.

Common use cases

Form handling

function useForm(initialValues) {
  const [values, setValues] = useLocalState(initialValues);

  const handleChange = (event) => {
    const { name, value } = event.target;
    setValues((prev) => ({ ...prev, [name]: value }));
  };

  return { values, handleChange };
}

Data fetching

function useFetch(url) {
  const [data, setData] = useLocalState(null);
  const [loading, setLoading] = useLocalState(true);

  useEffectOnce(() => {
    let cancelled = false;
    fetch(url)
      .then((res) => res.json())
      .then((json) => {
        if (!cancelled) setData(json);
      })
      .finally(() => setLoading(false));
    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading };
}

Conclusion

Compositions are just functions, and everything you know about writing good functions applies: single purpose, explicit inputs, owned cleanup. Master the three primitives, then let your abstractions grow out of repeated patterns rather than speculation.