DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 18: Advanced State Management with Redux

Advanced State Management with Redux

Introduction

As React applications grow in complexity, managing state becomes increasingly challenging. Component-level state and Context API work well for simpler applications, but larger applications with complex state interactions often require a more robust solution. Redux is a predictable state container designed to help you write JavaScript apps that behave consistently across different environments.

In this chapter, we’ll dive deep into advanced state management with Redux, exploring its core concepts, architecture, and how to effectively implement it in React applications.

Understanding Redux

What is Redux?

Redux is a state management library that helps you manage the global state of your application in a predictable way. While Redux is commonly used with React, it’s important to understand that Redux is framework-agnostic and can be used with any UI library or framework.

Core Principles of Redux

Redux is built on three fundamental principles:

  1. Single Source of Truth: The entire application state is stored in a single JavaScript object called the “store.”
  2. State is Read-Only: The only way to change the state is to emit an “action” that describes what happened.
  3. Changes are Made with Pure Functions: To specify how the state tree is transformed by actions, you write pure “reducers.”

When to Use Redux

Redux is particularly useful when:

  • You have significant amounts of application state needed in many places
  • The state is updated frequently
  • The logic to update state is complex
  • The application has a medium to large-sized codebase with many people working on it
  • You need to track how state is updated over time

However, remember that Redux introduces additional complexity, so it’s not always the right choice for every application. For smaller applications, React’s built-in state management tools (useState, useReducer, and Context) might be sufficient.

Redux Core Concepts

Actions

Actions are plain JavaScript objects that represent an intention to change the state. Actions are the only way to send data from your application to the Redux store. They must have a type property that indicates the type of action being performed:

// A simple action
const addTodo = {
  type: 'ADD_TODO',
  text: 'Learn Redux'
};

// Using action creators
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

Reducers

Reducers are pure functions that specify how the application’s state changes in response to actions. A reducer takes the previous state and an action as arguments and returns the next state:

function todoReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: Date.now(),
          text: action.text,
          completed: false
        }
      ];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
}

Key characteristics of reducers:

  • They should be pure functions with no side effects
  • They must never mutate their arguments
  • They should return a new state object if the state changes
  • They should return the existing state for unrecognized actions

Store

The store is the object that brings actions and reducers together. The store has the following responsibilities:

  • Holds the application state
  • Allows access to state via getState()
  • Allows state to be updated via dispatch(action)
  • Registers listeners via subscribe(listener)
  • Handles unregistering of listeners

Here’s how you create a store:

import { createStore } from 'redux';
import todoReducer from './reducers';

const store = createStore(todoReducer);

Implementing Redux in React Applications

Setting Up Redux

First, install the necessary packages:

npm install redux react-redux

Creating the Store

Let’s create a store for a todo application:

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

// Initial state
const initialState = {
  todos: []
};

// Reducer
function todoReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: Date.now(),
            text: action.text,
            completed: false
          }
        ]
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        )
      };
    default:
      return state;
  }
}

// Create and export the store
const store = createStore(todoReducer);
export default store;

Connecting Redux to React

To connect Redux to a React application, we use the Provider component from the react-redux library:

// 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')
);

Accessing and Updating Redux State in Components

There are two primary ways to connect React components to the Redux store:

1. Using connect() (Traditional Approach)

The connect() function connects a React component to the Redux store:

// TodoList.js
import React from 'react';
import { connect } from 'react-redux';

