Proper state initialization in React improves performance and prevents bugs. Here's how to do it effectively.
Basic Initialization#
1import { useState } from 'react';
2
3function Counter() {
4 // Direct value initialization
5 const [count, setCount] = useState(0);
6 const [name, setName] = useState('');
7 const [items, setItems] = useState([]);
8 const [user, setUser] = useState(null);
9
10 return <div>{count}</div>;
11}Lazy Initialization#
1// For expensive computations, use a function
2function ExpensiveComponent({ data }) {
3 // This function only runs ONCE on mount
4 const [processed, setProcessed] = useState(() => {
5 return expensiveProcessing(data);
6 });
7
8 return <div>{processed}</div>;
9}
10
11// BAD - runs on every render
12function BadExample({ data }) {
13 const [value, setValue] = useState(expensiveProcessing(data));
14 return <div>{value}</div>;
15}
16
17// Reading from localStorage
18function ThemeProvider({ children }) {
19 const [theme, setTheme] = useState(() => {
20 const saved = localStorage.getItem('theme');
21 return saved ? JSON.parse(saved) : 'light';
22 });
23
24 return (
25 <ThemeContext.Provider value={{ theme, setTheme }}>
26 {children}
27 </ThemeContext.Provider>
28 );
29}Props to State (Anti-pattern Warning)#
1// AVOID - state that mirrors props
2function BadComponent({ initialValue }) {
3 // This only uses initialValue once, updates to prop are ignored
4 const [value, setValue] = useState(initialValue);
5 return <input value={value} onChange={e => setValue(e.target.value)} />;
6}
7
8// BETTER - use key to reset when needed
9function Parent() {
10 const [userId, setUserId] = useState(1);
11
12 return (
13 // Key change resets component state
14 <UserForm key={userId} initialData={userData} />
15 );
16}
17
18// OR - use effect for sync (carefully)
19function SyncedComponent({ externalValue }) {
20 const [value, setValue] = useState(externalValue);
21
22 useEffect(() => {
23 setValue(externalValue);
24 }, [externalValue]);
25
26 return <input value={value} onChange={e => setValue(e.target.value)} />;
27}Derived State#
1// DON'T store derived state
2function BadList({ items }) {
3 const [filteredItems, setFilteredItems] = useState([]);
4 const [filter, setFilter] = useState('');
5
6 // Syncing derived state is error-prone
7 useEffect(() => {
8 setFilteredItems(items.filter(item => item.includes(filter)));
9 }, [items, filter]);
10
11 return /* ... */;
12}
13
14// DO compute during render
15function GoodList({ items }) {
16 const [filter, setFilter] = useState('');
17
18 // Derived value - no state needed
19 const filteredItems = items.filter(item => item.includes(filter));
20
21 // Use useMemo if expensive
22 const filteredItems = useMemo(
23 () => items.filter(item => item.includes(filter)),
24 [items, filter]
25 );
26
27 return /* ... */;
28}Complex State Objects#
1// Initialize with complete shape
2function Form() {
3 const [formData, setFormData] = useState({
4 name: '',
5 email: '',
6 phone: '',
7 address: {
8 street: '',
9 city: '',
10 zip: '',
11 },
12 });
13
14 const updateField = (field, value) => {
15 setFormData(prev => ({
16 ...prev,
17 [field]: value,
18 }));
19 };
20
21 const updateAddress = (field, value) => {
22 setFormData(prev => ({
23 ...prev,
24 address: {
25 ...prev.address,
26 [field]: value,
27 },
28 }));
29 };
30
31 return /* ... */;
32}useReducer for Complex State#
1import { useReducer } from 'react';
2
3const initialState = {
4 items: [],
5 loading: false,
6 error: null,
7 page: 1,
8};
9
10function reducer(state, action) {
11 switch (action.type) {
12 case 'FETCH_START':
13 return { ...state, loading: true, error: null };
14 case 'FETCH_SUCCESS':
15 return { ...state, loading: false, items: action.payload };
16 case 'FETCH_ERROR':
17 return { ...state, loading: false, error: action.payload };
18 case 'NEXT_PAGE':
19 return { ...state, page: state.page + 1 };
20 case 'RESET':
21 return initialState;
22 default:
23 return state;
24 }
25}
26
27function DataList() {
28 const [state, dispatch] = useReducer(reducer, initialState);
29
30 // Or with lazy initialization
31 const [state, dispatch] = useReducer(reducer, null, () => {
32 return getInitialStateFromStorage() || initialState;
33 });
34
35 return /* ... */;
36}Ref for Non-Render Values#
1import { useState, useRef, useEffect } from 'react';
2
3function Timer() {
4 const [count, setCount] = useState(0);
5
6 // Don't need state for values that don't affect render
7 const intervalRef = useRef(null);
8 const startTimeRef = useRef(null);
9
10 const start = () => {
11 startTimeRef.current = Date.now();
12 intervalRef.current = setInterval(() => {
13 setCount(c => c + 1);
14 }, 1000);
15 };
16
17 const stop = () => {
18 clearInterval(intervalRef.current);
19 console.log('Ran for:', Date.now() - startTimeRef.current);
20 };
21
22 useEffect(() => {
23 return () => clearInterval(intervalRef.current);
24 }, []);
25
26 return (
27 <div>
28 <p>{count}</p>
29 <button onClick={start}>Start</button>
30 <button onClick={stop}>Stop</button>
31 </div>
32 );
33}State Lifting#
1// Lift state to common ancestor
2function Parent() {
3 const [selectedId, setSelectedId] = useState(null);
4
5 return (
6 <div>
7 <List items={items} selectedId={selectedId} onSelect={setSelectedId} />
8 <Detail itemId={selectedId} />
9 </div>
10 );
11}
12
13function List({ items, selectedId, onSelect }) {
14 return (
15 <ul>
16 {items.map(item => (
17 <li
18 key={item.id}
19 onClick={() => onSelect(item.id)}
20 className={item.id === selectedId ? 'selected' : ''}
21 >
22 {item.name}
23 </li>
24 ))}
25 </ul>
26 );
27}Initial State from URL#
1function SearchPage() {
2 // Initialize from URL search params
3 const [searchParams, setSearchParams] = useSearchParams();
4
5 const [filters, setFilters] = useState(() => ({
6 query: searchParams.get('q') || '',
7 category: searchParams.get('cat') || 'all',
8 sort: searchParams.get('sort') || 'relevance',
9 }));
10
11 // Sync state back to URL
12 useEffect(() => {
13 const params = new URLSearchParams();
14 if (filters.query) params.set('q', filters.query);
15 if (filters.category !== 'all') params.set('cat', filters.category);
16 if (filters.sort !== 'relevance') params.set('sort', filters.sort);
17 setSearchParams(params);
18 }, [filters, setSearchParams]);
19
20 return /* ... */;
21}Reset State Pattern#
1function EditForm({ item, onSave, onCancel }) {
2 // Initialize from prop
3 const [draft, setDraft] = useState(item);
4
5 // Reset when item changes
6 useEffect(() => {
7 setDraft(item);
8 }, [item]);
9
10 // Or use key from parent
11 // <EditForm key={item.id} item={item} />
12
13 const handleReset = () => {
14 setDraft(item);
15 };
16
17 return (
18 <form>
19 <input
20 value={draft.name}
21 onChange={e => setDraft({ ...draft, name: e.target.value })}
22 />
23 <button type="button" onClick={handleReset}>Reset</button>
24 <button type="button" onClick={onCancel}>Cancel</button>
25 <button type="submit">Save</button>
26 </form>
27 );
28}Best Practices#
Initialization:
✓ Use lazy init for expensive ops
✓ Provide complete initial shape
✓ Use functions for localStorage
✓ Consider useReducer for complex
Performance:
✓ Compute derived values, don't store
✓ Use refs for non-render values
✓ useMemo for expensive derivations
✓ Split state by update frequency
Patterns:
✓ Lift state to common ancestor
✓ Use key to reset components
✓ Sync URL with state when needed
✓ Reset state explicitly when needed
Avoid:
✗ Mirroring props in state
✗ Storing derived state
✗ Expensive inline initialization
✗ Unnecessary state synchronization
Conclusion#
Proper state initialization improves performance and prevents bugs. Use lazy initialization for expensive computations, compute derived values during render instead of storing them, and use refs for values that don't affect rendering. Lift state to the appropriate level and use the key prop to reset components when needed.