Yuan's Blog

Redux

Redux Fundamentals

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

Core Concepts

  1. Store: The single source of truth that holds the application state
  2. Actions: Plain objects that describe what happened
  3. Reducers: Pure functions that specify how the state changes in response to actions
  4. Dispatch: The only way to trigger a state change

Three Principles

  1. Single source of truth: The global state is stored in a single store
  2. State is read-only: The only way to change state is to emit an action
  3. Changes are made with pure functions: Reducers are pure functions that take the previous state and an action, and return the next state

Setting Up Redux

Installation

npm install redux react-redux @reduxjs/toolkit

Basic Store Setup

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

Actions and Action Creators

Actions are payloads of information that send data from your application to your store.

// Action types
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'

// Action creators
const increment = () => ({
  type: INCREMENT,
})

const decrement = () => ({
  type: DECREMENT,
})

const incrementByAmount = (amount) => ({
  type: 'INCREMENT_BY_AMOUNT',
  payload: amount,
})

Reducers

Reducers specify how the application’s state changes in response to actions.

const initialState = {
  value: 0,
}

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

Redux Toolkit (RTK)

Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.

Creating a Slice

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

Connecting React Components

Using React-Redux Hooks

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, incrementByAmount } from './counterSlice'

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

  return (
    <div>
      <button onClick={() => dispatch(increment())}>+</button>
      <span>{count}</span>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
    </div>
  )
}

Provider Setup

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

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

Async Operations with Redux Thunk

Thunk Action Creator

import { createAsyncThunk } from '@reduxjs/toolkit'

export const fetchUserById = createAsyncThunk('users/fetchById', async (userId) => {
  const response = await fetch(`/api/users/${userId}`)
  return response.json()
})

Handling Async Actions in Slice

const usersSlice = createSlice({
  name: 'users',
  initialState: {
    entities: [],
    loading: 'idle',
  },
  reducers: {
    // standard reducer logic
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.loading = 'pending'
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.loading = 'idle'
        state.entities.push(action.payload)
      })
      .addCase(fetchUserById.rejected, (state) => {
        state.loading = 'idle'
      })
  },
})

Best Practices

  1. Use Redux Toolkit: It reduces boilerplate and includes useful utilities
  2. Normalize State Shape: Structure your state like a database
  3. Keep State Minimal: Only put data in Redux that’s truly global
  4. Use Selectors: Create reusable functions to extract data from state
  5. Split Reducers: Use combineReducers to manage different parts of state

When to Use Redux

Redux is most useful when:

  • You have large amounts of application state
  • The state is updated frequently
  • The logic to update state may be complex
  • The app has a medium or large-sized codebase
  • Multiple developers are working on the application

Common Patterns

Selector Functions

// Selectors
const selectCounter = (state) => state.counter.value
const selectCounterByAmount = (state, amount) => state.counter.value * amount

Middleware

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

Redux provides a predictable way to manage state in complex applications, making debugging easier and enabling powerful developer tools.