react 19 useContext, useReducer & localstorage
Muyiwa Johnson
Posted on June 8, 2024
Solving the State Management Problem in React: A Deep Dive into Centralized Notes Management
Introduction
As applications grow, managing state across various components becomes increasingly complex. Prop drilling can make your codebase cumbersome, and the lack of persistence between sessions can frustrate users who lose their data upon refreshing the browser. To tackle these issues, we'll explore a robust solution using React context, hooks, and reducers to manage and persist notes in a structured and maintainable way.
you might just need this if you are experienced
The Problem
- Prop Drilling: Passing state down multiple levels of components (prop drilling) leads to cluttered and hard-to-maintain code.
- State Persistence: Without persistent storage, data is lost when the user refreshes or closes the browser.
- State Initialization: Initializing state on every render can lead to performance issues.
- Global State Access: Components need a straightforward way to access and update global state.
- Error Handling: Reading from or writing to storage can lead to application crashes if not handled properly.
- Scalability: As the application grows, state management becomes harder to maintain and scale.
Our Solution: Centralized Notes Management
To address these problems, we’ll use a combination of React's createContext
, useContext
, useReducer
, and useEffect
hooks along with localStorage
for persistence. Here’s a detailed breakdown of the approach:
1. Centralized State Management
We start by creating a context to centralize the state:
import { createContext, useContext, useReducer, useEffect } from "react";
import NoteReducer from "../reducers/NoteReducer";
import InitialNotes from "../state/InitialNotes";
const NoteContext = createContext(null);
const NoteDispatchContext = createContext(null);
By using createContext
, we ensure that our state and dispatch functions are accessible globally within our application.
2. Persistence with Local Storage
We use localStorage
to ensure that notes data persists across sessions:
const getStoredNotes = (initialNotes = InitialNotes) => {
try {
const storedNotes = localStorage.getItem("storedNotes");
return storedNotes ? JSON.parse(storedNotes) : initialNotes;
} catch (error) {
console.error("Error reading from localStorage:", error);
return initialNotes;
}
};
This function retrieves notes from localStorage
or falls back to initial notes if an error occurs, ensuring robustness.
3. Lazy State Initialization
By initializing the state lazily, we improve performance:
const NotesProvider = ({ children }) => {
const [notes, dispatch] = useReducer(NoteReducer, undefined, getStoredNotes);
useEffect(() => {
try {
localStorage.setItem("storedNotes", JSON.stringify(notes));
} catch (error) {
console.error("Error saving to localStorage:", error);
}
}, [notes]);
return (
<NoteContext.Provider value={notes}>
<NoteDispatchContext.Provider value={dispatch}>
{children}
</NoteDispatchContext.Provider>
</NoteContext.Provider>
);
};
The useReducer
hook with lazy initialization ensures that state is only loaded from localStorage
once when the component mounts.
4. Global Access and Modification
To simplify state access and updates, we create custom hooks:
export const useNotesContext = () => useContext(NoteContext);
export const useNotesDispatchContext = () => useContext(NoteDispatchContext);
These hooks allow any component to easily access and modify the notes state.
5. Structured State Updates
Using a reducer centralizes the logic for state updates:
// NoteReducer.js
const NoteReducer = (state, action) => {
switch (action.type) {
case 'ADD_NOTE':
return [...state, action.payload];
case 'DELETE_NOTE':
return state.filter(note => note.id !== action.payload.id);
default:
return state;
}
};
export default NoteReducer;
This pattern ensures that state management is predictable and easy to maintain.
6. Error Handling for Data Persistence
By wrapping localStorage
operations in try-catch blocks, we prevent the application from crashing due to storage-related errors:
useEffect(() => {
try {
localStorage.setItem("storedNotes", JSON.stringify(notes));
} catch (error) {
console.error("Error saving to localStorage:", error);
}
}, [notes]);
7. Optimization for Re-renders
The useEffect
hook with a dependency on the notes state ensures that localStorage
is updated only when the notes state changes, minimizing unnecessary operations:
useEffect(() => {
try {
localStorage.setItem("storedNotes", JSON.stringify(notes));
} catch (error) {
console.error("Error saving to localStorage:", error);
}
}, [notes]);
8. Security Consideration
While localStorage
is convenient, it's important to consider security for sensitive data. For more secure storage, consider using encrypted storage options or other secure methods.
9. Scalability and Maintenance
The context and reducer pattern makes the codebase scalable and maintainable. Adding new features or modifying existing logic becomes easier as the state management logic is centralized and well-structured.
Conclusion
By leveraging React context, hooks, and reducers, we can create a robust solution for managing and persisting notes. This approach addresses common problems like prop drilling, state persistence, and performance issues, while providing a scalable and maintainable architecture. Whether you're building a simple note-taking app or a more complex application, these principles will help you manage state effectively and improve the overall user experience.
Posted on June 8, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.