DEV Community

Cover image for React in 2026: Start From Scratch the Right Way (+ Cheat Sheet)
Parsa Jiravand
Parsa Jiravand

Posted on

React in 2026: Start From Scratch the Right Way (+ Cheat Sheet)

A complete guide to modern React — no useEffect abuse, no performance guessing, just clean architecture you'll actually use.


Why This Guide Exists

Most React tutorials are still teaching 2020 patterns. In 2026, the ecosystem has shifted dramatically: React 19 is stable, the compiler handles most memoization automatically, and useEffect should be a last resort — not your go-to for data fetching, derived state, or event responses.

This guide walks you from project setup to production-ready patterns, with a cheat sheet you can bookmark.


1. Scaffolding: What to Use in 2026

If you're building a full-stack or SSR app

npx create-next-app@latest my-app --typescript --tailwind --eslint --app
Enter fullscreen mode Exit fullscreen mode

Next.js 15+ with the App Router is the default for production apps. It gives you Server Components, streaming, and partial pre-rendering out of the box.

If you're building a pure client SPA

npm create vite@latest my-app -- --template react-ts
Enter fullscreen mode Exit fullscreen mode

Vite is the standard bundler for SPAs. Fast HMR, zero config, ESM-first.

If you're building something UI-heavy or a design system

npm create remix@latest
Enter fullscreen mode Exit fullscreen mode

Remix is excellent when you care deeply about progressive enhancement and form handling.


2. Folder Structure (Scalable from Day 1)

src/
├── app/               # Pages / routing (Next.js) or routes/
├── components/
│   ├── ui/            # Generic, reusable UI primitives
│   └── features/      # Feature-specific components
├── hooks/             # Custom hooks
├── lib/               # Utilities, API clients, helpers
├── stores/            # Global state (Zustand, Jotai, etc.)
├── types/             # Shared TypeScript types
└── styles/            # Global CSS / Tailwind config
Enter fullscreen mode Exit fullscreen mode

Rule: Components should not import from pages/ or app/. Data flows down. Side effects live in hooks.


3. React 19 Features You Should Actually Use

use() — The New Data Primitive

import { use, Suspense } from 'react';

async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// In a Server Component or with a cached promise:
function UserCard({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // unwraps the promise
  return <div>{user.name}</div>;
}

// Wrap in Suspense at the boundary
<Suspense fallback={<Skeleton />}>
  <UserCard userPromise={fetchUser('123')} />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

useOptimistic — Instant UI, Real Sync

import { useOptimistic } from 'react';

function LikeButton({ post }: { post: Post }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    post.likes,
    (currentLikes, increment: number) => currentLikes + increment
  );

  async function handleLike() {
    addOptimisticLike(1);       // instant update
    await likePost(post.id);    // real request in background
  }

  return (
    <button onClick={handleLike}>
      {optimisticLikes} likes
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

useActionState — Form State Without the Chaos

import { useActionState } from 'react';

async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title') as string;
  if (!title) return { error: 'Title is required' };
  await savePost({ title });
  return { success: true };
}

function PostForm() {
  const [state, action, isPending] = useActionState(createPost, null);

  return (
    <form action={action}>
      <input name="title" />
      {state?.error && <p>{state.error}</p>}
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Create Post'}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Stop Using useEffect for These Things

This is the most common mistake. Here's what to replace:

❌ Deriving state with useEffect

// Wrong
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Enter fullscreen mode Exit fullscreen mode
// Correct — just compute it
const fullName = `${firstName} ${lastName}`;
Enter fullscreen mode Exit fullscreen mode

❌ Fetching data with useEffect

// Wrong
useEffect(() => {
  fetch('/api/posts').then(r => r.json()).then(setPosts);
}, []);
Enter fullscreen mode Exit fullscreen mode
// Correct — use a data fetching library
import { useQuery } from '@tanstack/react-query';

function Posts() {
  const { data, isLoading } = useQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then(r => r.json())
  });
}
Enter fullscreen mode Exit fullscreen mode

Or even better — use Server Components in Next.js and fetch server-side:

// app/posts/page.tsx — This is a Server Component
export default async function PostsPage() {
  const posts = await getPosts(); // direct DB/API call
  return <PostList posts={posts} />;
}
Enter fullscreen mode Exit fullscreen mode

❌ Responding to events with useEffect

// Wrong
useEffect(() => {
  if (submitted) {
    sendAnalytics();
    resetForm();
  }
}, [submitted]);
Enter fullscreen mode Exit fullscreen mode
// Correct — handle it in the event handler
function handleSubmit() {
  sendAnalytics();
  resetForm();
  setSubmitted(true);
}
Enter fullscreen mode Exit fullscreen mode

✅ When useEffect IS appropriate

  • Setting up a third-party library that imperatively mutates the DOM
  • WebSocket or EventSource subscriptions (with cleanup)
  • Syncing to localStorage or sessionStorage
  • Firing analytics on route change (sparingly)

5. State Management in 2026

Need Tool
Local UI state useState / useReducer
Server data + caching TanStack Query
Global client state Zustand or Jotai
Forms React Hook Form + Zod
URL state nuqs (Next.js) or useSearchParams
Server state (Next.js) Server Actions + revalidatePath

Zustand setup (minimal):

import { create } from 'zustand';

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
}

