react 19 useContext, useReducer & localstorage
Tags: webdev, javascript, react, beginners
Published: 2024-06-08 19:12:30

## 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**

![react context reducer & local storage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhx9m2b9hx28x4hgdo93.png)

### The Problem

1. **Prop Drilling**: Passing state down multiple levels of components (prop drilling) leads to cluttered and hard-to-maintain code.
2. **State Persistence**: Without persistent storage, data is lost when the user refreshes or closes the browser.
3. **State Initialization**: Initializing state on every render can lead to performance issues.
4. **Global State Access**: Components need a straightforward way to access and update global state.
5. **Error Handling**: Reading from or writing to storage can lead to application crashes if not handled properly.
6. **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:

```jsx
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:

```jsx
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:

```jsx
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:

```jsx
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:

```jsx
// 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:

```jsx
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:

```jsx
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.