카트에 아이템을 담을 때 전역적으로 state를 관리하고 싶어서 useContext와 useReducer를 사용했다.
useContext 사용 이유
Provider로 감싼 하위 컴포넌트에 props를 전달하지 않아도 state에 접근을 할 수 있게 된다. (Props Drilling를 피할 수 있다.)
useReducer 사용 이유
useContext로 내려받은 state에 대해서 state관리를 할 수 있게 된다. (state관리 재사용성에 유리)
해당 어플리케이션의 라우터 구조는 다음과 같다.
export default function App() {
return (
<StateContextProvider>
<Routes />
</StateContextProvider>
);
}
function Routes() {
return (
<BrowserRouter>
<Route exact path="/">
<OrderList />
</Route>
<Route exact path="/checkout">
<Checkout />
</Route>
<Route exact path="/shopping-cart">
<ShoppingCart />
</Route>
<Route exact path="/shopping">
<Shopping />
</Route>
</BrowserRouter>
);
}
import React, { useContext, useReducer } from "react";
const reducer = (state, action) => {
switch(action.type){
case 'ADD_CART':
const newProduct = {
id: action.product.id,
title: action.product.title,
price: Number(action.product.price)
};
return {
carts: [
...state.carts,
newProduct
]
};
case 'RESET_CART':
return {
...state,
carts: []
};
default:
throw new Error("reducer error");
}
};
const StateContext = React.createContext(null);
const initState = {
carts: [],
};
export function StateContextProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initState);
const addProduct = (product) => dispatch({ type: 'ADD_CART', product });
const resetCart = () => dispatch({ type: 'RESET_CART'});
return (
<StateContext.Provider value={{state, addProduct, resetCart}}>{children}</StateContext.Provider>
);
}
StateContext 하위에 있는 컴포넌트들은 useStateContext를 사용하여 value={{state, addProduct, resetCart}}
에 대해서 접근할 수 있게 된다.
export function useStateContext() {
return useContext(StateContext);
}
예를 들면 카트에 담긴 상품목록에 접근하는 예시는 다음과 같다.
import { useStateContext } from "./StateContext";
export default function Checkout() {
const {
state: { carts },
resetCart,
} = useStateContext();
return (
<PageLayout>
<Navigation />
<Header>
<h4>주문할 물품</h4>
</Header>
{carts.length === 0 &&
<Message>{'주문할 상품이 없습니다.'}</Message>
}
{carts.length > 0 &&
<>
<Carts carts={carts} />
<CheckoutForm
onSubmit={handleSubmit}
success={success}
error={error}
loading={loading}
/>
</>
}
</PageLayout>
);
}
useReducer를 사용하여 미리 정의해놓은 액션을 이용하면 전역적으로 적절한 상태관리를 할 수 있다.
예를 들면 카트에 상품을 담는 예시는 다음과 같다.
import { useStateContext } from "./StateContext";
export default function Shopping() {
const [products, setProducts] = useState([]);
const {state, addProduct} = useStateContext();
useEffect(() => {
getAllProducts().then((res) => setProducts(res));
}, []);
function handleClick(selectedProduct){
/* 중복적인 아이템에 대한 핸들링 필요 */
const newProduct = {
id: selectedProduct.id,
title: selectedProduct.title,
price: Number(selectedProduct.price)
}
// 카트에 새로운 아이템 추가
// === dispatch({ type: 'ADD_CART', newProduct });
addProduct(newProduct);
}
return (
<PageLayout>
<Navigation />
<Header>
<h4>상품 목록</h4>
</Header>
<div>
{products.map(product =>
<li key={product.id}>
<p>{product.title}</p>
<p>$ {product.price}</p>
<button onClick={() => handleClick(product)}>추가</button>
</li>
)}
</div>
</PageLayout>
)
}
useContext를 다시 복습하면서 느낀점은 관련된 상태가 하나라도 변화하면 하위 컴포넌트들이 다시 리렌더링되는데 불필요한 리렌더링이 되지 않도록 방어가 필요할 것 같다.