export const useCartStore = create<CartStore>((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) => set((state) => ({
    items: state.items.filter(i => i.id !== id)
  })),
}));
Enter fullscreen mode Exit fullscreen mode

6. Performance in 2026 (The React Compiler Changes Everything)

React 19 ships with the React Compiler (previously "React Forget"). It automatically memoizes components and values — meaning useMemo, useCallback, and React.memo are rarely needed manually.

What the compiler handles automatically

  • Skipping re-renders when props haven't changed
  • Stabilizing callback references
  • Memoizing expensive computations

What you still need to think about

Code splitting — always split by route:

import { lazy, Suspense } from 'react';

const HeavyDashboard = lazy(() => import('./HeavyDashboard'));

function App() {
  return (
    <Suspense fallback={<LoadingScreen />}>
      <HeavyDashboard />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

Virtualize long lists — use TanStack Virtual:

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: string[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map(item => (
          <div
            key={item.key}
            style={{ position: 'absolute', top: item.start, width: '100%' }}
          >
            {items[item.index]}
          </div>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Images — always use Next.js <Image> or a CDN:

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // for above-the-fold images
  placeholder="blur"
  blurDataURL={blurUrl}
/>
Enter fullscreen mode Exit fullscreen mode

7. TypeScript Patterns Worth Knowing

Component props with variants

type ButtonProps = {
  variant: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

function Button({ variant, size = 'md', isLoading, children, ...rest }: ButtonProps) {
  return (
    <button
      disabled={isLoading}
      className={cn(buttonVariants({ variant, size }))}
      {...rest}
    >
      {isLoading ? <Spinner /> : children}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Discriminated unions for component state

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

function DataView({ state }: { state: AsyncState<User[]> }) {
  if (state.status === 'loading') return <Skeleton />;
  if (state.status === 'error') return <Error message={state.error} />;
  if (state.status === 'success') return <UserList users={state.data} />;
  return null;
}
Enter fullscreen mode Exit fullscreen mode

8. The 2026 Cheat Sheet

Hooks reference

Hook Use it for
useState Simple local state
useReducer Complex local state with multiple sub-values
useRef DOM refs, mutable values that don't trigger re-render
useContext Consuming context (theme, auth, locale)
use() Unwrapping promises and context in any component
useOptimistic Instant UI feedback while async operation runs
useActionState Managing form state tied to a server action
useTransition Marking non-urgent state updates
useDeferredValue Deferring re-rendering of slow sub-trees
useEffect LAST RESORT — external system sync only

When to reach for what

Scenario Solution
Data from server TanStack Query or Server Components
Form handling useActionState + Server Actions, or React Hook Form
Global state Zustand (simple) or Jotai (atomic)
URL as state nuqs
List of 500+ items TanStack Virtual
Async feedback useOptimistic
Heavy component lazy() + Suspense
Derived value Compute inline, no state
Animation Framer Motion or CSS transitions

Anti-patterns to avoid

Anti-pattern Replacement
useEffect for data fetching TanStack Query / Server Components
useEffect for derived state Compute inline
useEffect for event responses Event handler
Prop drilling 3+ levels Zustand / context
React.memo everywhere Let the compiler handle it
Manual useMemo/useCallback Let the compiler handle it
any in TypeScript Discriminated unions, generics
Giant components (500+ lines) Split by concern

9. Recommended Stack (2026)

Framework:     Next.js 15 (App Router)
Language:      TypeScript (strict mode)
Styling:       Tailwind CSS v4 + shadcn/ui
Data fetching: TanStack Query v5
Global state:  Zustand
Forms:         React Hook Form + Zod
Testing:       Vitest + Testing Library + Playwright
Linting:       ESLint + Prettier + TypeScript strict
Deployment:    Vercel / Cloudflare Workers
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

The shift from "React as a UI library" to "React as an application framework" is complete in 2026. The mental model has changed:

  • Server first. Render as much as you can on the server.
  • Data co-located with components. No more global fetch orchestration.
  • Less manual optimization. The compiler does the heavy lifting.
  • useEffect is a code smell. If you're reaching for it, ask why first.

Build from these foundations and you'll spend less time debugging re-renders and more time shipping features.


Found this useful? Drop a like and follow — I write about React, TypeScript, and modern web architecture.

Tags: #react #javascript #typescript #webdev #frontend

Top comments (0)