기존 ADD_CART 쿼리문과 비슷하나 amount가 추가된 것이다.
▼ src/graphql/cart.js
export const UPDATE_CART = gql`
mutation UPDATE_CART($id: string, $amount: number) {
cart(id: $id, amount: $amount) {
id
imageUrl
price
title
amount
}
}
`;
업데이트를 하는데 cartData가 없는 것은 에러를 발생시킨다.
기존에서 amount만 전달된 값으로 바꿔주면 된다.
▼ src/mocks/handler.ts
graphql.mutation(UPDATE_CART, (req, res, ctx) => {
const newCartData = { ...cartData };
const { id, amount } = req.variables;
if (!newCartData[id]) {
throw new Error("없는 데이터입니다.");
}
const newItem = {
...newCartData[id],
amount,
};
newCartData[id] = newItem;
cartData = newCartData;
return res(ctx.data(newItem));
}),
=> 이렇게만 했을 때의 문제점은 amount의 수량을 화살표키(?)로 증감하여도 데이터 상으로는 amount가 변경되지만, 화면으로 보여지는 것은 바로 변경되지 않는다.
https://tanstack.com/query/v4/docs/react/guides/query-invalidation
: 캐시된 쿼리들을 무효화(invalidate)한다. 특정 쿼리나 쿼리 그룹에 속한 모든 쿼리들을 강제로 재요청하고, 캐시된 데이터를 업데이트할 수 있다.
▼ src/components/cart/item.tsx
const handleUpdateAmount = (e: SyntheticEvent) => {
const amount = Number((e.target as HTMLInputElement).value);
updateCart(
{ id, amount },
{
onSuccess: () => queryClient.invalidateQueries(QueryKeys.CART),
}
);
};
onSuccess에 넣지 않고 따로 넣을 경우 updateCart와 invalidationQueries 둘 다 비동기 요청이기 때문에 순서가 뒤죽박죽되어 제대로 작동하지 않는 것을 주의하자.
: 하나의 변경에도 모든 API를 다시 요청하므로 효율적인 방법이라고 볼 수 없다. request는 줄일 수 있다면 줄여야 한다. 캐시를 변경하는 방법은 없을까?
위 사진에서 보이는 것처럼 UPDATE를 할 때마다 GET을 같이한다.
https://tanstack.com/query/v4/docs/react/guides/optimistic-updates
▼ src/components/cart/item.tsx
const { mutate: updateCart } = useMutation(
({ id, amount }: { id: string; amount: number }) =>
graphqlFetcher(UPDATE_CART, { id, amount }),
{
onMutate: async ({ id, amount }) => {
await queryClient.cancelQueries(QueryKeys.CART);
const prevCart = queryClient.getQueryData<{ [key: string]: Cart }>(
QueryKeys.CART
);
if (!prevCart?.[id]) return prevCart;
const newCart = {
...(prevCart || {}),
[id]: { ...prevCart[id], amount },
};
queryClient.setQueryData(QueryKeys.CART, newCart);
return newCart;
},
onSuccess: (newValue) => {
const prevCart = queryClient.getQueryData<{
[key: string]: Cart;
}>(QueryKeys.CART);
const newCart = {
...(prevCart || {}),
[id]: newValue,
};
queryClient.setQueryData(QueryKeys.CART, newCart);
},
}
);
onSucess에 들어오는 newVaule는 바뀐 값 하나만 들어오는데, Cart 전체에 대한 데이터를 변경해야하므로, 나머지는 그대로 두고 새로 들어온 데이터만 변경된다.
update할 때마다 get cart를 하지 않게 된다.
amount가 1보다 작으면 그냥 return하도록 한다.
▼ src/components/cart/item.tsx
const handleUpdateAmount = (e: SyntheticEvent) => {
const amount = Number((e.target as HTMLInputElement).value);
if (amount < 1) return;
updateCart({ id, amount });
};
수량(amount)을 1 아래로 조절할 수 없도록 최솟값을 설정하고 handleUpdateAmount함수를 onChange함수에 연결해주었다.
<label>
<input className="cart-item_amount" type="number" value={amount} min={1} onChange={handleUpdateAmount />{" "} 개
</label>
각 상품마다 삭제버튼과 체크박스, 전체에 대한 전체선택 체크박스를 추가하였다.
.cart-item {
display: flex;
flex-direction: row;
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 1px solid #000;
justify-content: space-between;
align-items: center;
&_image {
width: 100%;
height: 200px;
object-fit: contain;
}
}
▼ src/graphql/cart.ts
export const DELETE_CART = gql`
mutation DELETE_CART($id: string) {
id
}
`;
▼ src/mockes/handler.ts
graphql.mutation(DELETE_CART, (req, res, ctx) => {
const id = req.variables.id;
const newCartData = { ...cartData };
delete newCartData[id];
cartData = newCartData;
return res(ctx.data(id));
}),
const { mutate: deleteCart } = useMutation(
({ id }: { id: string }) => graphqlFetcher(DELETE_CART, { id }),
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.CART);
},
}
);
const handleDeleteItem = () => {
deleteCart({ id });
};
<button className="cart-item_removeButton" type="button" onClick={handleDeleteItem}>x</button>
제어 컴포넌트 방식(state 사용)을 사용하지 않고 비제어 컴포넌트 방식을 사용한다.
https://developer.mozilla.org/en-US/docs/Web/API/FormData
▼ src/components/cart/index.tsx
...
const checkboxRefs = items.map(() => createRef<HTMLInputElement>());
...
{items.map((item, i) => (
<CartItem {...item} key={item.id} ref={checkboxRefs[i]} />
))}
...
▼ src/components/cart/item.tsx
...
const CartItem = (
{ id, title, imageUrl, price, amount }: Cart,
ref: ForwardedRef<HTMLInputElement>
) => {
...
<input
className="cart-item_checkbox"
type="checkbox"
name={`select-item`}
ref={ref}
/>
...
export default forwardRef(CartItem);
...
▼ src/components/cart/index.tsx
const formRef = useRef<HTMLFormElement>(null);
const handleCheckboxChanged = (e: SyntheticEvent) => {
if (!formRef.current) return;
const targetInput = e.target as HTMLInputElement;
const data = new FormData(formRef.current);
const selectedCount = data.getAll("select-item").length;
if (targetInput.classList.contains("cart_select-all")) {
const allchecked = targetInput.checked;
checkboxRefs.forEach((inputElem) => {
inputElem.current!.checked = allchecked;
});
} else {
const allchecked = selectedCount === items.length;
formRef.current.querySelector<HTMLInputElement>(
".cart_select-all"
)!.checked = allchecked;
}
};
전체를 form으로 감싸고 ref를 formRef로 지정하고 onChange함수에handleCheckboxChanded함수를 넣어준다.
return (
<form ref={formRef} onChange={handleCheckboxChanged}>
<label>
<input className="cart_select-all" name="select-all" type="checkbox" />
전체선택
</label>
<ul className="cart">
{items.map((item, i) => (
<CartItem {...item} key={item.id} ref={checkboxRefs[i]} />
))}
</ul>
</form>
);
첫번째 사진처럼 전체선택되지 않았을 때 두번째 사진처럼 선택되지 않은 나머지 두 항목을 체크하면 전체선택 체크박스도 함께 체크된다.