function TodoList({ todos, toggleTodo }) {
  return (
    <ul>
      {todos.map(todo => (
        <li
          key={todo.id}
          onClick={() => toggleTodo(todo.id)}
          style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
        >
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

// Map Redux state to component props
const mapStateToProps = state => ({
  todos: state.todos
});

// Map Redux actions to component props
const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch({ type: 'TOGGLE_TODO', id })
});

// Connect component to Redux store
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

2. Using Hooks (Modern Approach)

React-Redux provides hooks that allow you to interact with the Redux store:

// TodoList.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

function TodoList() {
  // Select data from the store
  const todos = useSelector(state => state.todos);
  // Get the dispatch function
  const dispatch = useDispatch();

  const toggleTodo = id => {
    dispatch({ type: 'TOGGLE_TODO', id });
  };

  return (
    <ul>
      {todos.map(todo => (
        <li
          key={todo.id}
          onClick={() => toggleTodo(todo.id)}
          style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
        >
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

Adding a Todo Form

Let’s create a form to add new todos:

// AddTodo.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';

function AddTodo() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();

  const handleSubmit = e => {
    e.preventDefault();
    if (!text.trim()) return;
    dispatch({ type: 'ADD_TODO', text });
    setText('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
        placeholder="Add a todo"
      />
      <button type="submit">Add Todo</button>
    </form>
  );
}

export default AddTodo;

Advanced Redux Patterns

Organizing Redux Code

As applications grow, it’s important to organize Redux code effectively. A common approach is the “Duck” pattern or “Redux Toolkit” approach, where related actions, action creators, and reducers are grouped together.

Duck Pattern

// ducks/todos.js
// Action Types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';

// Action Creators
export const addTodo = text => ({
  type: ADD_TODO,
  text
});

export const toggleTodo = id => ({
  type: TOGGLE_TODO,
  id
});

// Initial State
const initialState = {
  todos: []
};

// Reducer
export default function reducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: Date.now(),
            text: action.text,
            completed: false
          }
        ]
      };
    case TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        )
      };
    default:
      return state;
  }
}

Combining Reducers

For larger applications with many state slices, Redux provides the combineReducers utility:

// rootReducer.js
import { combineReducers } from 'redux';
import todosReducer from './ducks/todos';
import filtersReducer from './ducks/filters';

const rootReducer = combineReducers({
  todos: todosReducer,
  filters: filtersReducer
});

export default rootReducer;

Then use the root reducer when creating your store:

mport { createStore } from 'redux';
import rootReducer from './rootReducer';

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

Middleware and Async Actions

Redux middleware provides a third-party extension point between dispatching an action and the moment it reaches the reducer. Middleware is commonly used for logging, crash reporting, routing, handling asynchronous actions, etc.

Redux Thunk

Redux Thunk is middleware that allows you to write action creators that return a function instead of an action. This is particularly useful for handling asynchronous logic:

npm install redux-thunk
// Configuring the store with thunk
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './rootReducer';

const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;

Using thunk to fetch data:

// Async action creator
export const fetchTodos = () => {
  return async dispatch => {
    dispatch({ type: 'FETCH_TODOS_REQUEST' });
    try {
      const response = await fetch('https://api.example.com/todos');
      const data = await response.json();
      dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_TODOS_FAILURE', error });
    }
  };
};

Redux Saga

Redux Saga is a middleware library that makes handling side effects (like asynchronous data fetching) in Redux applications easier and better organized:

npm install redux-saga
// Setting up Redux Saga
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './rootReducer';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

// Run the saga
sagaMiddleware.run(rootSaga);

export default store;

Example saga for fetching todos:

// sagas.js
import { call, put, takeEvery } from 'redux-saga/effects';

// Worker saga
function* fetchTodos() {
  try {
    yield put({ type: 'FETCH_TODOS_REQUEST' });
    const response = yield call(fetch, 'https://api.example.com/todos');
    const data = yield response.json();
    yield put({ type: 'FETCH_TODOS_SUCCESS', payload: data });
  } catch (error) {
    yield put({ type: 'FETCH_TODOS_FAILURE', error });
  }
}

// Watcher saga
function* rootSaga() {
  yield takeEvery('FETCH_TODOS', fetchTodos);
}

export default rootSaga;

Modern Redux with Redux Toolkit

Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development. It simplifies common Redux tasks and helps you follow best practices automatically.

npm install @reduxjs/toolkit

Creating a Slice

// features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers
      // It doesn't actually mutate the state because it uses Immer internally
      state.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    }
  }
});

// Export actions
export const { addTodo, toggleTodo } = todosSlice.actions;

// Export reducer
export default todosSlice.reducer;

Configuring the Store

// store.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './features/todos/todosSlice';
import filtersReducer from './features/filters/filtersSlice';

const store = configureStore({
  reducer: {
    todos: todosReducer,
    filters: filtersReducer
  }
});

export default store;

Using Redux Toolkit with React Components

// TodoList.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { toggleTodo } from './features/todos/todosSlice';

