Master state architecture patterns and design principles for scalable React applications with proper state organization.
Master state architecture patterns and design principles for scalable React applications with proper state organization.
State management is the foundation of any React application. Understanding different patterns and when to apply them is crucial for building scalable, maintainable applications.
State that belongs to a single component and doesn't need to be shared.
1function Counter() {
2 const [count, setCount] = useState(0);
3 return <button onClick={() => setCount(count + 1)}>{count}</button>;
4}When to use:
State that needs to be accessed by multiple components at the same level or across different parts of the component tree.
Application-wide state that many components need access to.
1// User authentication, theme, language settings
2const globalState = {
3 user: { id: 1, name: 'John' },
4 theme: 'dark',
5 language: 'en'
6};Data fetched from external sources that needs synchronization.
1// API data, cached responses, loading states
2const serverState = {
3 users: [],
4 loading: false,
5 error: null,
6 lastUpdated: Date.now()
7};Each piece of state should have one authoritative source.
State changes should be predictable and traceable.
Keep only the minimal state necessary, derive everything else.
Organize state in a normalized way to avoid duplication and inconsistencies.
Move state to the closest common ancestor when multiple components need access.
1// ❌ Before - Duplicated state
2function UserProfile() {
3 const [user, setUser] = useState(null);
4 // ...
5}
6
7function UserSettings() {
8 const [user, setUser] = useState(null);
9 // ...
10}
11
12// ✅ After - Lifted state
13function App() {
14 const [user, setUser] = useState(null);
15
16 return (
17 <>
18 <UserProfile user={user} />
19 <UserSettings user={user} onUserUpdate={setUser} />
20 </>
21 );
22}Group related components that work together and share state internally.
1function Accordion({ children }) {
2 const [openIndex, setOpenIndex] = useState(null);
3
4 return (
5 <div className="accordion">
6 {React.Children.map(children, (child, index) =>
7 React.cloneElement(child, {
8 isOpen: openIndex === index,
9 onToggle: () => setOpenIndex(index === openIndex ? null : index)
10 })
11 )}
12 </div>
13 );
14}
15
16function AccordionItem({ title, children, isOpen, onToggle }) {
17 return (
18 <div className="accordion-item">
19 <button onClick={onToggle}>{title}</button>
20 {isOpen && <div>{children}</div>}
21 </div>
22 );
23}
24
25// Usage
26<Accordion>
27 <AccordionItem title="Section 1">Content 1</AccordionItem>
28 <AccordionItem title="Section 2">Content 2</AccordionItem>
29</Accordion>Use React Context to provide state to components deep in the tree without prop drilling.
1const ThemeContext = createContext();
2
3function ThemeProvider({ children }) {
4 const [theme, setTheme] = useState('light');
5
6 const toggleTheme = () => {
7 setTheme(prev => prev === 'light' ? 'dark' : 'light');
8 };
9
10 return (
11 <ThemeContext.Provider value={{ theme, toggleTheme }}>
12 {children}
13 </ThemeContext.Provider>
14 );
15}
16
17function useTheme() {
18 const context = useContext(ThemeContext);
19 if (!context) {
20 throw new Error('useTheme must be used within ThemeProvider');
21 }
22 return context;
23}Model complex state transitions with explicit states and actions.
1const STATES = {
2 IDLE: 'idle',
3 LOADING: 'loading',
4 SUCCESS: 'success',
5 ERROR: 'error'
6};
7
8function dataReducer(state, action) {
9 switch (state.status) {
10 case STATES.IDLE:
11 if (action.type === 'FETCH_START') {
12 return { status: STATES.LOADING, data: null, error: null };
13 }
14 break;
15
16 case STATES.LOADING:
17 if (action.type === 'FETCH_SUCCESS') {
18 return { status: STATES.SUCCESS, data: action.payload, error: null };
19 }
20 if (action.type === 'FETCH_ERROR') {
21 return { status: STATES.ERROR, data: null, error: action.error };
22 }
23 break;
24
25 case STATES.SUCCESS:
26 case STATES.ERROR:
27 if (action.type === 'RESET') {
28 return { status: STATES.IDLE, data: null, error: null };
29 }
30 break;
31 }
32 return state;
33}Organize state by feature rather than by data type.
1// ❌ Data-type organization
2const state = {
3 users: [],
4 posts: [],
5 comments: [],
6 loading: { users: false, posts: false, comments: false },
7 errors: { users: null, posts: null, comments: null }
8};
9
10// ✅ Feature-based organization
11const state = {
12 userManagement: {
13 users: [],
14 loading: false,
15 error: null,
16 selectedUser: null
17 },
18 postManagement: {
19 posts: [],
20 loading: false,
21 error: null,
22 filters: { category: 'all', sortBy: 'date' }
23 },
24 comments: {
25 byPostId: {},
26 loading: false,
27 error: null
28 }
29};Structure data to avoid duplication and make updates efficient.
1// ❌ Nested/Denormalized
2const state = {
3 posts: [
4 {
5 id: 1,
6 title: 'Post 1',
7 author: { id: 1, name: 'John', email: '[email protected]' },
8 comments: [
9 { id: 1, text: 'Great post!', author: { id: 2, name: 'Jane' } }
10 ]
11 }
12 ]
13};
14
15// ✅ Normalized
16const state = {
17 entities: {
18 users: {
19 1: { id: 1, name: 'John', email: '[email protected]' },
20 2: { id: 2, name: 'Jane', email: '[email protected]' }
21 },
22 posts: {
23 1: { id: 1, title: 'Post 1', authorId: 1, commentIds: [1] }
24 },
25 comments: {
26 1: { id: 1, text: 'Great post!', authorId: 2, postId: 1 }
27 }
28 },
29 ui: {
30 currentPostId: 1,
31 loading: false,
32 error: null
33 }
34};1function withLoading(WrappedComponent) {
2 return function WithLoadingComponent(props) {
3 const [loading, setLoading] = useState(false);
4
5 return (
6 <WrappedComponent
7 {...props}
8 loading={loading}
9 setLoading={setLoading}
10 />
11 );
12 };
13}1function useApiData(url) {
2 const [state, setState] = useState({
3 data: null,
4 loading: false,
5 error: null
6 });
7
8 const fetchData = useCallback(async () => {
9 setState(prev => ({ ...prev, loading: true, error: null }));
10 try {
11 const response = await fetch(url);
12 const data = await response.json();
13 setState({ data, loading: false, error: null });
14 } catch (error) {
15 setState(prev => ({ ...prev, loading: false, error }));
16 }
17 }, [url]);
18
19 useEffect(() => {
20 fetchData();
21 }, [fetchData]);
22
23 return { ...state, refetch: fetchData };
24}1function DataProvider({ children }) {
2 const [data, setData] = useState(null);
3 const [loading, setLoading] = useState(false);
4
5 return children({ data, loading, setData, setLoading });
6}
7
8// Usage
9<DataProvider>
10 {({ data, loading, setData, setLoading }) => (
11 <div>
12 {loading ? <Spinner /> : <DataDisplay data={data} />}
13 </div>
14 )}
15</DataProvider>useState for component-specific datauseState)1// ❌ Context value recreation causes re-renders
2function App() {
3 const [user, setUser] = useState(null);
4
5 return (
6 <UserContext.Provider value={{ user, setUser }}>
7 {/* All consumers re-render when App re-renders */}
8 </UserContext.Provider>
9 );
10}
11
12// ✅ Memoized context value
13function App() {
14 const [user, setUser] = useState(null);
15
16 const contextValue = useMemo(
17 () => ({ user, setUser }),
18 [user]
19 );
20
21 return (
22 <UserContext.Provider value={contextValue}>
23 {/* Only re-renders when user changes */}
24 </UserContext.Provider>
25 );
26}1// Split frequently changing state from static configuration
2const UserContext = createContext(); // Static user data
3const UserPreferencesContext = createContext(); // Frequently changing1// Only subscribe to specific parts of state
2const userName = useSelector(state => state.user.name);
3const userEmail = useSelector(state => state.user.email);Place state as close as possible to where it's used.
Keep business logic separate from UI state.
Use consistent patterns for state updates.
Design state that's easy to test in isolation.
Clearly document how state flows through your application.
Your progress is saved in this browser, on this device only.