Fiber is React's reimplementation of the core reconciler (since React 16).
Main goals:
Work units are called "fibers" — linked list instead of stack recursion.
React 18 uses a lane-based priority system instead of simple numbers.
Higher priority lanes (e.g. user input) can interrupt lower priority work (e.g. data fetching, transitions).
Enables features like:
In Fiber (React 16+):
Legacy (pre-16): render → commit (all sync, blocking)
Tearing = inconsistent state view during concurrent render (one part sees old state, another sees new).
React 18 prevents it via:
| Scenario | React 17 | React 18 |
|---|---|---|
| Event handlers | batched | batched |
| Promises / setTimeout | not batched | batched |
| Native event handlers | not batched | batched |
| useEffect | not batched | batched |
React 18 batches almost everything by default → fewer renders.
"use server" (Next.js) or implicitly server-rendered"use client"Streaming = send HTML chunks as soon as they are ready instead of waiting for whole page.
<Suspense> boundaries define fallbacks and streaming chunks:
Big win for TTFB and perceived performance.
Automatic memoization compiler (opt-in, experimental in 2025–2026).
Does what developers manually do with:
Analyzes component code → memoizes props, state dependencies, and computations where safe.
Goal: remove most manual memoization boilerplate.
Strict mode double-invoking effects + render → exposes previously hidden side effects.
Also:
Use react-window or react-virtualized (or TanStack Virtual).
Key techniques:
Can reduce DOM nodes from 10,000 to ~30–50.
| Context + useReducer | Zustand | Jotai | |
|---|---|---|---|
| Boilerplate | medium | very low | low |
| Re-renders | all consumers on change | selective (subscribe) | atomic / fine-grained |
| DevTools | basic | good | good |
| Best for | small–medium theme/auth | most apps | very fine-grained state |
useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], old => [...old, newTodo]);
return { previous };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previous);
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
});
| Method | Purpose | Triggers refetch? | Use when |
|---|---|---|---|
| invalidateQueries | mark as stale | yes (if active) | after mutation, data changed on server |
| setQueryData | immediately update cache | no | optimistic updates, manual cache write |
@welldone-software/why-did-you-render)Hydration mismatch = server-rendered HTML differs from what client renders.
Common causes:
const About = React.lazy(() => import('./About'));
function App() {
return (
}>
} />
);
}
Creates separate chunk, loads on demand.
| useMemo | React.memo | |
|---|---|---|
| Targets | value inside component | whole component render |
| Compares | deps array | props (shallow) |
| Prevents | re-calculation | re-render |
| Typical for | expensive derived data | list items, pure UI components |
Use error.js file in Next.js App Router (convention-based).
For manual handling:
notFound() / redirect() for expected casesAutomatically wraps page / segment in Suspense with the loading UI.
Shows instantly while server component / data fetches are pending.
Improves perceived performance (shows skeleton / spinner early).
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// implementation
}
Or more advanced with generics + type inference from default value.
Components that share implicit state via context (Tabs, Accordion, Select, Dialog).
Modern style (2025):
<Tabs>
<Tabs.List>...</Tabs.List>
<Tabs.Content value="1">...</Tabs.Content>
</Tabs>
Implemented with custom context + provider.
Use jest.useFakeTimers() + act:
jest.useFakeTimers();
render(<MyComponent />);
act(() => jest.advanceTimersByTime(1000));
expect(screen.getByText('done')).toBeInTheDocument();
startTransition = low-level API to wrap any state update as transition.
useTransition = hook version that also gives isPending flag.
Use startTransition outside components (e.g. libraries).
Context still most common for truly global data.
Pages Router (legacy):
App Router (modern):
{ cache: 'no-store' } = SSR{ next: { revalidate: 3600 } } = ISRconst { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
Use IntersectionObserver on last item to call fetchNextPage.
<Image> component| revalidatePath | revalidateTag | |
|---|---|---|
| Granularity | path / route | cache tag |
| Use case | after form submit affecting page | multiple pages share same data |
| Impact | re-renders layout + page | only tagged fetches |
Common patterns 2025–2026:
Hybrid rendering: static shell + dynamic holes filled at request time.
Static parts cached, dynamic parts (Suspense boundaries) rendered per request.
Combines best of SSG + SSR + streaming.
Available in Next.js 14+ (stable in 15).
SPA (Create React App / Vite):
Next.js:
Replaces getStaticPaths — defines dynamic routes to prerender at build time.
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}
html / bodynext build + next start + analyticsStatic page regenerated in background after time interval while serving stale version.
App Router: fetch(..., { next: { revalidate: 3600 } })
Pages Router: getStaticProps with revalidate: 60
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: post.ogImage }
};
}
| Option | Behavior | Use case |
|---|---|---|
| force-dynamic | SSR every request | personalized, real-time data |
| force-static | SSG at build | static landing pages, docs |
| default (auto) | static if no dynamic APIs used | most pages |
import { NextResponse } from 'next/server';
export function middleware(request) {
const token = request.cookies.get('token')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = { matcher: ['/dashboard/:path*'] };
| Aspect | Zustand | Redux Toolkit |
|---|---|---|
| Boilerplate | minimal | moderate (but RTK Query helps) |
| Bundle size | very small (~1–2 KB) | larger |
| DevTools | good | excellent (time-travel) |
| Best for | most apps, simplicity | large teams, complex async, enterprise |
useActionState (React 19)width, height, aspect-ratio)<Image> with fill / sizesfont-display: swap or self-hostNext.js still wins for SEO + SSR.