React.js 09: Redux
Course improvement: need side-by-side code comparison, wo Redux vs w Redux w w Redux RTK
Docs
https://redux.js.org/
https://github.com/erikras/ducks-modular-redux
https://immerjs.github.io/immer/
https://redux-toolkit.js.org/api/createAsyncThunk
Common Imports
import { createStore, combineReducers } from 'redux';
import { applyMiddleware } from 'redux';
import { createSlice , configureStore } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';
import { createAsyncThunk } from '@reduxjs/toolkit';
Problem Statement
With plain React, the three parts state-view-actions overlap quite a bit.
We would like a more elegant solution that separate those concerns.
Redux just does that.
Pitch
Redux an alternative to Context API.
Redux is a library for managing and updating application state based on the Flux architecture.
Redux is not limited to React. It can be used within the context of any UI framework whether React, Angular, jQuery or else.
More:
Complex apps have a multitude of states to keep track of, and passing states down the component tree can become tedious and inefficient.
Redux, as a valuable tool, enhances JavaScript frameworks and libraries by offering a consistent and predictable solution for state management.
Redux: Install
npm install --save-dev @testing-library/react
npm install --save-dev @testing-library/jest-dom
npm install --save-dev @testing-library/user-event
p.s. create-react-app
by default includes the three libraries above RTL
, jest-dom
, and user-event
(but also jest
)
npm install @reduxjs/toolkit
Redux: Watch Mode
npm test
Launches the test in watch mode, allowing the test to re-run every time the file is saved!
Type q
in the terminal to quit out of the watch mode.
Redux: Watch Mode
Customize terminal output by RTL
npm test -- --coverage
// --coverage : Indicates that test coverage information should be collected and reported in the output
npm test -- --silent
// --silent : Prevents tests from printing messages through the console.
npm test -- --help
// --help : Displays help
Redux: Best Practices
State
– the current data used in the appView
– the user interface displayed to usersActions
– events that a user can take to change the state
Redux expects reducers to produce the same output given the same state and action
Store
→ View
→ Actions
→ Store
Redux: Core Principles
Pure Functions — always have the same outputs given the same inputs
Immutable Updates
A reducer must be a pure function, and it must update the state immutably.
Reducer: Principles
A reducer is a function
that determines the application’s next state
given a current state
and a specific action
.
If no state
is provided, it returns a default initial state
.
If the action
is not recognized, it returns the current state
.
Reducers should only calculate the new
state
value based on two arguments:state
andaction
Reducers are not allowed to modify the existing
state
— but copy the existingstate
and make changes to the copied values.Reducers must not do any asynchronous logic or other “side effects” — pull these outside the reducer functions
Redux: Principles
A container that holds and manages your application’s global state
.
The store is a container for state
, it provides a way to dispatch actions
, and it calls the reducer when actions
are dispatched.
Typically there is only one store
in a Redux application.
Redux : Immutable Updates
Plain strings
, numbers
, and booleans
are immutable in JavaScript, so we can just return them without making a copy
When it is an object
, you must copy the contents of the argument obj into a new object
and when required you update some property()
of the new object
.
You usually to that with the spread annotation ({...obj}
) followed by the necessary updates that either add or override a property.
// adds a property case 'songs/addGlobalSong': { return [...state, action.payload]; } // overrides a property return { ...obj, completed: !obj.completed }
Redux:
In Redux, actions are represented as plain JS objects. Example:
const action = { type: 'todos/addTodo', payload: 'Take selfies' };
The action.payload
property is used to hold additional data that the reducer might need to carry out a given action. The name payload is simply a convention, and its value can be anything!
MUST: type property — a
string
value. Describes the action.COULD: payload property — an
object
value. Includes info related to the actionWhen an
action
is generated and notifies other parts of the application, we say that theaction
is dispatched
Actions in Redux represent specific events that occur.
Redux: Store
Store
Redux is a state-management library centered around a single, powerful object called the store
.
This one object is responsible for holding the entire application’s state
, receiving action
objects and then executing state
changes based on the type of the action
received, and informing (executing) listener functions when such changes occur.
Redux: Reducer
Reducer composition is a design pattern for managing a Redux store
with multiple slices.
The root
reducer delegates actions to slice
reducers that are responsible for updating only their assigned slice
of the store
’s state.
The root
reducer then reassembles the slices
into a new state
object
.
Redux: Slice
Slice
const state = { songs: ['Claire De Lune', 'Garota de Ipanema', 'We Will Rock You'], playMode: 'SHUFFLE' }
Given this state
object for a playlist application, "Slice"
is the Redux-specific term describes state.songs
and state.playMode
Property is the general term to describe the values of an object
, and Slice
is Redux specific
Redux: Slice
Advanced
Slice
AdvancedA “slice” of state is a segment of the global state that focuses on a particular feature.
It encompasses the related data, along with its associated reducers, actions, and selectors.
Think of it as a self-contained unit dedicated to managing a specific part of your application’s functionality.
For each slice of the state, we usually define a corresponding reducer. These are known as “slice reducers.”
Each reducer is akin to a worker solely responsible for managing the state of its respective slice.
This modular approach simplifies complex applications and makes debugging a breeze.
Redux: RTK
Redux Toolkit (RTK) contains packages
and functions
that build in suggested best practices, simplify most Redux tasks, prevent common mistakes, and make it easier to write Redux applications.
Redux: Reducer
const reducer = (state = initialState, action) => { switch (action.type) { case 'songs/addSong': { return [ ...state, action.payload]; } case 'songs/removeSong': { return state.filter(song => song !== action.payload); } case 'todos/removeAll': { return []; } default: { return state; } } }
Redux: API
import { createStore } from 'redux'
store.getState()
// Returns the current value of the store’s state
store.dispatch(action)
// Executes store’s reducer function with the action object
store.subscribe(listener)
// Executes listener function each time a change to the store's state occurs
subscribe()
returns an unsubscribe function // to stop the listener from responding to changes to the store
Redux API: Example
// Create Store import { createStore } from 'redux' const initialState = 'on'; const lightSwitchReducer = (state = initialState, action) => { switch (action.type) { case 'toggle': return state === 'on' ? 'off' : 'on'; default: return state; } } const store = createStore(lightSwitchReducer);
Redux API: Example II
import { createStore } from 'redux'; const countReducer = (state = 0, action) => { switch (action.type) { case 'increment': return state + 1; case 'decrement': return state - 1; default: return state; } } const store = createStore(countReducer); const render = () => { document.getElementById('count').text = store.getState(); // Display the current state. } render(); // Render once with the initial state of 0. store.subscribe(render); // Re-render on state changes. document.getElementById('plusButton').addEventListener('click', () => { store.dispatch( { type : 'increment' } ); // Request a state change. store.dispatch( { type : 'increment' } ); console.log( store.getState() ); store.dispatch( { type : 'decrement' } ); store.dispatch( { type : 'decrement' } ); store.dispatch( { type : 'decrement' } ); console.log( store.getState() ); });
Redux API: Subscribe and Unsubscribe
// lightSwitchReducer(), toggle(), and store omitted... const reactToChange = () => { console.log(`The light was switched ${store.getState()}!`); } const unsubscribe = store.subscribe(reactToChange); store.dispatch(toggle()); // reactToChange() is called, printing: // 'The light was switched off!' store.dispatch(toggle()); // reactToChange() is called, printing: // 'The light was switched on!' unsubscribe(); // reactToChange() is now unsubscribed store.dispatch(toggle()); // no print statement! console.log(store.getState()); // Prints 'off'
Redux API: Subscribe and Unsubscribe
App.js
import React from "react"; import { increment, decrement } from "./store"; function App({ state, dispatch}) { const incrementerClicked = () => { // Dispatch increment action dispatch( increment() ); } const decrementerClicked = () => { // Dispatch decrement action dispatch( decrement() ); } return( <main> <p id='counter'>{state}</p> <button id='incrementer' onClick={incrementerClicked}>+</button> <button id='decrementer' onClick={decrementerClicked}>-</button> </main> ) } export default App;
index.js
import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App.js" import { store } from "./store.js" const root = createRoot(document.getElementById("app")); // Store State Change Listener const render = () => { root.render(<App state={store.getState()} dispatch={store.dispatch}/>); } render(); store.subscribe(render); // Subscribe to state changes
Store.js
import { createStore } from 'redux'; // Action Creators export function increment() { return {type: 'increment'} } export function decrement() { return {type: 'decrement'} } // Reducer / Store const initialState = 0; const countReducer = (state = initialState, action) => { switch (action.type) { case 'increment': return state + 1; case 'decrement': return state - 1; default: return state; } }; export const store = createStore(countReducer);
Redux:
Fun stuff with updating state slices
case 'cart/changeItemQuantity': { const { name, newQuantity } = action.payload; const itemToUpdate = cart[name]; const updatedItem = { ...itemToUpdate, quantity : newQuantity } return { ...cart, [name] : updatedItem // I first thought it'd be cart[name] but I was incorrect }; }
Redux:
Middleware is the code that runs in the middle—usually between a framework receiving a request and producing a response.
Middleware is a powerful tool for extending, modifying, or customizing a framework or library’s default behavior to meet an application’s specific needs.
Middlewares must conform to a specific, nested function structure in order to work as part of the pipeline.
This nested structure is also called a higher-order function.
callback function
vs higher-order function
callback function
vs higher-order function
A higher-order function is a function that takes another function(s) as an argument(s) and/or returns a function to its callers.
A callback function is a function that is passed to another function with the expectation that the other function will call it.
So a callback is not necessarily itself a higher-order function, but a function which receives a callback as an argument is
thunk
vs higher-order function
thunk
vs higher-order function
Thunk is a function that returns another function.
Thunks may accept functions as arguments, but that’s not a requirement.
Redux Toolkit and thunk middleware
Redux Toolkit provides configureStore, which returns a store that applies a thunk middleware by default.
Redux: Middleware createAsyncThunk()
createAsyncThunk()
import { createAsyncThunk } from '@reduxjs/toolkit' import { fetchUser } from './api' const fetchUserById = createAsyncThunk( 'users/fetchUserById', // action type async (userId, thunkAPI) => { // payload creator const response = await fetchUser(userId); return response.json(); } )
Last updated