Havent used React-Redux in awhile? Forgot how it works? This guide will get you upto speed on how it flows in the shortest most plain simple approach possible
Install
yarn add redux react-redux
Redux Flow
- A UI Element calls an Action Creator (similar to setState)
- Action Creator calls the right Action(s) (Action Creator is just an organizational pattern)
- Action(s) dispatches to the Reducer (Abstracted within Redux)
- Reducer updates the Store (similar to State)
- Store change is reflected in all UI Elements (similar to useState)
Code Patterns
UI Element
import { depositMoney, withdrawMoney, } from "./state/action-creators/actionCreators"; const SomeComponent = () => { const dispatch = useDispatch(); const account = useSelector((state) => state.account); // Gets the desired state value const depositMoneyAction = bindActionCreators(depositMoney, dispatch); const withdrawMoneyAction = bindActionCreators(withdrawMoney, dispatch); return ( <Component> <h1>{account}</h1> <button onClick={() => depositMoneyAction(1000)}>Deposit 1000</button> <button onClick={() => withdrawMoneyAction(1000)}>Withdraw 1000</button> </Component> ) }
Action Creator & Actions
Contains two bits of info, type and payload
Type is what the Reducer needs to know what function to run, I also hate that it just a string. This is where ‘following’ redux is painful but its the only real hurdle
The ACTION is the string and nothing more
// /state/actionCreators/actionCreators.js export const depositMoney = (amount) => { return { type: "deposit", // Action payload: amount, }; }; export const withdrawMoney = (amount) => { return { type: "withdraw", // Action payload: amount, }; };
Reducer
From here, everything goes into the magic box that is redux, basically, don’t worry about how we went from the action to here, it’s all abstracted with that string value ‘deposit’ or ‘withdraw’ with the previous type
// state/reducers/reducers.js import accountReducer from "./accountReducer"; import fooReducer from "./fooReducer"; import barReducer from "./barReducer"; const reducers = combineReducers({ account: accountReducer, foo: fooReducer, bar: barReducer });
// state/reducers/accountReducer.js const reducer = (state = 0, action) => { switch (action.type) { case "deposit": return state + action.payload; case "withdraw": return state - action.payload; default: return state; } }; export default reducer; // state = 0 is the same as the value you'd pass into useState(<here>)
Store
If the context provider is state and is stored at a high-level componenent like App.tsx, then Redux Store does the same thing but stored in isolation from the components
We simply instantiate it, then pass in all reducers so when redux gets that string key (eg ”deposit”/”withdraw”) it kicks into its logic
// state/store.js import { createStore } from "redux"; import reducers from "./reducers/reducers"; export const store = createStore(reducers, {});
Ducks Pattern
A great pattern to co-locate the action creator, action type and reducer together with some simple rules:
A Module..
- MUST export a default function called reducer()
- MUST export it’s action creators as functions
- MUST have action types in the casing
ACTION_TYPE
Ducks Template
Converted from previous section
// banking.duck.js // Actions types const DEPOSIT = "deposit"; const WITHDRAW = "withdraw"; // Reducer export default function reducer(state = 0, action) { // Default State switch (action.type) { case "deposit": return state + action.payload; case "withdraw": return state - action.payload; default: return state; } } // Action Creators export const depositMoney = (amount) => { return { type: DEPOSIT, payload: amount, }; }; export const withdrawMoney = (amount) => { return { type: WITHDRAW, payload: amount, }; };
// Paired with the same UI Component As Before import { depositMoney, withdrawMoney, } from "./state/action-creators/actionCreators"; const SomeComponent = () => { const dispatch = useDispatch(); const account = useSelector((state) => state.account); // Gets the desired state value const depositMoneyAction = bindActionCreators(depositMoney, dispatch); const withdrawMoneyAction = bindActionCreators(withdrawMoney, dispatch); return ( <Component> <h1>{account}</h1> <button onClick={() => depositMoneyAction(1000)}>Deposit 1000</button> <button onClick={() => withdrawMoneyAction(1000)}>Withdraw 1000</button> </Component> ) }
Middleware
The above is great for merely updating a store value but more often you’ll want to trigger a function. Why? Delayed actions or asynchronous code like GET requests.
For some reason redux can’t do this by default.
To allow this behaviour we add middleware, specifically an npm module called redux-thunk
Install
yarn add redux-thunk
Update the store with the enhancer
// state/store.js import { legacy_createStore as createStore, applyMiddleware, compose, } from "redux"; import reducers from "./reducers/reducers"; import thunk from "redux-thunk"; const middlewares = [thunk]; const enhancer = compose(applyMiddleware(...middlewares)); export const store = createStore(reducers, {}, enhancer);
Unit Testing
// state/duck.test.js import reducer, { depositMoney, withdrawMoney } from "./ducks"; it("deposits money", () => { const updatedState = reducer(, depositMoney(2000)); // No state override expect(updatedState).toEqual(7000); }); it("withdraws money", () => { const initialState = 5000; const updatedState = reducer(initialState, withdrawMoney(2000)); expect(updatedState).toEqual(3000); });