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:
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 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
}
}
The store is the object that brings actions and reducers together. It has the following responsibilities:
getState()
dispatch(action)
subscribe(listener)
Example:
import { createStore } from 'redux'
import todoReducer from './reducers'
const store = createStore(todoReducer)
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 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 appconnect()
: Connects a React component to the Redux storeuseSelector()
: Hook to extract data from the Redux store stateuseDispatch()
: Hook to dispatch actionsExample:
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 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 setupcreateSlice()
: Combines reducer logic and actionscreateAsyncThunk
: Simplified async logicExample:
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
}
})
This example demonstrates how to use Redux Toolkit to manage the state of a simple shopping cart application.
First, let's set up our project and install necessary dependencies:
npm init -y
npm install @reduxjs/toolkit react-redux react react-dom
Create the following file structure:
src/
features/
cart/
cartSlice.js
products/
productsSlice.js
app/
store.js
App.js
index.js
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,
},
})
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
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
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')
)
This example demonstrates several key features of Redux Toolkit:
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
.
createSlice
: This powerful function generates action creators and action types automatically. We use it to define our cart and products slices.
Immer Integration: Redux Toolkit uses Immer internally, allowing us to write "mutating" logic in our reducers that actually results in immutable updates.
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:
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
}
}
)
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
})
}
This example demonstrates how to use Redux Thunk for handling asynchronous actions in a user authentication 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.
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)
}
}
)
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
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
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.
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]
}
}
}
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