Home » State Management with Redux

Redux is a powerful library used for managing global state in JavaScript applications, especially in React. Redux is particularly useful for larger applications where managing state across many components can become complex.

Introduction to Redux

Redux is based on the principle of a single source of truth, meaning all your application’s state is stored in one central store. The main concepts in Redux are:

  • Store: The global state of the application.
  • Actions: Descriptions of state changes.
  • Reducers: Pure functions that take the current state and an action, then return a new state.
  • Dispatch: A method to send actions to the store.

Setting Up Redux in a React Project

To use Redux in a React project, you’ll need to install the following packages:

npm install redux react-redux


Here’s how you can set up Redux:
1.Create Actions: Actions are plain JavaScript objects that describe the type of state change.

// actions.js
export const increment = () => {
  return {
    type: 'INCREMENT',
  };
};

export const decrement = () => {
  return {
    type: 'DECREMENT',
  };
};

2.Create a Reducer: A reducer is a pure function that takes the current state and an action, then returns a new state.

// reducer.js
const initialState = {
  count: 0,
};

const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1,
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1,
      };
    default:
      return state;
  }
};

export default counterReducer;

3.Create the Redux Store: The store holds the global state of the application.

// store.js
import { createStore } from 'redux';
import counterReducer from './reducer';

const store = createStore(counterReducer);

export default store;

4.Provide the Store to the React Application: Use the ‘Provider’ component from ‘react-redux’ to make the store available to all components.

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

5.Connect Components to Redux: Use the ‘useSelector’ and ‘useDispatch’ hooks from ‘react-redux’ to interact with the store.

// Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';

function Counter() {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}

export default Counter;

Example: Building a Counter App with Redux

Here’s a complete example of a simple counter app using Redux:

// actions.js
export const increment = () => ({ type: 'INCREMENT' });
export const decrement = () => ({ type: 'DECREMENT' });

// reducer.js
const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

export default counterReducer;

// store.js
import { createStore } from 'redux';
import counterReducer from './reducer';

const store = createStore(counterReducer);
export default store;

// Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';

function Counter() {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}

export default Counter;

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';

ReactDOM.render(
  <Provider store={store}>
    <Counter />
  </Provider>,
  document.getElementById('root')
);

State Management with Recoil

Recoil is a state management library for React that is designed to be easy to use and scalable. It allows you to create shared state (atoms) and derive state from other state (selectors) in a way that feels natural in React.

Introduction to Recoil

Recoil offers several benefits:

  • Simplicity: Recoil’s API is simple and integrates seamlessly with React.
  • Performance: It optimizes performance by minimizing unnecessary renders.
  • Scalability: Recoil can handle complex state management needs in large applications.

Creating Atoms and Selectors

  • Atoms: The basic units of state in Recoil.
  • Selectors: Derived state that depends on other atoms or selectors.

Here’s an example of creating atoms and selectors:

// atoms.js
import { atom } from 'recoil';

export const countState = atom({
  key: 'countState', // unique ID
  default: 0, // default value
});

// selectors.js
import { selector } from 'recoil';
import { countState } from './atoms';

export const doubleCountState = selector({
  key: 'doubleCountState',
  get: ({ get }) => {
    const count = get(countState);
    return count * 2;
  },
});

Using Recoil in Components

To use Recoil, wrap your application with theRecoilRoot’ component and access the atoms and selectors in your components using the ‘useRecoilState’ and useRecoilValue’ hooks.

// Counter.js
import React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { countState } from './atoms';
import { doubleCountState } from './selectors';