function TodoList() {
  const todos = useSelector(state => state.todos);
  const dispatch = useDispatch();

  return (
    <ul>
      {todos.map(todo => (
        <li
          key={todo.id}
          onClick={() => dispatch(toggleTodo(todo.id))}
          style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
        >
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

Async Logic with createAsyncThunk

Redux Toolkit provides createAsyncThunk to simplify async logic:

javascript

// features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// First, create the thunk
export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async () => {
    const response = await fetch('https://api.example.com/todos');
    return response.json();
  }
);

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
    error: null
  },
  reducers: {
    // ... other reducers
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  }
});

export default todosSlice.reducer;

Performance Optimization in Redux

Memoizing Selectors with Reselect

Reselect is a library for creating memoized selector functions. It helps prevent unnecessary recalculations:

npm install reselect
import { createSelector } from 'reselect';

// Input selectors
const getTodos = state => state.todos;
const getVisibilityFilter = state => state.visibilityFilter;

// Memoized selector
export const getVisibleTodos = createSelector(
  [getTodos, getVisibilityFilter],
  (todos, filter) => {
    switch (filter) {
      case 'SHOW_ALL':
        return todos;
      case 'SHOW_COMPLETED':
        return todos.filter(todo => todo.completed);
      case 'SHOW_ACTIVE':
        return todos.filter(todo => !todo.completed);
      default:
        return todos;
    }
  }
);

With Redux Toolkit, createSelector is already included:

import { createSelector } from '@reduxjs/toolkit';

// Same usage as above

Normalizing State Shape

For complex applications with relationships between entities, normalizing your state can significantly improve performance:

// Instead of:
const state = {
  posts: [
    {
      id: 1,
      author: { id: 1, name: 'User 1' },
      comments: [
        { id: 1, author: { id: 2, name: 'User 2' }, content: 'Comment 1' }
      ]
    }
  ]
};

// Use a normalized structure:
const state = {
  posts: {
    byId: {
      1: { id: 1, author: 1, comments: [1] }
    },
    allIds: [1]
  },
  users: {
    byId: {
      1: { id: 1, name: 'User 1' },
      2: { id: 2, name: 'User 2' }
    },
    allIds: [1, 2]
  },
  comments: {
    byId: {
      1: { id: 1, author: 2, content: 'Comment 1' }
    },
    allIds: [1]
  }
};

Redux Toolkit includes createEntityAdapter which helps with normalization:

import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';

const todosAdapter = createEntityAdapter();

// Initial state will have entities and ids properties
const initialState = todosAdapter.getInitialState({
  status: 'idle',
  error: null
});

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    todoAdded: todosAdapter.addOne,
    todosLoaded: todosAdapter.setAll
    // Additional actions...
  }
});

Redux DevTools

Redux DevTools is a powerful debugging tool that allows you to:

  • Inspect every state and action payload
  • Go back in time by “cancelling” actions
  • Debug using hot reloading
  • Generate tests

Setting Up Redux DevTools

Install the browser extension from the Chrome Web Store or Firefox Add-ons.

Then, connect your Redux store to the DevTools:

// Without Redux Toolkit
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(thunk))
);

With Redux Toolkit, DevTools are already integrated:

import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

const store = configureStore({
  reducer: rootReducer
  // That's it! DevTools is automatically included
});

Common Redux Patterns and Best Practices

Organizing by Feature

Instead of separating files by type (actions, reducers, selectors), organize by feature:

src/
  features/
    todos/
      todosSlice.js     // Contains actions, reducer, selectors
      TodoList.js       // React component
      AddTodo.js        // React component
    filter/
      filterSlice.js    // Contains actions, reducer, selectors
      FilterButtons.js  // React component
  app/
    store.js           // Redux store setup
  index.js             // App entry point

Action Types as Constants

Define action types as constants to prevent typos:

// actionTypes.js
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';

Redux Toolkit handles this automatically with createSlice.

Using Selectors

Always access state through selector functions:

// Bad
const todos = useSelector(state => state.todos);

// Good
const selectTodos = state => state.todos;
const todos = useSelector(selectTodos);

This makes it easier to change the state shape later.

Avoiding Common Anti-patterns

  1. Mutating state directly:
javascript
// Bad
function reducer(state, action) {
  state.completed = true; // Mutating state
  return state;
}

// Good
function reducer(state, action) {
  return { ...state, completed: true };
}
  1. Putting too much in Redux: Not all state needs to go in Redux. Local component state is often more appropriate for UI state.
  2. Redundant data: Avoid storing derivable data in the Redux store. Use selectors to compute derived data.

Real-World Redux Application Example

Let’s walk through a more complete example of a todo application using Redux Toolkit:

Setting Up the Store

// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from '../features/todos/todosSlice';
import filtersReducer from '../features/filters/filtersSlice';

export const store = configureStore({
  reducer: {
    todos: todosReducer,
    filters: filtersReducer
  }
});

Todo Feature

// features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=10');
  return response.json();
});

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    entities: [],
    status: 'idle',
    error: null
  },
  reducers: {
    todoAdded: (state, action) => {
      state.entities.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      });
    },
    todoToggled: (state, action) => {
      const todo = state.entities.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    todoDeleted: (state, action) => {
      state.entities = state.entities.filter(todo => todo.id !== action.payload);
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.entities = action.payload.map(todo => ({
          id: todo.id,
          text: todo.title,
          completed: todo.completed
        }));
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  }
});

export const { todoAdded, todoToggled, todoDeleted } = todosSlice.actions;

export default todosSlice.reducer;

// Selectors
export const selectAllTodos = state => state.todos.entities;
export const selectTodoById = (state, todoId) =>
  state.todos.entities.find(todo => todo.id === todoId);

Filters Feature

// features/filters/filtersSlice.js
import { createSlice } from '@reduxjs/toolkit';

