An Overview of State Management using Redux
Individual components (Pages) import actions, reducers, store, and react-redux functions to reference and update states!
store/store.js
: Where we create the store that we want to managecreateStore
function - function to create the store and put it into the variable. argument is the representative combined reducer function we want to use to update the stores) index.js
: where we render the App Provider
component - makes the redux store available to any nested components that need to access the redux store //store.js - createStore, composeEnhancers
import { compose, createStore, applyMiddleware } from "redux";
import rootReducer from '../reducers/index';
import thunk from "redux-thunk";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
//index.js - wrap in Provider tag
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store/store';
import { Provider } from 'react-redux';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
composeEnhancers
- Redux devtools extension - to use with chrome devtools (makes it easier to test & debug?)Redux-thunk
- Redux Middleware, code that lets us intercept redux actions before they reach the reducer In Redux, action creators are expected to return objects. However, using Redux Thunk allows us to pass functions within our action creators to create an asynchronous Redux.
... This means that Redux Thunk can be used to make API requests, delay a dispatch, or set dispatch conditions. Essentially, it provides full control over the dispatch method. (link)
In our code below, the notification dispatch actions are delayed with setTimeout to be triggered after other actions have already been completed.
actions/index.js
- where we create actions for dispatching to reducersaction objects generally contain two types of keys: type, and payload (it can contain as many keys as we want, but must contain type)
action.type
- a must include. reducer does something different with the payload depending on what action type is receivedaction.payload
- the data passed to the reducer (apparently, calling it payload is just convention and you can name the key whatever you want)// action types
export const ADD_TO_CART = "ADD_TO_CART";
export const REMOVE_FROM_CART = "REMOVE_FROM_CART";
export const SET_QUANTITY = "SET_QUANTITY";
export const NOTIFY = "NOTIFY";
export const ENQUEUE_NOTIFICATION = "ENQUEUE_NOTIFICATION";
export const DEQUEUE_NOTIFICATION = "DEQUEUE_NOTIFICATION";
// actions creator functions
export const addToCart = (itemId) => {
return {
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId
}
}
}
export const removeFromCart = (itemId) => {
return {
type: REMOVE_FROM_CART,
payload: {
itemId
}
}
}
export const setQuantity = (itemId, quantity) => {
return {
type: SET_QUANTITY,
payload: {
quantity: quantity,
itemId
}
}
}
export const notify = (message, dismissTime = 5000) => dispatch => {
const uuid = Math.random()
dispatch(enqueueNotification(message, dismissTime, uuid))
setTimeout(() => {
dispatch(dequeueNotification())
}, dismissTime)
}
export const enqueueNotification = (message, dismissTime, uuid) => {
return {
type: ENQUEUE_NOTIFICATION,
payload: {
message,
dismissTime,
uuid
}
}
}
export const dequeueNotification = () => {
return {
type: DEQUEUE_NOTIFICATION
}
}
reducers/initialState.js
- where we set the initial 'state' object, for use in reducersreducers/index.js
: where the main reducer is defined and linked to the store combineReducers
: to combine multiple reducers into a single reducer which can then be passed to the createStore methodreducers/itemReducer.js
, reducers/notificationReducer.js
) - where we define how we want to be updating the state based on the action payload received //reducers/initialState.js
export const initialState =
{
"items": [
{
"id": 1,
"name": "노른자 분리기",
"img": "../images/egg.png",
"price": 9900
},
//...
],
"cartItems": [
{
"itemId": 1,
"quantity": 1
},
{
"itemId": 5,
"quantity": 7
},
{
"itemId": 2,
"quantity": 3
}
]
}
//reducers/index.js
import { combineReducers } from 'redux';
import itemReducer from './itemReducer';
import notificationReducer from './notificationReducer';
const rootReducer = combineReducers({
itemReducer,
notificationReducer
});
export default rootReducer;
//reducers/itemReducer.js
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index";
import { initialState } from "./initialState";
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:{
return Object.assign({}, state, {
cartItems: [...state.cartItems, action.payload]
})
}
case REMOVE_FROM_CART:{
return Object.assign({}, state, {
cartItems: state.cartItems.filter(el => el.itemId !== action.payload.itemId)
})
}
case SET_QUANTITY:{
let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId);
let obj = { itemId: action.payload.itemId, quantity: action.payload.quantity};
return Object.assign({}, state, {
cartItems: [
...state.cartItems.slice(0, idx),
obj,
...state.cartItems.slice(idx+1)
]
})
}
default:
return state;
}
}
export default itemReducer;
//reducers/notificationReducer.js
import { ENQUEUE_NOTIFICATION, DEQUEUE_NOTIFICATION } from "../actions/index";
import { initialState } from "./initialState";
const notificationReducer = (state = {notifications:[]}, action) => {
switch (action.type) {
case ENQUEUE_NOTIFICATION:
return Object.assign({}, state, {
notifications: [...state.notifications, action.payload]
})
case DEQUEUE_NOTIFICATION:
return Object.assign({}, state, {
notifications: state.notifications.slice(1)
})
default:
return state;
}
}
export default notificationReducer;
Two pages both display and update states based on user interaction:
ItemListContainer
& ShoppingCart
useSelector
Hook - takes a function argument to return the part of the state you wantuseDispatch
Hook - assigns a dispatch function, which delivers the actions to the reduceractions/index.js
to dispatch to reducers //pages/ItemListContainer.js
import React from 'react';
import { addToCart, notify } from '../actions/index';
import { useSelector, useDispatch } from 'react-redux';
import Item from '../components/Item';
function ItemListContainer() {
const state = useSelector(state => state.itemReducer); //state we want to update
const { items, cartItems } = state;
const dispatch = useDispatch(); //dispatch function
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
dispatch(addToCart(item.id));
dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`))
}
else {
dispatch(notify('이미 추가된 상품입니다.'))
}
}
return (
<div id="item-list-container">
<div id="item-list-body">
<div id="item-list-title">쓸모없는 선물 모음</div>
{items.map((item, idx) => <Item item={item} key={idx} handleClick={() => {
handleClick(item)
}} />)}
</div>
</div>
);
}
export default ItemListContainer;
The checkedItems
state is managed on the ShoppingCart
Component level (and not by Redux.) Whenever a checkbox interaction occurs, it triggers a state change, which re-calculates the object total
(price, quantity) used and rendered by OrderSummary
Component. In short, any checkbox interaction triggers a re-rendering of the ShoppingCart
& all its nested components (OrderSummary
included.)
//pages/ShoppingCart.js
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { removeFromCart, setQuantity } from '../actions';
import CartItem from '../components/CartItem'
import OrderSummary from '../components/OrderSummary'
export default function ShoppingCart() {
const state = useSelector(state => state.itemReducer);
const { cartItems, items } = state;
const dispatch = useDispatch();
const [checkedItems, setCheckedItems] = useState(cartItems.map((el) => el.itemId))
const handleCheckChange = (checked, id) => {
if (checked) {
setCheckedItems([...checkedItems, id]);
}
else {
setCheckedItems(checkedItems.filter((el) => el !== id));
}
};
const handleAllCheck = (checked) => {
if (checked) {
setCheckedItems(cartItems.map((el) => el.itemId))
}
else {
setCheckedItems([]);
}
};
const handleQuantityChange = (quantity, itemId) => {
dispatch(setQuantity(itemId, quantity))
}
const handleDelete = (itemId) => {
setCheckedItems(checkedItems.filter((el) => el !== itemId));
dispatch(removeFromCart(itemId));
}
const getTotal = () => {
let cartIdArr = cartItems.map((el) => el.itemId)
let total = {
price: 0,
quantity: 0,
}
for (let i = 0; i < cartIdArr.length; i++) {
if (checkedItems.indexOf(cartIdArr[i]) > -1) {
let quantity = cartItems[i].quantity
let price = items.filter((el) => el.id === cartItems[i].itemId)[0].price
total.price = total.price + quantity * price
total.quantity = total.quantity + quantity
}
}
return total
}
const renderItems = items.filter((el) => cartItems.map((el) => el.itemId).indexOf(el.id) > -1)
const total = getTotal()
return (
<div id="item-list-container">
<div id="item-list-body">
<div id="item-list-title">장바구니</div>
<span id="shopping-cart-select-all">
<input
type="checkbox"
checked={
checkedItems.length === cartItems.length ? true : false
}
onChange={(e) => handleAllCheck(e.target.checked)} >
</input>
<label >전체선택</label>
</span>
<div id="shopping-cart-container">
{!cartItems.length ? (
<div id="item-list-text">
장바구니에 아이템이 없습니다.
</div>
) : (
<div id="cart-item-list">
{renderItems.map((item, idx) => {
const quantity = cartItems.filter(el => el.itemId === item.id)[0].quantity
return <CartItem
key={idx}
handleCheckChange={handleCheckChange}
handleQuantityChange={handleQuantityChange}
handleDelete={handleDelete}
item={item}
checkedItems={checkedItems}
quantity={quantity}
/>
})}
</div>
)}
<OrderSummary total={total.price} totalQty={total.quantity} />
</div>
</div >
</div>
)
}
The SET_QUANTITY
action type is supposed to change the quantity of an existing item in in cartItems.
//Using slice() and findIndex
case SET_QUANTITY:{
let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId);
let obj = { itemId: action.payload.itemId, quantity: action.payload.quantity};
return Object.assign({}, state, {
cartItems: [
...state.cartItems.slice(0, idx),
obj,
...state.cartItems.slice(idx+1)
]
})
}
//Copy->mutate->assign
case SET_QUANTITY:{
let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId);
const arr = [...state.cartItems];
arr[idx].quantity = action.payload.quantity;
return Object.assign({}, state, cartItems: arr})
}
//OR
case SET_QUANTITY:{
let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId)
let newArr = [...state.cartItems];
newArr[idx] = action.payload;
return {...state, cartItems: newArr}
}
How Does Redux-Thunk Work?
Asynchronous Redux using Redux Thunk