function Counter() {
  const [count, setCount] = useRecoilState(countState);
  const doubleCount = useRecoilValue(doubleCountState);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double Count: {doubleCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

export default Counter;

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import Counter from './Counter';

ReactDOM.render(
  <RecoilRoot>
    <Counter />
  </RecoilRoot>,
  document.getElementById('root')
);

Example: Building a Todo App with Recoil

Here’s an example of building a simple todo app with Recoil:

// atoms.js
import { atom } from 'recoil';

export const todoListState = atom({
  key: 'todoListState',
  default: [],
});

// TodoItem.js
import React from 'react';
import { useRecoilState } from 'recoil';
import { todoListState } from './atoms';

function TodoItem({ item }) {
  const [todoList, setTodoList] = useRecoilState(todoListState);

  const toggleComplete = () => {
    const updatedList = todoList.map((todo) =>
      todo.id === item.id ? { ...todo, isComplete: !todo.isComplete } : todo
    );
    setTodoList(updatedList);
  };

  return (
    <li>
      <input
        type="checkbox"
        checked={item.isComplete}
        onChange={toggleComplete}
      />
      {item.text}
    </li>
  );
}

export default TodoItem;

// TodoList.js
import React from 'react';
import { useRecoilValue } from 'recoil';
import { todoListState } from './atoms';
import TodoItem from './TodoItem';

function TodoList() {
  const todoList = useRecoilValue(todoListState);

  return (
    <ul>
      {todoList.map((item) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </ul>
  );
}

export default TodoList;

// AddTodo.js
import React, { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { todoListState } from './atoms';

function AddTodo() {
  const [inputValue, setInputValue] = useState('');
  const setTodoList = useSetRecoilState(todoListState);

  const addItem = () => {
    setTodoList((oldTodoList) => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isComplete: false,
      },
    ]);
    setInputValue('');
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={addItem}>Add</button>
    </div>
  );
}

export default AddTodo;

// utils.js
let id = 0;
export function getId() {
  return id++;
}

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import TodoList from './TodoList';
import AddTodo from './AddTodo';

ReactDOM.render(
  <RecoilRoot>
    <AddTodo />
    <TodoList />
  </RecoilRoot>,
  document.getElementById('root')
);

Data Fetching with React Query

React Query is a library that simplifies data fetching and state management in React applications. It provides powerful features like caching, background updates, and error handling, making it a go-to tool for managing server state.

Introduction to React Query

React Query focuses on server-state management, which is distinct from client-state management (handled by tools like Redux and Recoil). Key features include:

  • Caching: Automatically caches data and keeps it in sync with the server.
  • Automatic Refetching: Automatically refetches data when queries become stale.
  • Parallel Queries: Supports fetching multiple queries simultaneously.

Setting Up React Query

To get started with React Query, install the package:

npm install @tanstack/react-query

Next, wrap your application with the ‘QueryClientProvider’ and create a ‘QueryClient’:

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

const queryClient = new QueryClient();

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
  document.getElementById('root')
);

Fetching Data with React Query

Use theuseQuery’ hook to fetch data:

// App.js
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

function App() {
  const { data, error, isLoading } = useQuery(['todos'], () =>
    axios.get('/api/todos').then((res) => res.data)
  );

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

export default App;

Mutating Data with React Query

TheuseMutation’ hook is used to create, update, or delete data:

// App.js
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

function App() {
  const queryClient = useQueryClient();
  const { data } = useQuery(['todos'], () =>
    axios.get('/api/todos').then((res) => res.data)
  );

  const mutation = useMutation(
    (newTodo) => axios.post('/api/todos', newTodo),
    {
      onSuccess: () => {
        queryClient.invalidateQueries(['todos']);
      },
    }
  );

  const [newTodo, setNewTodo] = useState('');

  return (
    <div>
      <ul>
        {data?.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <input
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
        placeholder="New Todo"
      />
      <button
        onClick={() => {
          mutation.mutate({ title: newTodo });
          setNewTodo('');
        }}
      >
        Add Todo
      </button>
    </div>
  );
}

export default App;

Advanced Features of React Query

  • Optimistic Updates: Update the UI before the mutation is completed, giving a snappy feel.
  • Infinite Queries: Load more data as the user scrolls.
  • Prefetching: Preload data before the user navigates to a new page.

Best Practices for State Management in React

  1. Choose the Right Tool: Use useState’ and ‘useReducer’ for local component state. Opt for Context API for simple global state needs, and consider Redux, Recoil, or React Query for complex state management.
  2. Keep State Minimal: Only store the minimal amount of state necessary. Derived data should be calculated in render or selectors.
  3. Modularize Your State: Break down your state into smaller, manageable pieces. For example, use multiple reducers or Recoil atoms.
  4. Avoid Prop Drilling: Use Context or a state management library to avoid passing props through many layers of components.
  5. Optimize Performance: UseuseMemo’ and useCallback’ to prevent unnecessary re-renders. With libraries like Redux, consider usingreselect’ to memoize selectors.
  6. Handle Side Effects Properly: For side effects like data fetching, use tools like React Query oruseEffect’ with appropriate dependencies.

Conclusion

State management is a crucial part of building scalable and maintainable React applications. WhileuseState’ and useReducer are excellent for local state, managing global state across complex applications often requires more powerful tools like Redux, Recoil, or React Query.

Each of these tools has its strengths and is suitable for different scenarios. Redux excels at handling complex global state, Recoil offers a more React-centric approach with minimal boilerplate, and React Query shines in managing server-state and caching.

By choosing the right tool and following best practices, you can create React applications that are both performant and easy to maintain.

Leave a Reply

Your email address will not be published. Required fields are marked *