logo
eng-flag

Redux Cheatsheet

Table of Contents

  1. Core Concepts
  2. Actions
  3. Reducers
  4. Store
  5. Middleware
  6. React-Redux
  7. Redux Toolkit
  8. Redux Toolkit Example
  9. Best Practices
  10. Advanced Concepts

Core Concepts

Redux is a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments, and are easy to test.

The three core principles of Redux:

  1. Single source of truth
  2. State is read-only
  3. Changes are made with pure functions

Actions

Actions are plain JavaScript objects that have a type field. They describe what happened in the app.

Example:

const addTodo = (text) => {
  return {
    type: 'ADD_TODO',
    payload: text
  }
}

Reducers

Reducers specify how the application's state changes in response to actions. They are pure functions that take the previous state and an action, and return the next state.

Example:

const initialState = {
  todos: []
}

function todoReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      }
    default:
      return state
  }
}

Store

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

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

Example:

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

const store = createStore(todoReducer)

Middleware

Middleware provides a third-party extension point between dispatching an action and the moment it reaches the reducer. It's commonly used for logging, crash reporting, performing asynchronous tasks, etc.

Example (Logger Middleware):

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const store = createStore(
  todoReducer,
  applyMiddleware(logger)
)

React-Redux

React-Redux is the official React binding for Redux. It lets your React components read data from a Redux store, and dispatch actions to the store to update data.

Key concepts:

  • Provider: Makes the Redux store available to the rest of your app
  • connect(): Connects a React component to the Redux store
  • useSelector(): Hook to extract data from the Redux store state
  • useDispatch(): Hook to dispatch actions

Example:

import { Provider } from 'react-redux'
import store from './store'

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

// In a component file
import { useSelector, useDispatch } from 'react-redux'

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

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

Redux Toolkit

Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development. It includes utilities to simplify common use cases like store setup, creating reducers, immutable update logic, and more.

Key features:

  • configureStore(): Simplified store setup
  • createSlice(): Combines reducer logic and actions
  • createAsyncThunk: Simplified async logic

Example:

import { configureStore, createSlice } from '@reduxjs/toolkit'

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      state.push(action.payload)
    }
  }
})

export const { addTodo } = todosSlice.actions

const store = configureStore({
  reducer: {
    todos: todosSlice.reducer
  }
})

Redux Toolkit Shopping Cart Example

This example demonstrates how to use Redux Toolkit to manage the state of a simple shopping cart application.

Setup

First, let's set up our project and install necessary dependencies:

npm init -y
npm install @reduxjs/toolkit react-redux react react-dom

File Structure

Create the following file structure:

src/
  features/
    cart/
      cartSlice.js
    products/
      productsSlice.js
  app/
    store.js
  App.js
  index.js

Implementing the Store

Let's start by creating our store using Redux Toolkit's configureStore.

File: src/app/store.js

import { configureStore } from '@reduxjs/toolkit'
import cartReducer from '../features/cart/cartSlice'
import productsReducer from '../features/products/productsSlice'

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    products: productsReducer,
  },
})

Creating Slices

Now, let's create our slices for managing the cart and products state.

File: src/features/cart/cartSlice.js

import { createSlice } from '@reduxjs/toolkit'

const cartSlice = createSlice({
  name: 'cart',
  initialState: [],
  reducers: {
    addToCart: (state, action) => {
      const itemExists = state.find((item) => item.id === action.payload.id)
      if (itemExists) {
        itemExists.quantity++
      } else {
        state.push({ ...action.payload, quantity: 1 })
      }
    },
    incrementQuantity: (state, action) => {
      const item = state.find((item) => item.id === action.payload)
      item.quantity++
    },
    decrementQuantity: (state, action) => {
      const item = state.find((item) => item.id === action.payload)
      if (item.quantity === 1) {
        const index = state.findIndex((item) => item.id === action.payload)
        state.splice(index, 1)
      } else {
        item.quantity--
      }
    },
    removeFromCart: (state, action) => {
      const index = state.findIndex((item) => item.id === action.payload)
      state.splice(index, 1)
    },
  },
})

