React 19 Skill
This skill provides comprehensive knowledge and patterns for working with React 19 effectively in modern applications.
When to Use This Skill
Use this skill when:
-
Building React applications with React 19 features
-
Working with React hooks and component patterns
-
Implementing server components and server functions
-
Using concurrent features and transitions
-
Optimizing React application performance
-
Troubleshooting React-specific issues
-
Working with React DOM APIs and client/server rendering
-
Using React Compiler features
Core Concepts
React 19 Overview
React 19 introduces significant improvements:
-
Server Components - Components that render on the server
-
Server Functions - Functions that run on the server from client code
-
Concurrent Features - Better performance with concurrent rendering
-
React Compiler - Automatic memoization and optimization
-
Form Actions - Built-in form handling with useActionState
-
Improved Hooks - New hooks like useOptimistic, useActionState
-
Better Hydration - Improved SSR and hydration performance
Component Fundamentals
Use functional components with hooks:
// Functional component with props interface interface ButtonProps { label: string onClick: () => void variant?: 'primary' | 'secondary' }
const Button = ({ label, onClick, variant = 'primary' }: ButtonProps) => {
return (
<button
onClick={onClick}
className={btn btn-${variant}}
>
{label}
</button>
)
}
Key Principles:
-
Use functional components over class components
-
Define prop interfaces in TypeScript
-
Use destructuring for props
-
Provide default values for optional props
-
Keep components focused and composable
React Hooks Reference
State Hooks
useState
Manage local component state:
const [count, setCount] = useState<number>(0) const [user, setUser] = useState<User | null>(null)
// Named return variables pattern const handleIncrement = () => { setCount(prev => prev + 1) // Functional update }
// Update object state immutably setUser(prev => prev ? { ...prev, name: 'New Name' } : null)
useReducer
Manage complex state with reducer pattern:
type State = { count: number; status: 'idle' | 'loading' } type Action = | { type: 'increment' } | { type: 'decrement' } | { type: 'setStatus'; status: State['status'] }
const reducer = (state: State, action: Action): State => { switch (action.type) { case 'increment': return { ...state, count: state.count + 1 } case 'decrement': return { ...state, count: state.count - 1 } case 'setStatus': return { ...state, status: action.status } default: return state } }
const [state, dispatch] = useReducer(reducer, { count: 0, status: 'idle' })
useActionState
Handle form actions with pending states (React 19):
const [state, formAction, isPending] = useActionState( async (previousState: FormState, formData: FormData) => { const name = formData.get('name') as string
// Server action or async operation
const result = await saveUser({ name })
return { success: true, data: result }
}, { success: false, data: null } )
return ( <form action={formAction}> <input name="name" /> <button disabled={isPending}> {isPending ? 'Saving...' : 'Save'} </button> </form> )
Effect Hooks
useEffect
Run side effects after render:
// Named return variables preferred useEffect(() => { const controller = new AbortController()
const fetchData = async () => { const response = await fetch('/api/data', { signal: controller.signal }) const data = await response.json() setData(data) }
fetchData()
// Cleanup function return () => { controller.abort() } }, [dependencies]) // Dependencies array
Key Points:
-
Always return cleanup function for subscriptions
-
Use dependency array correctly to avoid infinite loops
-
Don't forget to handle race conditions with AbortController
-
Effects run after paint, not during render
useLayoutEffect
Run effects synchronously after DOM mutations but before paint:
useLayoutEffect(() => { // Measure DOM nodes const height = ref.current?.getBoundingClientRect().height setHeight(height) }, [])
Use when you need to:
-
Measure DOM layout
-
Synchronously re-render before browser paints
-
Prevent visual flicker
useInsertionEffect
Insert styles before any DOM reads (for CSS-in-JS libraries):
useInsertionEffect(() => { const style = document.createElement('style') style.textContent = '.my-class { color: red; }' document.head.appendChild(style)
return () => { document.head.removeChild(style) } }, [])
Performance Hooks
useMemo
Memoize expensive calculations:
const expensiveValue = useMemo(() => { return computeExpensiveValue(a, b) }, [a, b])
When to use:
-
Expensive calculations that would slow down renders
-
Creating stable object references for dependency arrays
-
Optimizing child component re-renders
When NOT to use:
-
Simple calculations (overhead not worth it)
-
Values that change frequently
useCallback
Memoize callback functions:
const handleClick = useCallback(() => { console.log('Clicked', value) }, [value])
// Pass to child that uses memo <ChildComponent onClick={handleClick} />
Use when:
-
Passing callbacks to optimized child components
-
Function is a dependency in another hook
-
Function is used in effect cleanup
Ref Hooks
useRef
Store mutable values that don't trigger re-renders:
// DOM reference const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => { inputRef.current?.focus() }, [])
// Mutable value storage const countRef = useRef<number>(0) countRef.current += 1 // Doesn't trigger re-render
useImperativeHandle
Customize ref handle for parent components:
interface InputHandle { focus: () => void clear: () => void }
const CustomInput = forwardRef<InputHandle, InputProps>((props, ref) => { const inputRef = useRef<HTMLInputElement>(null)
useImperativeHandle(ref, () => ({ focus: () => { inputRef.current?.focus() }, clear: () => { if (inputRef.current) { inputRef.current.value = '' } } }))
return <input ref={inputRef} {...props} /> })
Context Hooks
useContext
Access context values:
// Create context interface ThemeContext { theme: 'light' | 'dark' toggleTheme: () => void }
const ThemeContext = createContext<ThemeContext | null>(null)
// Provider const ThemeProvider = ({ children }: { children: React.ReactNode }) => { const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = useCallback(() => { setTheme(prev => prev === 'light' ? 'dark' : 'light') }, [])
return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ) }
// Consumer const ThemedButton = () => { const context = useContext(ThemeContext) if (!context) throw new Error('useTheme must be used within ThemeProvider')
const { theme, toggleTheme } = context
return ( <button onClick={toggleTheme}> Current theme: {theme} </button> ) }
Transition Hooks
useTransition
Mark state updates as non-urgent:
const [isPending, startTransition] = useTransition()
const handleTabChange = (newTab: string) => { startTransition(() => { setTab(newTab) // Non-urgent update }) }
return ( <> <button onClick={() => handleTabChange('profile')}> Profile </button> {isPending && <Spinner />} <TabContent tab={tab} /> </> )
Use for:
-
Marking expensive updates as non-urgent
-
Keeping UI responsive during state transitions
-
Preventing loading states for quick updates
useDeferredValue
Defer re-rendering for non-urgent updates:
const [query, setQuery] = useState('') const deferredQuery = useDeferredValue(query)
// Use deferred value for expensive rendering const results = useMemo(() => { return searchResults(deferredQuery) }, [deferredQuery])
return ( <> <input value={query} onChange={e => setQuery(e.target.value)} /> <Results data={results} /> </> )
Optimistic Updates
useOptimistic
Show optimistic state while async operation completes (React 19):
const [optimisticMessages, addOptimisticMessage] = useOptimistic( messages, (state, newMessage: string) => [ ...state, { id: 'temp', text: newMessage, pending: true } ] )
const handleSend = async (formData: FormData) => { const message = formData.get('message') as string
// Show optimistic update immediately addOptimisticMessage(message)
// Send to server await sendMessage(message) }
return ( <> {optimisticMessages.map(msg => ( <div key={msg.id} className={msg.pending ? 'opacity-50' : ''}> {msg.text} </div> ))} <form action={handleSend}> <input name="message" /> <button>Send</button> </form> </> )
Other Hooks
useId
Generate unique IDs for accessibility:
const id = useId()
return ( <> <label htmlFor={id}>Name:</label> <input id={id} type="text" /> </> )
useSyncExternalStore
Subscribe to external stores:
const subscribe = (callback: () => void) => { store.subscribe(callback) return () => store.unsubscribe(callback) }
const getSnapshot = () => store.getState() const getServerSnapshot = () => store.getInitialState()
const state = useSyncExternalStore( subscribe, getSnapshot, getServerSnapshot )
useDebugValue
Display custom label in React DevTools:
const useCustomHook = (value: string) => {
useDebugValue(value ? Active: ${value} : 'Inactive')
return value
}
React Components
Fragment
Group elements without extra DOM nodes:
// Short syntax <> <ChildA /> <ChildB /> </>
// Full syntax (when you need key prop) <Fragment key={item.id}> <dt>{item.term}</dt> <dd>{item.description}</dd> </Fragment>
Suspense
Show fallback while loading:
<Suspense fallback={<Loading />}> <AsyncComponent /> </Suspense>
// With error boundary <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Loading />}> <AsyncComponent /> </Suspense> </ErrorBoundary>
StrictMode
Enable additional checks in development:
<StrictMode> <App /> </StrictMode>
StrictMode checks:
-
Warns about deprecated APIs
-
Detects unexpected side effects
-
Highlights potential problems
-
Double-invokes functions to catch bugs
Profiler
Measure rendering performance:
<Profiler id="App" onRender={onRender}> <App /> </Profiler>
const onRender = (
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) => {
console.log(${id} took ${actualDuration}ms)
}
React APIs
memo
Prevent unnecessary re-renders:
const ExpensiveComponent = memo(({ data }: Props) => { return <div>{data}</div> }, (prevProps, nextProps) => { // Return true if props are equal (skip render) return prevProps.data === nextProps.data })
lazy
Code-split components:
const Dashboard = lazy(() => import('./Dashboard'))
<Suspense fallback={<Loading />}> <Dashboard /> </Suspense>
startTransition
Mark updates as transitions imperatively:
startTransition(() => { setTab('profile') })
cache (React Server Components)
Cache function results per request:
const getUser = cache(async (id: string) => { return await db.user.findUnique({ where: { id } }) })
use (React 19)
Read context or promises in render:
// Read context const theme = use(ThemeContext)
// Read promise (must be wrapped in Suspense) const data = use(fetchDataPromise)
Server Components & Server Functions
Server Components
Components that run only on the server:
// app/page.tsx (Server Component by default) const Page = async () => { // Can fetch data directly const posts = await db.post.findMany()
return ( <div> {posts.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> ) }
export default Page
Benefits:
-
Direct database access
-
Zero bundle size for server-only code
-
Automatic code splitting
-
Better performance
Server Functions
Functions that run on server, callable from client:
'use server'
export async function createPost(formData: FormData) { const title = formData.get('title') as string const content = formData.get('content') as string
const post = await db.post.create({ data: { title, content } })
revalidatePath('/posts') return post }
Usage from client:
'use client'
import { createPost } from './actions'
const PostForm = () => { const [state, formAction] = useActionState(createPost, null)
return ( <form action={formAction}> <input name="title" /> <textarea name="content" /> <button>Create</button> </form> ) }
Directives
'use client'
Mark file as client component:
'use client'
import { useState } from 'react'
// This component runs on client export const Counter = () => { const [count, setCount] = useState(0) return <button onClick={() => setCount(c => c + 1)}>{count}</button> }
'use server'
Mark functions as server functions:
'use server'
export async function updateUser(userId: string, data: UserData) { return await db.user.update({ where: { id: userId }, data }) }
React DOM
Client APIs
createRoot
Create root for client rendering (React 19):
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root')!) root.render(<App />)
// Update root root.render(<App newProp="value" />)
// Unmount root.unmount()
hydrateRoot
Hydrate server-rendered HTML:
import { hydrateRoot } from 'react-dom/client'
hydrateRoot(document.getElementById('root')!, <App />)
Component APIs
createPortal
Render children outside parent DOM hierarchy:
import { createPortal } from 'react-dom'
const Modal = ({ children }: { children: React.ReactNode }) => { return createPortal( <div className="modal">{children}</div>, document.body ) }
flushSync
Force synchronous update:
import { flushSync } from 'react-dom'
flushSync(() => { setCount(1) }) // DOM is updated synchronously
Form Components
with actions
const handleSubmit = async (formData: FormData) => { 'use server' const email = formData.get('email') await saveEmail(email) }
<form action={handleSubmit}> <input name="email" type="email" /> <button>Subscribe</button> </form>
useFormStatus
import { useFormStatus } from 'react-dom'
const SubmitButton = () => { const { pending } = useFormStatus()
return ( <button disabled={pending}> {pending ? 'Submitting...' : 'Submit'} </button> ) }
React Compiler
Configuration
Configure React Compiler in babel or bundler config:
// babel.config.js module.exports = { plugins: [ ['react-compiler', { compilationMode: 'annotation', // or 'all' panicThreshold: 'all_errors', }] ] }
Directives
"use memo"
Force memoization of component:
'use memo'
const ExpensiveComponent = ({ data }: Props) => { const processed = expensiveComputation(data) return <div>{processed}</div> }
"use no memo"
Prevent automatic memoization:
'use no memo'
const SimpleComponent = ({ text }: Props) => { return <div>{text}</div> }
Best Practices
Component Design
-
Keep components focused - Single responsibility principle
-
Prefer composition - Build complex UIs from simple components
-
Extract custom hooks - Reusable logic in hooks
-
Named return variables - Use named returns in functions
-
Type everything - Proper TypeScript interfaces for all props
Performance
-
Use React.memo sparingly - Only for expensive components
-
Optimize context - Split contexts to avoid unnecessary re-renders
-
Lazy load routes - Code-split at route boundaries
-
Use transitions - Mark non-urgent updates with useTransition
-
Virtualize lists - Use libraries like react-window for long lists
State Management
-
Local state first - useState for component-specific state
-
Lift state up - Only when multiple components need it
-
Use reducers for complex state - useReducer for complex logic
-
Context for global state - Theme, auth, etc.
-
External stores - TanStack Query, Zustand for complex apps
Error Handling
-
Error boundaries - Catch rendering errors
-
Guard clauses - Early returns for invalid states
-
Null checks - Always check for null/undefined
-
Try-catch in effects - Handle async errors
-
User-friendly errors - Show helpful error messages
Testing Considerations
-
Testable components - Pure, predictable components
-
Test user behavior - Not implementation details
-
Mock external dependencies - APIs, context, etc.
-
Test error states - Verify error handling works
-
Accessibility tests - Test keyboard navigation, screen readers
Common Patterns
Compound Components
interface TabsProps { children: React.ReactNode defaultValue: string }
const TabsContext = createContext<{ value: string setValue: (v: string) => void } | null>(null)
const Tabs = ({ children, defaultValue }: TabsProps) => { const [value, setValue] = useState(defaultValue)
return ( <TabsContext.Provider value={{ value, setValue }}> {children} </TabsContext.Provider> ) }
const TabsList = ({ children }: { children: React.ReactNode }) => ( <div role="tablist">{children}</div> )
const TabsTrigger = ({ value, children }: { value: string, children: React.ReactNode }) => { const context = useContext(TabsContext) if (!context) throw new Error('TabsTrigger must be used within Tabs')
return ( <button role="tab" aria-selected={context.value === value} onClick={() => context.setValue(value)} > {children} </button> ) }
const TabsContent = ({ value, children }: { value: string, children: React.ReactNode }) => { const context = useContext(TabsContext) if (!context) throw new Error('TabsContent must be used within Tabs')
if (context.value !== value) return null
return <div role="tabpanel">{children}</div> }
// Usage <Tabs defaultValue="profile"> <TabsList> <TabsTrigger value="profile">Profile</TabsTrigger> <TabsTrigger value="settings">Settings</TabsTrigger> </TabsList> <TabsContent value="profile">Profile content</TabsContent> <TabsContent value="settings">Settings content</TabsContent> </Tabs>
Render Props
interface DataFetcherProps<T> { url: string children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode }
const DataFetcher = <T,>({ url, children }: DataFetcherProps<T>) => { const [data, setData] = useState<T | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<Error | null>(null)
useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false)) }, [url])
return <>{children(data, loading, error)}</> }
// Usage <DataFetcher<User> url="/api/user"> {(user, loading, error) => { if (loading) return <Spinner /> if (error) return <Error error={error} /> if (!user) return null return <UserProfile user={user} /> }} </DataFetcher>
Custom Hooks Pattern
const useLocalStorage = <T,>(key: string, initialValue: T) => { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key) return item ? JSON.parse(item) : initialValue } catch (error) { console.error(error) return initialValue } })
const setValue = useCallback((value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value setStoredValue(valueToStore) window.localStorage.setItem(key, JSON.stringify(valueToStore)) } catch (error) { console.error(error) } }, [key, storedValue])
return [storedValue, setValue] as const }
Troubleshooting
Common Issues
Infinite Loops
-
Check useEffect dependencies
-
Ensure state updates don't trigger themselves
-
Use functional setState updates
Stale Closures
-
Add all used variables to dependency arrays
-
Use useCallback for functions in dependencies
-
Consider using refs for values that shouldn't trigger re-renders
Performance Issues
-
Use React DevTools Profiler
-
Check for unnecessary re-renders
-
Optimize with memo, useMemo, useCallback
-
Consider code splitting
Hydration Mismatches
-
Ensure server and client render same HTML
-
Avoid using Date.now() or random values during render
-
Use useEffect for browser-only code
-
Check for conditional rendering based on browser APIs
References
-
React Documentation: https://react.dev
-
React API Reference: https://react.dev/reference/react
-
React DOM Reference: https://react.dev/reference/react-dom
-
React Compiler: https://react.dev/reference/react-compiler
-
Rules of React: https://react.dev/reference/rules
Related Skills
-
typescript - TypeScript patterns and types for React
-
ndk - Nostr integration with React hooks
-
skill-creator - Creating reusable component libraries