const filtersSlice = createSlice({
  name: 'filters',
  initialState: {
    status: 'all' // 'all' | 'active' | 'completed'
  },
  reducers: {
    statusFilterChanged: (state, action) => {
      state.status = action.payload;
    }
  }
});

export const { statusFilterChanged } = filtersSlice.actions;
export default filtersSlice.reducer;

// Selector that combines information from multiple slices
export const selectFilteredTodos = state => {
  const todos = state.todos.entities;
  const status = state.filters.status;
 
  if (status === 'all') {
    return todos;
  }
 
  return todos.filter(todo => {
    if (status === 'active') {
      return !todo.completed;
    }
   
    if (status === 'completed') {
      return todo.completed;
    }
   
    return true;
  });
};

React Components

// App.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTodos } from './features/todos/todosSlice';
import TodoList from './features/todos/TodoList';
import AddTodo from './features/todos/AddTodo';
import StatusFilter from './features/filters/StatusFilter';

function App() {
  const dispatch = useDispatch();
  const todosStatus = useSelector(state => state.todos.status);
  const error = useSelector(state => state.todos.error);

  useEffect(() => {
    if (todosStatus === 'idle') {
      dispatch(fetchTodos());
    }
  }, [todosStatus, dispatch]);

  let content;
 
  if (todosStatus === 'loading') {
    content = <div>Loading...</div>;
  } else if (todosStatus === 'succeeded') {
    content = <TodoList />;
  } else if (todosStatus === 'failed') {
    content = <div>Error: {error}</div>;
  }

  return (
    <div className="App">
      <h1>Todo App with Redux Toolkit</h1>
      <AddTodo />
      <StatusFilter />
      {content}
    </div>
  );
}

export default App;

// features/todos/TodoList.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { todoToggled, todoDeleted } from './todosSlice';
import { selectFilteredTodos } from '../filters/filtersSlice';

function TodoList() {
  const filteredTodos = useSelector(selectFilteredTodos);
  const dispatch = useDispatch();

  if (filteredTodos.length === 0) {
    return <div>No todos found</div>;
  }

  return (
    <ul>
      {filteredTodos.map(todo => (
        <li key={todo.id} style={{ marginBottom: '10px' }}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => dispatch(todoToggled(todo.id))}
          />
          <span
            style={{
              marginLeft: '10px',
              textDecoration: todo.completed ? 'line-through' : 'none'
            }}
          >
            {todo.text}
          </span>
          <button
            style={{ marginLeft: '10px' }}
            onClick={() => dispatch(todoDeleted(todo.id))}
          >
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;
// features/filters/StatusFilter.js
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { statusFilterChanged } from './filtersSlice';

function StatusFilter() {
  const dispatch = useDispatch();
  const currentFilter = useSelector(state => state.filters.status);

  const filters = [
    { text: 'All', value: 'all' },
    { text: 'Active', value: 'active' },
    { text: 'Completed', value: 'completed' }
  ];

  return (
    <div>
      <h3>Filter by Status:</h3>
      <div>
        {filters.map(filter => (
          <button
            key={filter.value}
            onClick={() => dispatch(statusFilterChanged(filter.value))}
            style={{
              marginRight: '5px',
              fontWeight: currentFilter === filter.value ? 'bold' : 'normal'
            }}
          >
            {filter.text}
          </button>
        ))}
      </div>
    </div>
  );
}

export default StatusFilter;

Summary

In this chapter, we covered Redux, a powerful state management library for React applications. Here’s a recap of what we learned:

  • Core Redux Concepts: Store, actions, reducers, and the data flow
  • Setting up Redux: How to integrate Redux with React applications
  • Redux Middleware: How to handle side effects and async operations
  • Redux Toolkit: The modern way to write Redux with less boilerplate
  • Performance Optimizations: Memoization and normalization techniques
  • Best Practices: Organization patterns and avoiding common mistakes

Redux provides a predictable state container that can make your application more maintainable as it grows. However, remember to evaluate whether Redux is necessary for your specific use case, as it adds complexity that may not always be warranted.

Exercises

  1. Create a simple counter application using Redux Toolkit
  2. Build a book tracking app with Redux that lets users add, remove, and mark books as read
  3. Implement an authentication flow with Redux, including login, logout, and protected routes
  4. Add Redux DevTools to an existing application and practice time-travel debugging
  5. Refactor a React application that uses Context API to use Redux Toolkit instead
  6. Implement an asynchronous data fetching pattern using Redux Thunk or createAsyncThunk
  7. Create a normalized state structure for a complex application with related entities

Further Reading and Resources

Official Documentation

Books

  • “Redux in Action” by Marc Garreau and Will Faurot
  • “Learning Redux” by Daniel Bugl

Related Articles

Tools

Advanced Concepts

By mastering Redux, you’ve added a powerful tool to your React developer toolkit that will help you manage complex application state with confidence and predictability.

Scroll to Top