export const { addToCart, incrementQuantity, decrementQuantity, removeFromCart } = cartSlice.actions
//Exports the reducer functions inside cartSlice.actions. This allows us to use these functions with dispatch in components.
export default cartSlice.reducer
//Exports the cartSlice.reducer. This allows us to add this reducer to the Redux store.

File: src/features/products/productsSlice.js

import { createSlice } from '@reduxjs/toolkit'

const productsSlice = createSlice({
  name: 'products',
  initialState: [
    { id: 1, name: 'iPhone 12', price: 999 },
    { id: 2, name: 'AirPods Pro', price: 249 },
    { id: 3, name: 'MacBook Air', price: 999 },
    { id: 4, name: 'iPad Pro', price: 799 },
  ],
  reducers: {},
})

export default productsSlice.reducer

Creating React Components

Now let's create our React components to display the products and cart.

File: src/App.js

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { addToCart, incrementQuantity, decrementQuantity, removeFromCart } from './features/cart/cartSlice'

function App() {
  const dispatch = useDispatch()
  const products = useSelector((state) => state.products)
  const cart = useSelector((state) => state.cart)

  return (
    <div>
      <h1>Shopping Cart Example</h1>
      <h2>Products</h2>
      {products.map((product) => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <p>${product.price}</p>
          <button onClick={() => dispatch(addToCart(product))}>Add to Cart</button>
        </div>
      ))}
      <h2>Cart</h2>
      {cart.map((item) => (
        <div key={item.id}>
          <h3>{item.name}</h3>
          <p>
            ${item.price} x {item.quantity} = ${item.price * item.quantity}
          </p>
          <button onClick={() => dispatch(incrementQuantity(item.id))}>+</button>
          <button onClick={() => dispatch(decrementQuantity(item.id))}>-</button>
          <button onClick={() => dispatch(removeFromCart(item.id))}>Remove</button>
        </div>
      ))}
      <h3>Total: ${cart.reduce((total, item) => total + item.price * item.quantity, 0)}</h3>
    </div>
  )
}

export default App

Setting up the Entry Point

Finally, let's set up our entry point to render the app with Redux.

File: src/index.js

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

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

Explanation

This example demonstrates several key features of Redux Toolkit:

  1. configureStore: We use this to set up our Redux store with minimal configuration. It automatically sets up the Redux DevTools extension and adds some middleware like redux-thunk.

  2. createSlice: This powerful function generates action creators and action types automatically. We use it to define our cart and products slices.

  3. Immer Integration: Redux Toolkit uses Immer internally, allowing us to write "mutating" logic in our reducers that actually results in immutable updates.

  4. React-Redux Hooks: We use useSelector to access state and useDispatch to dispatch actions in our React components.

This shopping cart example shows how to:

  • Display a list of products
  • Add products to the cart
  • Increment and decrement quantities in the cart
  • Remove items from the cart
  • Calculate the total price

Best Practices

  1. Use Redux Toolkit for new projects
  2. Keep your state normalized
  3. Use selector functions to read from store
  4. Use action creators to create actions
  5. Use meaningful action types
  6. Keep reducers pure and simple
  7. Use immutable update patterns in reducers
  8. Use middleware for side effects
  9. Use Redux DevTools for debugging

Advanced Concepts

Selector Memoization

Use reselect to create memoized selector functions for better performance.

import { createSelector } from 'reselect'

const getTodos = state => state.todos
const getFilter = state => state.filter

const getVisibleTodos = createSelector(
  [getTodos, getFilter],
  (todos, filter) => {
    switch (filter) {
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed)
      default:
        return todos
    }
  }
)

Async Actions with Redux Thunk

