← Back to all posts

Building Modern Web Applications with React

Patterns and mental models for building maintainable React apps in 2026.


Component design starts with data

Before writing any JSX, ask: what data does this component need, and where does it come from? Components that reach far for their data — through context, global state, or prop-drilling — are harder to test and harder to move.

The best components are the ones you can drop into a different part of your app with minimal changes.

Co-locate state

State should live as close as possible to where it’s used. Global state is expensive — it makes every component that reads it a dependent. Use it only when the alternative is clearly worse.

// Instead of pulling from global state:
function PostList() {
  const posts = usePostsStore((s) => s.posts);
  return <ul>{posts.map(p => <PostItem key={p.id} post={p} />)}</ul>;
}

// Prefer fetching at the boundary:
function PostList() {
  const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
  return <ul>{posts?.map(p => <PostItem key={p.id} post={p} />)}</ul>;
}

Derive, don’t sync

Syncing state across multiple sources is a common source of bugs. If a value can be derived from existing state, derive it:

// Don't:
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);

// Do:
const [items, setItems] = useState([]);
const count = items.length;

Treat effects as escape hatches

useEffect is often overused. Before reaching for it, ask whether the work can happen during render, in an event handler, or in a data-fetching layer like React Query or SWR.

When you do use effects, keep them small and give them one clear job.