React 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
statevalue based on two arguments:stateandactionReducers are not allowed to modify the existing
state— but copy the existingstateand 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
stringvalue. Describes the action.COULD: payload property — an
objectvalue. Includes info related to the actionWhen an
actionis generated and notifies other parts of the application, we say that theactionis dispatched
Actions in Redux represent specific events that occur.
Redux: Store
StoreRedux 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
Sliceconst 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 functionA 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 functionThunk 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