The mental model shift
The single biggest mistake teams make when adopting the Next.js App Router is bringing their Pages Router mental model with them. In the Pages Router, everything is a client component by default. In the App Router, everything is a server component by default — and this changes how you should think about data fetching, state, and component design.
Server components: what they are good at
Server components render on the server and send HTML to the client. They can: - Access databases and file systems directly - Fetch data without client-side loading states - Import large server-only libraries without bloating the client bundle - Keep sensitive logic and environment variables server-side
For most content-heavy UI — navigation, article bodies, data tables, profile pages — server components are the right choice. They are faster for the user and simpler to reason about.
Client components: when you actually need them
Add 'use client' only when you need: - useState, useReducer, or other React state hooks - useEffect and browser APIs - Event handlers (onClick, onChange) - Third-party libraries that depend on browser globals
The most common mistake is adding 'use client' to a component because it "needs to be interactive", then having every child inherit the client boundary unnecessarily. Push 'use client' as low in the tree as possible — to the specific button, input, or interactive widget — not to the whole page.
Data fetching patterns
In the App Router, data fetching at the component level replaces centralised getServerSideProps. Each server component can fetch its own data directly, in parallel, without prop drilling.
The pattern for parallel data fetching that avoids waterfalls:
const [projectsPromise, testimonialsPromise] = await Promise.all([
fetchProjects(),
fetchTestimonials(),
]);For data that rarely changes (blog posts, docs, product listings), use React's cache() function combined with a revalidate option to serve the first request from the database and subsequent requests from the cache until the revalidation period expires.
Loading and error states
loading.tsx and error.tsx files in the App Router create automatic Suspense and ErrorBoundary wrappers for each route segment. Use them. They decouple your loading state UI from your component logic and prevent individual data-fetching failures from breaking the entire page.
The streaming advantage
One of the underappreciated benefits of the App Router is streaming. Because server components render progressively, the browser can start painting meaningful content before all the data has loaded. Combined with Suspense boundaries around slower data sources, users see something useful within milliseconds of the first byte arriving.
What to avoid
The patterns that create the most problems in production: - Context providers that wrap the entire app in a client boundary - Fetching data in client components when a server component would work - useEffect-based data fetching for data that exists at render time - Client components that import large libraries that should be server-only
The App Router rewards discipline. Get the server/client boundary right from the start, and you will have a fast, maintainable, SEO-friendly application that scales well. Get it wrong, and you end up with a client-heavy SPA that looks like a Pages Router app wearing a costume.