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:
typescript
1// Functional component with props interface
2interface ButtonProps {
3 label: string
4 onClick: () => void
5 variant?: 'primary' | 'secondary'
6}
7
8const Button = ({ label, onClick, variant = 'primary' }: ButtonProps) => {
9 return (
10 <button
11 onClick={onClick}
12 className={`btn btn-${variant}`}
13 >
14 {label}
15 </button>
16 )
17}
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:
typescript
1const [count, setCount] = useState<number>(0)
2const [user, setUser] = useState<User | null>(null)
3
4// Named return variables pattern
5const handleIncrement = () => {
6 setCount((prev) => prev + 1) // Functional update
7}
8
9// Update object state immutably
10setUser((prev) => (prev ? { ...prev, name: 'New Name' } : null))
useReducer
Manage complex state with reducer pattern:
typescript
1type State = { count: number; status: 'idle' | 'loading' }
2type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'setStatus'; status: State['status'] }
3
4const reducer = (state: State, action: Action): State => {
5 switch (action.type) {
6 case 'increment':
7 return { ...state, count: state.count + 1 }
8 case 'decrement':
9 return { ...state, count: state.count - 1 }
10 case 'setStatus':
11 return { ...state, status: action.status }
12 default:
13 return state
14 }
15}
16
17const [state, dispatch] = useReducer(reducer, { count: 0, status: 'idle' })
useActionState
Handle form actions with pending states (React 19):
typescript
1const [state, formAction, isPending] = useActionState(
2 async (previousState: FormState, formData: FormData) => {
3 const name = formData.get('name') as string
4
5 // Server action or async operation
6 const result = await saveUser({ name })
7
8 return { success: true, data: result }
9 },
10 { success: false, data: null }
11)
12
13return (
14 <form action={formAction}>
15 <input name="name" />
16 <button disabled={isPending}>
17 {isPending ? 'Saving...' : 'Save'}
18 </button>
19 </form>
20)
Effect Hooks
useEffect
Run side effects after render:
typescript
1// Named return variables preferred
2useEffect(() => {
3 const controller = new AbortController()
4
5 const fetchData = async () => {
6 const response = await fetch('/api/data', {
7 signal: controller.signal,
8 })
9 const data = await response.json()
10 setData(data)
11 }
12
13 fetchData()
14
15 // Cleanup function
16 return () => {
17 controller.abort()
18 }
19}, [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:
typescript
1useLayoutEffect(() => {
2 // Measure DOM nodes
3 const height = ref.current?.getBoundingClientRect().height
4 setHeight(height)
5}, [])
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):
typescript
1useInsertionEffect(() => {
2 const style = document.createElement('style')
3 style.textContent = '.my-class { color: red; }'
4 document.head.appendChild(style)
5
6 return () => {
7 document.head.removeChild(style)
8 }
9}, [])
useMemo
Memoize expensive calculations:
typescript
1const expensiveValue = useMemo(() => {
2 return computeExpensiveValue(a, b)
3}, [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:
typescript
1const handleClick = useCallback(() => {
2 console.log('Clicked', value)
3}, [value])
4
5// Pass to child that uses memo
6<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:
typescript
1// DOM reference
2const inputRef = useRef<HTMLInputElement>(null)
3
4useEffect(() => {
5 inputRef.current?.focus()
6}, [])
7
8// Mutable value storage
9const countRef = useRef<number>(0)
10countRef.current += 1 // Doesn't trigger re-render
useImperativeHandle
Customize ref handle for parent components:
typescript
1interface InputHandle {
2 focus: () => void
3 clear: () => void
4}
5
6const CustomInput = forwardRef<InputHandle, InputProps>((props, ref) => {
7 const inputRef = useRef<HTMLInputElement>(null)
8
9 useImperativeHandle(ref, () => ({
10 focus: () => {
11 inputRef.current?.focus()
12 },
13 clear: () => {
14 if (inputRef.current) {
15 inputRef.current.value = ''
16 }
17 }
18 }))
19
20 return <input ref={inputRef} {...props} />
21})
Context Hooks
useContext
Access context values:
typescript
1// Create context
2interface ThemeContext {
3 theme: 'light' | 'dark'
4 toggleTheme: () => void
5}
6
7const ThemeContext = createContext<ThemeContext | null>(null)
8
9// Provider
10const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
11 const [theme, setTheme] = useState<'light' | 'dark'>('light')
12
13 const toggleTheme = useCallback(() => {
14 setTheme(prev => prev === 'light' ? 'dark' : 'light')
15 }, [])
16
17 return (
18 <ThemeContext.Provider value={{ theme, toggleTheme }}>
19 {children}
20 </ThemeContext.Provider>
21 )
22}
23
24// Consumer
25const ThemedButton = () => {
26 const context = useContext(ThemeContext)
27 if (!context) throw new Error('useTheme must be used within ThemeProvider')
28
29 const { theme, toggleTheme } = context
30
31 return (
32 <button onClick={toggleTheme}>
33 Current theme: {theme}
34 </button>
35 )
36}
Transition Hooks
useTransition
Mark state updates as non-urgent:
typescript
1const [isPending, startTransition] = useTransition()
2
3const handleTabChange = (newTab: string) => {
4 startTransition(() => {
5 setTab(newTab) // Non-urgent update
6 })
7}
8
9return (
10 <>
11 <button onClick={() => handleTabChange('profile')}>
12 Profile
13 </button>
14 {isPending && <Spinner />}
15 <TabContent tab={tab} />
16 </>
17)
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:
typescript
1const [query, setQuery] = useState('')
2const deferredQuery = useDeferredValue(query)
3
4// Use deferred value for expensive rendering
5const results = useMemo(() => {
6 return searchResults(deferredQuery)
7}, [deferredQuery])
8
9return (
10 <>
11 <input value={query} onChange={e => setQuery(e.target.value)} />
12 <Results data={results} />
13 </>
14)
Optimistic Updates
useOptimistic
Show optimistic state while async operation completes (React 19):
typescript
1const [optimisticMessages, addOptimisticMessage] = useOptimistic(
2 messages,
3 (state, newMessage: string) => [
4 ...state,
5 { id: 'temp', text: newMessage, pending: true }
6 ]
7)
8
9const handleSend = async (formData: FormData) => {
10 const message = formData.get('message') as string
11
12 // Show optimistic update immediately
13 addOptimisticMessage(message)
14
15 // Send to server
16 await sendMessage(message)
17}
18
19return (
20 <>
21 {optimisticMessages.map(msg => (
22 <div key={msg.id} className={msg.pending ? 'opacity-50' : ''}>
23 {msg.text}
24 </div>
25 ))}
26 <form action={handleSend}>
27 <input name="message" />
28 <button>Send</button>
29 </form>
30 </>
31)
Other Hooks
useId
Generate unique IDs for accessibility:
typescript
1const id = useId()
2
3return (
4 <>
5 <label htmlFor={id}>Name:</label>
6 <input id={id} type="text" />
7 </>
8)
useSyncExternalStore
Subscribe to external stores:
typescript
1const subscribe = (callback: () => void) => {
2 store.subscribe(callback)
3 return () => store.unsubscribe(callback)
4}
5
6const getSnapshot = () => store.getState()
7const getServerSnapshot = () => store.getInitialState()
8
9const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
useDebugValue
Display custom label in React DevTools:
typescript
1const useCustomHook = (value: string) => {
2 useDebugValue(value ? `Active: ${value}` : 'Inactive')
3 return value
4}
React Components
Fragment
Group elements without extra DOM nodes:
typescript
1// Short syntax
2<>
3 <ChildA />
4 <ChildB />
5</>
6
7// Full syntax (when you need key prop)
8<Fragment key={item.id}>
9 <dt>{item.term}</dt>
10 <dd>{item.description}</dd>
11</Fragment>
Suspense
Show fallback while loading:
typescript
1<Suspense fallback={<Loading />}>
2 <AsyncComponent />
3</Suspense>
4
5// With error boundary
6<ErrorBoundary fallback={<Error />}>
7 <Suspense fallback={<Loading />}>
8 <AsyncComponent />
9 </Suspense>
10</ErrorBoundary>
StrictMode
Enable additional checks in development:
typescript
1<StrictMode>
2 <App />
3</StrictMode>
StrictMode checks:
- Warns about deprecated APIs
- Detects unexpected side effects
- Highlights potential problems
- Double-invokes functions to catch bugs
Profiler
Measure rendering performance:
typescript
1<Profiler id="App" onRender={onRender}>
2 <App />
3</Profiler>
4
5const onRender = (
6 id: string,
7 phase: 'mount' | 'update',
8 actualDuration: number,
9 baseDuration: number,
10 startTime: number,
11 commitTime: number
12) => {
13 console.log(`${id} took ${actualDuration}ms`)
14}
React APIs
memo
Prevent unnecessary re-renders:
typescript
1const ExpensiveComponent = memo(({ data }: Props) => {
2 return <div>{data}</div>
3}, (prevProps, nextProps) => {
4 // Return true if props are equal (skip render)
5 return prevProps.data === nextProps.data
6})
lazy
Code-split components:
typescript
1const Dashboard = lazy(() => import('./Dashboard'))
2
3<Suspense fallback={<Loading />}>
4 <Dashboard />
5</Suspense>
startTransition
Mark updates as transitions imperatively:
typescript
1startTransition(() => {
2 setTab('profile')
3})
cache (React Server Components)
Cache function results per request:
typescript
1const getUser = cache(async (id: string) => {
2 return await db.user.findUnique({ where: { id } })
3})
use (React 19)
Read context or promises in render:
typescript
1// Read context
2const theme = use(ThemeContext)
3
4// Read promise (must be wrapped in Suspense)
5const data = use(fetchDataPromise)
Server Components & Server Functions
Server Components
Components that run only on the server:
typescript
1// app/page.tsx (Server Component by default)
2const Page = async () => {
3 // Can fetch data directly
4 const posts = await db.post.findMany()
5
6 return (
7 <div>
8 {posts.map(post => (
9 <PostCard key={post.id} post={post} />
10 ))}
11 </div>
12 )
13}
14
15export 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:
typescript
1'use server'
2
3export async function createPost(formData: FormData) {
4 const title = formData.get('title') as string
5 const content = formData.get('content') as string
6
7 const post = await db.post.create({
8 data: { title, content },
9 })
10
11 revalidatePath('/posts')
12 return post
13}
Usage from client:
typescript
1'use client'
2
3import { createPost } from './actions'
4
5const PostForm = () => {
6 const [state, formAction] = useActionState(createPost, null)
7
8 return (
9 <form action={formAction}>
10 <input name="title" />
11 <textarea name="content" />
12 <button>Create</button>
13 </form>
14 )
15}
Directives
'use client'
Mark file as client component:
typescript
1'use client'
2
3import { useState } from 'react'
4
5// This component runs on client
6export const Counter = () => {
7 const [count, setCount] = useState(0)
8 return <button onClick={() => setCount(c => c + 1)}>{count}</button>
9}
'use server'
Mark functions as server functions:
typescript
1'use server'
2
3export async function updateUser(userId: string, data: UserData) {
4 return await db.user.update({ where: { id: userId }, data })
5}
React DOM
Client APIs
createRoot
Create root for client rendering (React 19):
typescript
1import { createRoot } from 'react-dom/client'
2
3const root = createRoot(document.getElementById('root')!)
4root.render(<App />)
5
6// Update root
7root.render(<App newProp="value" />)
8
9// Unmount
10root.unmount()
hydrateRoot
Hydrate server-rendered HTML:
typescript
1import { hydrateRoot } from 'react-dom/client'
2
3hydrateRoot(document.getElementById('root')!, <App />)
Component APIs
createPortal
Render children outside parent DOM hierarchy:
typescript
1import { createPortal } from 'react-dom'
2
3const Modal = ({ children }: { children: React.ReactNode }) => {
4 return createPortal(
5 <div className="modal">{children}</div>,
6 document.body
7 )
8}
flushSync
Force synchronous update:
typescript
1import { flushSync } from 'react-dom'
2
3flushSync(() => {
4 setCount(1)
5})
6// DOM is updated synchronously
<form> with actions
typescript
1const handleSubmit = async (formData: FormData) => {
2 'use server'
3 const email = formData.get('email')
4 await saveEmail(email)
5}
6
7<form action={handleSubmit}>
8 <input name="email" type="email" />
9 <button>Subscribe</button>
10</form>
typescript
1import { useFormStatus } from 'react-dom'
2
3const SubmitButton = () => {
4 const { pending } = useFormStatus()
5
6 return (
7 <button disabled={pending}>
8 {pending ? 'Submitting...' : 'Submit'}
9 </button>
10 )
11}
React Compiler
Configuration
Configure React Compiler in babel or bundler config:
javascript
1// babel.config.js
2module.exports = {
3 plugins: [
4 [
5 'react-compiler',
6 {
7 compilationMode: 'annotation', // or 'all'
8 panicThreshold: 'all_errors',
9 },
10 ],
11 ],
12}
Directives
"use memo"
Force memoization of component:
typescript
1'use memo'
2
3const ExpensiveComponent = ({ data }: Props) => {
4 const processed = expensiveComputation(data)
5 return <div>{processed}</div>
6}
"use no memo"
Prevent automatic memoization:
typescript
1'use no memo'
2
3const SimpleComponent = ({ text }: Props) => {
4 return <div>{text}</div>
5}
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
- 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
typescript
1interface TabsProps {
2 children: React.ReactNode
3 defaultValue: string
4}
5
6const TabsContext = createContext<{
7 value: string
8 setValue: (v: string) => void
9} | null>(null)
10
11const Tabs = ({ children, defaultValue }: TabsProps) => {
12 const [value, setValue] = useState(defaultValue)
13
14 return (
15 <TabsContext.Provider value={{ value, setValue }}>
16 {children}
17 </TabsContext.Provider>
18 )
19}
20
21const TabsList = ({ children }: { children: React.ReactNode }) => (
22 <div role="tablist">{children}</div>
23)
24
25const TabsTrigger = ({ value, children }: { value: string, children: React.ReactNode }) => {
26 const context = useContext(TabsContext)
27 if (!context) throw new Error('TabsTrigger must be used within Tabs')
28
29 return (
30 <button
31 role="tab"
32 aria-selected={context.value === value}
33 onClick={() => context.setValue(value)}
34 >
35 {children}
36 </button>
37 )
38}
39
40const TabsContent = ({ value, children }: { value: string, children: React.ReactNode }) => {
41 const context = useContext(TabsContext)
42 if (!context) throw new Error('TabsContent must be used within Tabs')
43
44 if (context.value !== value) return null
45
46 return <div role="tabpanel">{children}</div>
47}
48
49// Usage
50<Tabs defaultValue="profile">
51 <TabsList>
52 <TabsTrigger value="profile">Profile</TabsTrigger>
53 <TabsTrigger value="settings">Settings</TabsTrigger>
54 </TabsList>
55 <TabsContent value="profile">Profile content</TabsContent>
56 <TabsContent value="settings">Settings content</TabsContent>
57</Tabs>
Render Props
typescript
1interface DataFetcherProps<T> {
2 url: string
3 children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
4}
5
6const DataFetcher = <T,>({ url, children }: DataFetcherProps<T>) => {
7 const [data, setData] = useState<T | null>(null)
8 const [loading, setLoading] = useState(true)
9 const [error, setError] = useState<Error | null>(null)
10
11 useEffect(() => {
12 fetch(url)
13 .then(res => res.json())
14 .then(setData)
15 .catch(setError)
16 .finally(() => setLoading(false))
17 }, [url])
18
19 return <>{children(data, loading, error)}</>
20}
21
22// Usage
23<DataFetcher<User> url="/api/user">
24 {(user, loading, error) => {
25 if (loading) return <Spinner />
26 if (error) return <Error error={error} />
27 if (!user) return null
28 return <UserProfile user={user} />
29 }}
30</DataFetcher>
Custom Hooks Pattern
typescript
1const useLocalStorage = <T>(key: string, initialValue: T) => {
2 const [storedValue, setStoredValue] = useState<T>(() => {
3 try {
4 const item = window.localStorage.getItem(key)
5 return item ? JSON.parse(item) : initialValue
6 } catch (error) {
7 console.error(error)
8 return initialValue
9 }
10 })
11
12 const setValue = useCallback(
13 (value: T | ((val: T) => T)) => {
14 try {
15 const valueToStore = value instanceof Function ? value(storedValue) : value
16 setStoredValue(valueToStore)
17 window.localStorage.setItem(key, JSON.stringify(valueToStore))
18 } catch (error) {
19 console.error(error)
20 }
21 },
22 [key, storedValue],
23 )
24
25 return [storedValue, setValue] as const
26}
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
- 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
- typescript - TypeScript patterns and types for React
- ndk - Nostr integration with React hooks
- skill-creator - Creating reusable component libraries