Context API & useReducer
Props Drilling
- 컴포넌트 트리에서 데이터를 하위 컴포넌트로 전달하기 위해 중간 컴포넌트를 통해
property를 내려주는 것 - 중간 컴포넌트는 원하는 자식 컴포넌트에게
property를 전달하기 위해 필요하다. - 그러나, 해당 값을 직접 사용하지 않는 경우에도
property를 받고 전달해야 한다.
장점
- 명시적인 값의 사용
- 값 추적이 편해진다.
- 코드 변경 파악이 편해진다.
단점
property데이터 형식 변경의 불편- 중간 컴포넌트에 불필요한
property전달 - 누락된
property파악 어려움 property이름 변경 추적의 어려움
해결법
Context APIRedux또는 다른 상태 관리 라이브러리Custom HooksRender Props과children props
Context API
children props을 이용한 컴포넌트 합성으로 해결할 수 있지만, 상위 컴포넌트가 비대해질 수 있으므로,Context API의 사용도 고려해봐야 한다.state와 연결이 쉽다.- 관습적으로
Context API를 저장하는 폴더를store로 지정한다.
Provider로 Context 이용가능하다.
Provide로Context이용할 때,Context가 전송하는value데이터는 객체 형태이다.- 객체에서 하나의 데이터만 변경되어도
Provider로 감싼 모든 자식 컴포넌트에서 리렌더링이 일어난다.
해결법
Context데이터를 가지고 있는 컴포넌트에useMemo훅을 이용하여 데이터를 캐싱하여 불필요한 리렌더링 방지Context쪼개서 여러 개를 사용한다.React.memo고차함수 사용하여 자식 컴포넌트의 불필요한 리렌더링 방지한다.- 나의 해결법은
useMemo훅을 이용했다.
const [shoppingCart, setShoppingCart] = useState({ items: [] });
const valueCtx = useMemo(
() => ({
items: shoppingCart.items,
}),
[shoppingCart],
return (
<CartContext.Provider value={valueCtx}>
<Header />
<Shop />
</CartContext.Provider>
);
Context 소비
useContext로 이용한다.- 비구조화 할당도 가능하다.
const cartCtx=useContext(CartContext);
const totalPrice = cartCtx.items.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
## Context와 State 연결
```javascript
import { useState, useMemo } from 'react';
import Header from './components/Header';
import Shop from './components/Shop';
import Product from './components/Product';
import { DUMMY_PRODUCTS } from './dummy-products';
import { CartContext } from './store/ShoppingCardContext';
function App() {
const [shoppingCart, setShoppingCart] = useState({ items: [] });
function handleAddItemToCart(id) {
// 생략
}
function handleUpdateCartItemQuantity(productId, amount) {
// 생략
}
const valueCtx = useMemo(
() => ({
items: shoppingCart.items,
addItemToCart: handleAddItemToCart,
updateCartItemQuantity: handleUpdateCartItemQuantity,
}),
[shoppingCart],
);
return (
<CartContext.Provider value={valueCtx}>
<Header />
<Shop />
</CartContext.Provider>
);
}
export default App;
Context 추적을 위한 익명 함수
- 익명 함수로 만들어두어서 자동완성을 통해,
Context추적을 편하게 하였다.
import { createContext } from 'react';
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
updateCartItemQuantity: () => {},
});
Context Consumer
useContext훅을 사용하지 않는 접근법- 컨텍스트 값에 대한 액세스를 가진 JSX 코드를 묶음
- 그러나 복잡해서 보통
useContext를 더 선호한다.
import { useContext } from 'react';
import { CartContext } from '../store/ShoppingCardContext';
export default function Cart({ onUpdateItemQuantity }) {
return (
<CartContext.Consumer>
{(cartCtx) => {
const totalPrice = cartCtx.items.reduce((acc, item) => acc + item.price * item.quantity, 0);
const formattedTotalPrice = `$${totalPrice.toFixed(2)}`;
return (
<div id="cart">
{cartCtx.items.length === 0 && <p>No items in cart!</p>}
{cartCtx.items.length > 0 && (
<ul id="cart-items">
{cartCtx.items.map((item) => {
const formattedPrice = `$${item.price.toFixed(2)}`;
return (
<li key={item.id}>
<div>
<span>{item.name}</span>
<span> ({formattedPrice})</span>
</div>
<div className="cart-item-actions">
<button onClick={() => onUpdateItemQuantity(item.id, -1)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => onUpdateItemQuantity(item.id, 1)}>+</button>
</div>
</li>
);
})}
</ul>
)}
<p id="cart-total-price">
Cart Total: <strong>{formattedTotalPrice}</strong>
</p>
</div>
);
}}
</CartContext.Consumer>
);
}
Context 멀티소싱 & 분리된 컴포넌트에 State 연동
- 상위 컴포넌트에
Context를 비롯한 다른 로직이 너무나도 많아져서 상위 컴포넌트가 비대해지는 문제가 발생한다. - 해결법으로
Context를 다른 컴포넌트로 분리
예시
// App.jsx
import Header from './components/Header';
import Shop from './components/Shop';
import CartContextProvider from './store/ShoppingCardContext';
function App() {
return (
<CartContextProvider>
<Header />
<Shop />
</CartContextProvider>
);
}
export default App;
// ShoppingCardContext.jsx
import { createContext, useMemo, useState } from 'react';
import { DUMMY_PRODUCTS } from '../dummy-products';
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
updateCartItemQuantity: () => {},
});
export default function CartContextProvider({ children }) {
const [shoppingCart, setShoppingCart] = useState({ items: [] });
function handleAddItemToCart(id) {
setShoppingCart((prevShoppingCart) => {
const updatedItems = [...prevShoppingCart.items];
const existingCartItemIndex = updatedItems.findIndex((cartItem) => cartItem.id === id);
const existingCartItem = updatedItems[existingCartItemIndex];
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
quantity: existingCartItem.quantity + 1,
};
updatedItems[existingCartItemIndex] = updatedItem;
} else {
const product = DUMMY_PRODUCTS.find((products) => products.id === id);
const newItem = {
id,
name: product.title,
price: product.price,
quantity: 1,
};
updatedItems.push(newItem);
}
return {
items: updatedItems,
};
});
}
function handleUpdateCartItemQuantity(productId, amount) {
setShoppingCart((prevShoppingCart) => {
const updatedItems = [...prevShoppingCart.items];
const updatedItemIndex = updatedItems.findIndex((item) => item.id === productId);
const updatedItem = {
...updatedItems[updatedItemIndex],
};
updatedItem.quantity += amount;
if (updatedItem.quantity <= 0) {
updatedItems.splice(updatedItemIndex, 1);
} else {
updatedItems[updatedItemIndex] = updatedItem;
}
return {
items: updatedItems,
};
});
}
const valueCtx = useMemo(
() => ({
items: shoppingCart.items,
addItemToCart: handleAddItemToCart,
updateCartItemQuantity: handleUpdateCartItemQuantity,
}),
[shoppingCart],
);
return <CartContext.Provider value={valueCtx}>{children}</CartContext.Provider>;
}
상태 관리를 위한 useReducer
Reducer: 복잡한 값을 하나의 단순한 값으로 바꾸는 함수- 불편한 반복을 피하기 위해서
useState가 아닌useReducer를 이용하여 상태 관리. useState와는 다르게state를 업데이트하는 로직을 다른 컴포넌트에 작성할 수 있고, 주로 복잡한 여러 상태를 다룰 때 사용한다.- 첫 인자로는 다루려는
state가 필요하다. - 다음 인자로는
action을 보낼 수 있는dispatch함수가 필요하다. useReducer안에는 상태 제어 함수가 필요하고, 처음 상태는 필요하면 넣는다.
const [state, dispatch]=useReducer(theFunctionControlTheState, initialState);
컴포넌트 바깥에 함수를 정의하는 이유
- 컴포넌트가 실행될 때마다 컴포넌트 바깥의
Reducer함수가 재생성되지 않도록 한다. - 컴포넌트에서 정의 또는 업데이트되는 그 어떤 값에도 직접적인 액세스를 필요로 하지 않는다.
- 속성에 접근할 필요가 없다.
Reducer 함수의 인자로 state와 action
useReducer의dispatch를 통해 보내진 후에 리액트가 리듀서 함수를 호출할 것이기에state와action이 필요하다.action을 줄 때,type에string으로 명시적으로 상태를 관리하고, 패러미터로payload라 명시한다.
shoppingCartDispatch({
type: 'ADD_ITEM',
payload: id,
});
예시
switch로 액션 타입을 나눌 수도 있지만, 나는if를 더 선호해서if로 하였다.
리액트를 다루는 기술에서 본
action과payload의 의미를 이를 통해서 겨우 깨닫고, 진짜 감탄했다. 아는만큼 보인다는 사실이 진짜였구나...
앞으로useReducer자주 애용해야겠다.
import { createContext, useMemo, useState, useReducer } from 'react';
import { DUMMY_PRODUCTS } from '../dummy-products';
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
updateCartItemQuantity: () => {},
});
function shoppingCartReducer(state, action) {
if (action.type === 'ADD_ITEM') {
const updatedItems = [...state.items];
const existingCartItemIndex = updatedItems.findIndex((cartItem) => cartItem.id === action.payload);
const existingCartItem = updatedItems[existingCartItemIndex];
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
quantity: existingCartItem.quantity + 1,
};
updatedItems[existingCartItemIndex] = updatedItem;
} else {
const product = DUMMY_PRODUCTS.find((products) => products.id === action.payload);
const newItem = {
id: product.id,
name: product.title,
price: product.price,
quantity: 1,
};
updatedItems.push(newItem);
}
return {
...state,
items: updatedItems,
};
}
if (action.type === 'UPDATE_ITEM') {
const updatedItems = [...state.items];
const updatedItemIndex = updatedItems.findIndex((item) => item.id === action.payload.productId);
const updatedItem = {
...updatedItems[updatedItemIndex],
};
updatedItem.quantity += action.payload.amount;
if (updatedItem.quantity <= 0) {
updatedItems.splice(updatedItemIndex, 1);
} else {
updatedItems[updatedItemIndex] = updatedItem;
}
return {
...state,
items: updatedItems,
};
}
return state;
}
export default function CartContextProvider({ children }) {
const [shoppingCartState, shoppingCartDispatch] = useReducer(shoppingCartReducer, { items: [] });
function handleAddItemToCart(id) {
shoppingCartDispatch({
type: 'ADD_ITEM',
payload: id,
});
}
function handleUpdateCartItemQuantity(productId, amount) {
shoppingCartDispatch({
type: 'UPDATE_ITEM',
payload: {
productId,
amount,
},
});
}
const valueCtx = useMemo(
() => ({
items: shoppingCartState.items,
addItemToCart: handleAddItemToCart,
updateCartItemQuantity: handleUpdateCartItemQuantity,
}),
[shoppingCartState],
);
return <CartContext.Provider value={valueCtx}>{children}</CartContext.Provider>;
}'React 완벽 가이드' 카테고리의 다른 글
| 컴포넌트 재생성 및 Rerendering, Optimizing React(with useCallback ,memo and useMemo) (0) | 2024.03.29 |
|---|---|
| Side Effects & useEffect (0) | 2024.03.29 |
| Refs & Portals (1) | 2024.03.29 |
| React - the complete guide 06: CSS with React (0) | 2024.03.29 |
| React 핵심 이론 2(React Core 2) (0) | 2024.03.29 |