State Initialization and Default Values
1. Initial State from Props and External Sources
Initialization Source
Pattern
Use Case
Caveat
Props (Direct)
useState(props.initialValue)
Set initial state from parent component
⚠️ Only used on mount, doesn't update when prop changes
Props (Lazy)
useState(() => computeFromProps(props))
Compute initial state from props once
⚠️ Computation runs only on mount
URL Params
useState(() => new URLSearchParams(location.search).get('id'))
Initialize from query string
Sync with URL on changes using useEffect
Environment Variables
useState(process.env.REACT_APP_DEFAULT_THEME)
Configuration-based defaults
Build-time values, not runtime
External API
Initialize empty, fetch in useEffect
Data from server on mount
Handle loading state, errors
Pattern
Code
Behavior
Initialize from Prop
const [count, setCount] = useState(props.initialCount)
Uses prop value on mount, ignores future prop changes
Sync with Prop Changes
useEffect(() => setState(props.value), [props.value])
Updates state when prop changes (controlled pattern)
Key-based Reset
<Component key={props.id} />
Remounts component with fresh state when key changes
Example: Initialize state from props
// Pattern 1: Direct initialization (one-time)
function Counter ({ initialCount = 0 }) {
// Only uses initialCount on first render
const [ count , setCount ] = useState (initialCount);
// If parent changes initialCount, this component's state won't update
return < div >{count}</ div >;
}
// Pattern 2: Sync with prop changes (controlled component)
function SyncedCounter ({ count : propCount }) {
const [ count , setCount ] = useState (propCount);
// Update local state when prop changes
useEffect (() => {
setCount (propCount);
}, [propCount]);
return < div >{count}</ div >;
}
// Pattern 3: Key-based reset (best for resetting state)
function EditForm ({ userId }) {
const [ name , setName ] = useState ( '' );
const [ email , setEmail ] = useState ( '' );
// When userId changes, component remounts with fresh state
return < form >...</ form >;
}
// Parent component uses key to reset EditForm
function UserEditor ({ userId }) {
return < EditForm key = {userId} userId = {userId} />;
}
Example: Initialize from URL and external sources
import { useState, useEffect } from 'react' ;
import { useSearchParams } from 'react-router-dom' ;
function ProductFilter () {
// Initialize from URL query params
const [ searchParams ] = useSearchParams ();
const [ category , setCategory ] = useState (() =>
searchParams. get ( 'category' ) || 'all'
);
const [ minPrice , setMinPrice ] = useState (() =>
Number (searchParams. get ( 'minPrice' )) || 0
);
// Initialize from environment
const [ theme , setTheme ] = useState (
process.env. REACT_APP_DEFAULT_THEME || 'light'
);
// Initialize from external API
const [ userPreferences , setUserPreferences ] = useState ( null );
useEffect (() => {
// Fetch user preferences on mount
fetch ( '/api/preferences' )
. then ( res => res. json ())
. then ( data => setUserPreferences (data));
}, []);
return < div >Filter UI</ div >;
}
Common Mistake: Using useState(props.value) and expecting it to update when prop
changes. This creates a "stale initial state" issue. Use controlled component pattern with useEffect or
key-based reset instead.
2. Derived Initial State Patterns
Pattern
Implementation
Use Case
Computed from Props
useState(() => transformProps(props))
Process/transform props for initial state
Merged Defaults
useState({ ...defaults, ...props.overrides })
Combine default values with prop overrides
Normalized Data
useState(() => normalizeData(props.items))
Convert array to normalized structure (byId, allIds)
Filtered/Sorted
useState(() => props.items.filter(condition).sort(compare))
Pre-process collection for initial state
Scenario
Avoid (Re-computed Every Render)
Use (Computed Once)
Array Processing
useState(items.map(transform))
useState(() => items.map(transform))
Object Merge
useState({ ...defaults, ...props })
useState(() => ({ ...defaults, ...props }))
Date Calculation
useState(new Date())
useState(() => new Date())
Example: Derived initial state patterns
// Pattern 1: Computed from props
function TaskList ({ tasks , filter }) {
// Compute initial filtered list once
const [ filteredTasks , setFilteredTasks ] = useState (() =>
tasks. filter ( task => task.status === filter)
);
return < ul >{filteredTasks. map ( t => < li key = {t.id}>{t.title}</ li >)}</ ul >;
}
// Pattern 2: Merged defaults with prop overrides
function ConfigPanel ({ userConfig = {} }) {
const defaultConfig = {
theme: 'light' ,
language: 'en' ,
notifications: true ,
autoSave: false
};
// Merge defaults with user overrides
const [ config , setConfig ] = useState (() => ({
... defaultConfig,
... userConfig
}));
return < div >Config UI</ div >;
}
// Pattern 3: Normalize data structure
function UserDirectory ({ users }) {
// Normalize array to efficient lookup structure
const [ usersById , setUsersById ] = useState (() => {
const normalized = {};
users. forEach ( user => {
normalized[user.id] = user;
});
return normalized;
});
const [ userIds , setUserIds ] = useState (() => users. map ( u => u.id));
return < div >User directory</ div >;
}
// Pattern 4: Complex transformation
function DataChart ({ rawData }) {
const [ chartData , setChartData ] = useState (() => {
// Expensive transformation done only once
return rawData
. filter ( d => d.value > 0 )
. map ( d => ({ x: d.timestamp, y: d.value }))
. sort (( a , b ) => a.x - b.x);
});
return < Chart data = {chartData} />;
}
Performance Tip: Always use lazy initialization with arrow function when computing initial
state from props or external data. This ensures expensive computations run only once on mount, not on every
render.
3. State Hydration from localStorage/sessionStorage
Storage Type
Lifespan
Scope
Use Case
localStorage
Permanent (until cleared)
All tabs, same origin
User preferences, settings, auth tokens
sessionStorage
Session (tab close)
Single tab only
Temporary form data, wizard state
IndexedDB
Permanent
All tabs, large data
Offline data, cached API responses
Hydration Pattern
Implementation
Features
Basic Read
useState(() => JSON.parse(localStorage.getItem(key)) || default)
Simple one-time load
With Error Handling
Try-catch around JSON.parse
Handles corrupt/invalid data
Auto-sync (Write)
useEffect to save on state changes
Persists state automatically
Custom Hook RECOMMENDED
useLocalStorage(key, defaultValue)
Reusable, encapsulated logic
Example: localStorage hydration patterns
import { useState, useEffect } from 'react' ;
// Pattern 1: Basic hydration with fallback
function ThemeSelector () {
const [ theme , setTheme ] = useState (() => {
const saved = localStorage. getItem ( 'theme' );
return saved || 'light' ; // Fallback to 'light'
});
// Save to localStorage when theme changes
useEffect (() => {
localStorage. setItem ( 'theme' , theme);
}, [theme]);
return < select value = {theme} onChange = { e => setTheme (e.target.value)}>...</ select >;
}
// Pattern 2: With JSON parsing and error handling
function UserPreferences () {
const [ preferences , setPreferences ] = useState (() => {
try {
const saved = localStorage. getItem ( 'preferences' );
return saved ? JSON . parse (saved) : { notifications: true , autoSave: false };
} catch (error) {
console. error ( 'Failed to parse preferences:' , error);
return { notifications: true , autoSave: false };
}
});
useEffect (() => {
try {
localStorage. setItem ( 'preferences' , JSON . stringify (preferences));
} catch (error) {
console. error ( 'Failed to save preferences:' , error);
}
}, [preferences]);
return < div >Preferences UI</ div >;
}
Example: Custom useLocalStorage hook
import { useState, useEffect } from 'react' ;
function useLocalStorage ( key , defaultValue ) {
// Initialize from localStorage
const [ value , setValue ] = useState (() => {
try {
const item = window.localStorage. getItem (key);
return item ? JSON . parse (item) : defaultValue;
} catch (error) {
console. error ( `Error reading localStorage key "${ key }":` , error);
return defaultValue;
}
});
// Sync to localStorage on changes
useEffect (() => {
try {
window.localStorage. setItem (key, JSON . stringify (value));
} catch (error) {
console. error ( `Error setting localStorage key "${ key }":` , error);
}
}, [key, value]);
return [value, setValue];
}
// Usage: Clean, reusable
function App () {
const [ theme , setTheme ] = useLocalStorage ( 'theme' , 'light' );
const [ user , setUser ] = useLocalStorage ( 'user' , null );
const [ settings , setSettings ] = useLocalStorage ( 'settings' , { lang: 'en' });
return (
< div >
< button onClick = {() => setTheme (theme === 'light' ? 'dark' : 'light' )}>
Toggle Theme
</ button >
</ div >
);
}
Example: sessionStorage for temporary state
// Multi-step form with sessionStorage backup
function MultiStepForm () {
const [ step , setStep ] = useState (() => {
const saved = sessionStorage. getItem ( 'formStep' );
return saved ? Number (saved) : 1 ;
});
const [ formData , setFormData ] = useState (() => {
const saved = sessionStorage. getItem ( 'formData' );
return saved ? JSON . parse (saved) : { name: '' , email: '' , phone: '' };
});
// Save to sessionStorage on changes
useEffect (() => {
sessionStorage. setItem ( 'formStep' , step. toString ());
}, [step]);
useEffect (() => {
sessionStorage. setItem ( 'formData' , JSON . stringify (formData));
}, [formData]);
// Clear on successful submit
const handleSubmit = async () => {
await submitForm (formData);
sessionStorage. removeItem ( 'formStep' );
sessionStorage. removeItem ( 'formData' );
};
return < form >Step {step} form</ form >;
}
Storage Limits: localStorage/sessionStorage typically have 5-10MB limits per origin. Always
handle quota exceeded errors. For larger data, use IndexedDB. Never store sensitive data unencrypted.
4. Controlled vs Uncontrolled Component Patterns
Pattern
State Location
Value Source
Updates Via
Controlled
React state (parent or component)
value prop from state
onChange handler updates state
Uncontrolled
DOM (native element state)
defaultValue prop (initial only)
DOM manages its own state, access via ref
Aspect
Controlled
Uncontrolled
Single Source of Truth
✅ React state is source of truth
❌ DOM is source of truth
Real-time Validation
✅ Easy to validate on each keystroke
❌ Validate only on submit/blur
Conditional Rendering
✅ Can easily show/hide based on state
❌ Limited control over display logic
Performance
⚠️ Re-renders on every change
✅ No re-renders, faster for large forms
Code Complexity
❌ More boilerplate (state + handlers)
✅ Less code, simpler for basic forms
Integration
✅ Works well with React ecosystem
⚠️ Limited to native inputs
Example: Controlled component pattern
import { useState } from 'react' ;
// Controlled input: React state is source of truth
function ControlledForm () {
const [ name , setName ] = useState ( '' );
const [ email , setEmail ] = useState ( '' );
const [ age , setAge ] = useState ( '' );
// Real-time validation
const isValidEmail = email. includes ( '@' );
const isValidAge = age === '' || ( Number (age) > 0 && Number (age) < 150 );
const handleSubmit = ( e ) => {
e. preventDefault ();
console. log ({ name, email, age });
};
return (
< form onSubmit = {handleSubmit}>
< input
type = "text"
value = {name}
onChange = { e => setName (e.target.value)}
placeholder = "Name"
/>
< input
type = "email"
value = {email}
onChange = { e => setEmail (e.target.value)}
placeholder = "Email"
/>
{ ! isValidEmail && email && < span >Invalid email</ span >}
< input
type = "number"
value = {age}
onChange = { e => setAge (e.target.value)}
placeholder = "Age"
/>
{ ! isValidAge && < span >Invalid age</ span >}
< button type = "submit" disabled = { ! isValidEmail || ! isValidAge}>
Submit
</ button >
</ form >
);
}
Example: Uncontrolled component pattern
import { useRef } from 'react' ;
// Uncontrolled input: DOM holds the state
function UncontrolledForm () {
const nameRef = useRef ( null );
const emailRef = useRef ( null );
const ageRef = useRef ( null );
const handleSubmit = ( e ) => {
e. preventDefault ();
// Access values from DOM via refs
const formData = {
name: nameRef.current.value,
email: emailRef.current.value,
age: ageRef.current.value
};
console. log (formData);
};
return (
< form onSubmit = {handleSubmit}>
< input
type = "text"
ref = {nameRef}
defaultValue = ""
placeholder = "Name"
/>
< input
type = "email"
ref = {emailRef}
defaultValue = ""
placeholder = "Email"
/>
< input
type = "number"
ref = {ageRef}
defaultValue = ""
placeholder = "Age"
/>
< button type = "submit" >Submit</ button >
</ form >
);
}
Example: Hybrid approach
// Hybrid: Controlled for some fields, uncontrolled for others
function HybridForm () {
// Controlled for important fields that need validation
const [ email , setEmail ] = useState ( '' );
const [ password , setPassword ] = useState ( '' );
// Uncontrolled for less critical fields
const nameRef = useRef ( null );
const bioRef = useRef ( null );
const handleSubmit = ( e ) => {
e. preventDefault ();
const formData = {
email,
password,
name: nameRef.current.value,
bio: bioRef.current.value
};
console. log (formData);
};
const isValidEmail = email. includes ( '@' );
const isValidPassword = password. length >= 8 ;
return (
< form onSubmit = {handleSubmit}>
{ /* Controlled - need validation */ }
< input
type = "email"
value = {email}
onChange = { e => setEmail (e.target.value)}
/>
{ ! isValidEmail && email && < span >Invalid email</ span >}
< input
type = "password"
value = {password}
onChange = { e => setPassword (e.target.value)}
/>
{ ! isValidPassword && password && < span >Min 8 characters</ span >}
{ /* Uncontrolled - simple fields */ }
< input type = "text" ref = {nameRef} defaultValue = "" placeholder = "Name" />
< textarea ref = {bioRef} defaultValue = "" placeholder = "Bio" />
< button type = "submit" disabled = { ! isValidEmail || ! isValidPassword}>
Submit
</ button >
</ form >
);
}
Recommendation: Prefer controlled components for React apps -
they integrate better with React's data flow and enable powerful features like real-time validation. Use uncontrolled only for performance-critical scenarios or when integrating with
non-React code.
5. Default Props and State Fallback Strategies
Fallback Strategy
Implementation
Use Case
Default Parameters
function Comp({ value = 'default' })
Simple primitive defaults
Nullish Coalescing
const val = prop ?? 'default'
Distinguish null/undefined from other falsy values
Logical OR
const val = prop || 'default'
Any falsy value triggers fallback
Object Spread
{ ...defaults, ...props }
Merge default object with prop overrides
Conditional Render
{value !== undefined ? <Component /> : <Fallback />}
Render different components based on prop presence
Operator
Triggers Fallback When
Example
|| (OR)
Falsy: false, 0, '', null, undefined, NaN
count || 10 → 0 triggers fallback (unexpected!)
?? (Nullish) RECOMMENDED
Only null or undefined
count ?? 10 → 0 doesn't trigger fallback ✅
Example: Default prop patterns
// Pattern 1: Default parameters (function signature)
function Button ({ label = 'Click me' , variant = 'primary' , onClick }) {
return < button className = {variant} onClick = {onClick}>{label}</ button >;
}
// Pattern 2: Nullish coalescing for state initialization
function Counter ({ initialCount }) {
// Correctly handles initialCount = 0 (0 is valid, not fallback)
const [ count , setCount ] = useState (initialCount ?? 10 );
return < div >{count}</ div >;
}
// ❌ Wrong: OR operator treats 0 as falsy
function BadCounter ({ initialCount }) {
const [ count , setCount ] = useState (initialCount || 10 );
// If initialCount = 0, uses 10 instead (unexpected!)
return < div >{count}</ div >;
}
// Pattern 3: Object spread for complex defaults
function UserProfile ({ user = {} }) {
const defaultUser = {
name: 'Guest' ,
avatar: '/default-avatar.png' ,
role: 'viewer' ,
preferences: {
theme: 'light' ,
language: 'en'
}
};
// Merge defaults with provided user data
const profile = {
... defaultUser,
... user,
preferences: {
... defaultUser.preferences,
... user.preferences
}
};
return < div >{profile.name}</ div >;
}
Example: Fallback strategies for missing data
import { useState, useEffect } from 'react' ;
function DataDisplay ({ dataSource }) {
const [ data , setData ] = useState ( null );
const [ error , setError ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
fetch (dataSource)
. then ( res => res. json ())
. then (setData)
. catch (setError)
. finally (() => setLoading ( false ));
}, [dataSource]);
// Fallback strategy: Check state in order
if (loading) {
return < LoadingSpinner />;
}
if (error) {
return < ErrorMessage error = {error} />;
}
if ( ! data || data. length === 0 ) {
return < EmptyState message = "No data available" />;
}
return (
< div >
{data. map ( item => (
< div key = {item.id}>
{ /* Nested fallbacks for optional fields */ }
< h2 >{item.title ?? 'Untitled' }</ h2 >
< p >{item.description || 'No description provided' }</ p >
< span >{item.author?.name ?? 'Anonymous' }</ span >
</ div >
))}
</ div >
);
}
// Pattern: Optional chaining + nullish coalescing
function SafeDataAccess ({ data }) {
return (
< div >
{ /* Safe navigation through nested properties */ }
< p >City: {data?.user?.address?.city ?? 'Unknown' }</ p >
< p >Posts: {data?.user?.posts?. length ?? 0 }</ p >
< p >Name: {data?.user?.profile?.name || 'Guest' }</ p >
</ div >
);
}
Example: Conditional rendering fallbacks
function ContentPanel ({ content , placeholder , emptyMessage }) {
// Strategy 1: Render placeholder if content is undefined
if (content === undefined ) {
return < Placeholder text = {placeholder} />;
}
// Strategy 2: Render empty state if content exists but is empty
if (content === null || (Array. isArray (content) && content. length === 0 )) {
return < EmptyState message = {emptyMessage ?? 'No content available' } />;
}
// Strategy 3: Render actual content
return < div >{content}</ div >;
}
// Usage with multiple fallback levels
function App () {
const [ data , setData ] = useState ( undefined ); // undefined = not loaded yet
return (
< ContentPanel
content = {data}
placeholder = "Loading content..."
emptyMessage = "No items to display"
/>
);
}
State Initialization Best Practices Summary
Use lazy initialization for expensive computations from props or external
sources
Prefer ?? (nullish coalescing) over || for defaults to handle 0, false, ''
correctly
Implement custom hooks like useLocalStorage for reusable persistence logic
Choose controlled components for better React integration and validation
Use key prop to reset component state when data source changes
Add error handling for localStorage parsing and quota exceeded errors
Apply optional chaining (?.) with nullish coalescing for safe nested access