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
- Store: The single source of truth that holds the application state
- Actions: Plain objects that describe what happened
- Reducers: Pure functions that specify how the state changes in response to actions
- Dispatch: The only way to trigger a state change
Three Principles
- Single source of truth: The global state is stored in a single store
- State is read-only: The only way to change state is to emit an action
- 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
- Use Redux Toolkit: It reduces boilerplate and includes useful utilities
- Normalize State Shape: Structure your state like a database
- Keep State Minimal: Only put data in Redux that’s truly global
- Use Selectors: Create reusable functions to extract data from state
- 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.