Redux Thunk middleware allows you to write action creators that return a function instead of an action.

import { createAsyncThunk } from '@reduxjs/toolkit'

export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async () => {
    const response = await fetch('/api/todos')
    return response.json()
  }
)

// In your slice
extraReducers: (builder) => {
  builder
    .addCase(fetchTodos.pending, (state) => {
      state.status = 'loading'
    })
    .addCase(fetchTodos.fulfilled, (state, action) => {
      state.status = 'succeeded'
      state.todos = action.payload
    })
    .addCase(fetchTodos.rejected, (state, action) => {
      state.status = 'failed'
      state.error = action.error.message
    })
}

Async Actions with Redux Thunk: User Authentication Scenario

This example demonstrates how to use Redux Thunk for handling asynchronous actions in a user authentication scenario.

Scenario

We have a web application that requires user authentication. We'll implement a login feature using Redux Thunk to handle the asynchronous API call for user login.

Implementation

1. Create the Async Thunk

First, we'll create an async thunk for the login action:

import { createAsyncThunk } from '@reduxjs/toolkit'

export const loginUser = createAsyncThunk(
  'auth/loginUser',
  async (credentials, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      })
      
      if (!response.ok) {
        throw new Error('Login failed')
      }
      
      const data = await response.json()
      return data
    } catch (error) {
      return rejectWithValue(error.message)
    }
  }
)

2. Set up the Auth Slice

Next, we'll create an auth slice to manage the authentication state:

import { createSlice } from '@reduxjs/toolkit'
import { loginUser } from './authThunks'

const authSlice = createSlice({
  name: 'auth',
  initialState: {
    user: null,
    status: 'idle',
    error: null
  },
  reducers: {
    logout: (state) => {
      state.user = null
      state.status = 'idle'
      state.error = null
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginUser.pending, (state) => {
        state.status = 'loading'
      })
      .addCase(loginUser.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.user = action.payload
        state.error = null
      })
      .addCase(loginUser.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.payload
      })
  }
})

export const { logout } = authSlice.actions
export default authSlice.reducer

3. Use in a Component

Finally, we can use the loginUser thunk in a React component:

import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { loginUser } from './authThunks'

const LoginForm = () => {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
  const dispatch = useDispatch()
  const { status, error } = useSelector(state => state.auth)

  const handleSubmit = (e) => {
    e.preventDefault()
    dispatch(loginUser({ username, password }))
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Username"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Logging in...' : 'Login'}
      </button>
      {error && <p>{error}</p>}
    </form>
  )
}

export default LoginForm

Explanation

We create an async thunk loginUser that handles the API call for user login. The auth slice manages the authentication state, including pending, fulfilled, and rejected states of the login action. In the React component, we dispatch the loginUser thunk when the form is submitted. The component also displays loading state and any error messages.

Normalized State Shape

For complex applications, consider normalizing your state shape:

{
  entities: {
    todos: {
      byId: {
        1: { id: 1, text: 'Buy milk', completed: false },
        2: { id: 2, text: 'Walk the dog', completed: true }
      },
      allIds: [1, 2]
    },
    users: {
      byId: {
        1: { id: 1, name: 'John Doe' },
        2: { id: 2, name: 'Jane Smith' }
      },
      allIds: [1, 2]
    }
  }
}

Testing Redux

  1. Test action creators
  2. Test reducers
  3. Test selectors
  4. Use Jest for unit testing
  5. Use Redux Mock Store for integration testing

Example (Testing a reducer):

import todoReducer from './todoReducer'
import { addTodo } from './todoActions'

describe('todo reducer', () => {
  it('should handle ADD_TODO', () => {
    const initialState = []
    const action = addTodo('Learn Redux')
    const nextState = todoReducer(initialState, action)

    expect(nextState).toEqual([
      { text: 'Learn Redux', completed: false }
    ])
  })
})

2024 © All rights reserved - buraxta.com