题意:"React-query 的 `setQueryData` 没有触发组件重新渲染"
问题背景:
I've been dealing for a while with this problem and still can't tackle it.
"我已经处理这个问题一段时间了,但仍然无法解决它。"
I'm using React-query as a server state management library and I'm trying to get my UI state synchronized with my server state when a mutations occurs. Since I can use the mutation response to avoid a new API call, I'm using the setQueryData feature that React-query gives us.
"我正在使用 React-query 作为服务器状态管理库,试图在发生变更时将我的 UI 状态与服务器状态同步。由于我可以使用变更响应来避免新的 API 调用,因此我使用了 React-query 提供的 `setQueryData` 功能。"
The problem is that the old-data is being correctly modified (I can see it in the react-query DevTools) when a mutation is successful, but the component using it isn't being re-rendered, making my UI State not synchronized with my Server state (well, at least the user can't see the update).
"问题是,当变更成功时,旧数据被正确修改(我在 React-query DevTools 中可以看到),但是使用它的组件没有重新渲染,这导致我的 UI 状态与服务器状态不同步(好吧,至少用户无法看到更新)。"
Let me show some code and hope someone can give me some insights.
"让我展示一些代码,希望有人能给我一些见解。"
Component using the query: 使用查询的组件
const Detail = ({ orderId }) => {
const { workGroups } = useWorkGroups();
const navigate = useNavigate();
const queryClient = useQueryClient();
const orderQueries = queryClient.getQueryData(["orders"]);
const queryOrder = orderQueries?.find((ord) => ord.id === orderId);
// more code
Component mutating the query
: "变更查询的组件:"
const Deliver = ({
setIsModalOpened,
artisan,
index,
queryQuantity,
queryOrder,
}) => {
const [quantity, setQuantity] = useState(() => queryQuantity);
const { mutate: confirmOrderDelivered } = useMutateOrderDeliveredByArtisan(
queryOrder.id
);
const onSubmit = () => {
confirmOrderDelivered(
{
id: queryOrder.artisan_production_orders[index].id,
artisan: artisan.user,
items: [
{
quantity_delivered: quantity,
},
],
},
{
onSuccess: setIsModalOpened(false),
}
);
};
// more code
Now the mutation function (ik it's a lot of logic but I dont' want to refetch the data using invalidateQueries since we're dealing with users with a really bad internet connection). Ofc you don't need to understand each step of the fn but what it basically does is update the old queried data. In the beginning I thought it was a mutation reference problem since React using a strict comparison under the hood but I also checked it and It doesn't look like it's the problem.
:
"现在是变更函数(我知道逻辑很多,但我不想使用 `invalidateQueries` 来重新获取数据,因为我们正在处理网络连接非常差的用户)。当然,你不需要理解函数的每一步,但它基本上是在更新旧的查询数据。一开始我认为这是一个变更引用问题,因为 React 在内部使用严格比较,但我也检查过,这似乎不是问题所在。"
{
onSuccess: (data) => {
queryClient.setQueryData(["orders"], (oldQueryData) => {
let oldQueryDataCopy = [...oldQueryData];
const index = oldQueryDataCopy.findIndex(
(oldData) => oldData.id === orderId
);
let artisanProdOrders =
oldQueryDataCopy[index].artisan_production_orders;
let artisanProductionOrderIdx = artisanProdOrders.findIndex(
(artProdOrd) => artProdOrd.id === data.id
);
artisanProdOrders[artisanProductionOrderIdx] = {
...artisanProdOrders[artisanProductionOrderIdx],
items: data.items,
};
const totalDelivered = artisanProdOrders.reduce((acc, el) => {
const delivered = el.items[0].quantity_delivered;
return acc + delivered;
}, 0);
oldQueryDataCopy[index] = {
...oldQueryDataCopy[index],
artisan_production_orders: artisanProdOrders,
items: [
{
...oldQueryDataCopy[index].items[0],
quantity_delivered: totalDelivered,
},
],
};
return oldQueryDataCopy;
});
},
onError: (err) => {
throw new Error(err);
},
}
And last but not least: I already checked that the oldQueryData is being correctly modified (console loging in the onSuccess fn in the mutation response) and, as I said before, the data is correctly modified in the React-query DevTools.
"最后但同样重要的是:我已经检查过 `oldQueryData` 被正确修改了(在 mutation 响应的 `onSuccess` 函数中进行了控制台日志记录),并且正如我之前所说,数据在 React-query DevTools 中也被正确修改。"
I know this is a lot of code and the problem seems to be complex but I really believe that it might be a really easy thing that I'm not pointing out because of how tired I already am.
"我知道这段代码很多,问题似乎很复杂,但我真的相信这可能是一个非常简单的事情,我没有指出来是因为我已经很疲惫了。"
Thanks!
问题解决:
Well, I fixed it in the worst possible way imho, so I will answer this question but I really would like to read your thoughts.
"好吧,我以我认为最糟糕的方式修复了它,所以我会回答这个问题,但我真的很想听听你的想法。"
It looks like the new query data setted on the expected query is re-rendering the component only if the mutation function is located in the component that we actually want to re-render.
"看起来,如果 mutation 函数位于我们实际想要重新渲染的组件中,那么设置在预期查询上的新查询数据只会重新渲染该组件。"
With that in mind what I did was just colocate my mutation function in the parent component and pass it down through the child component.
"考虑到这一点,我所做的就是将我的 mutation 函数放在父组件中,并通过子组件传递下去。"
Something like this: 类似这样
const Detail = ({ orderId }) => {
const { workGroups } = useWorkGroups();
const navigate = useNavigate();
const { mutate: confirmOrderDelivered } = useMutateOrderDeliveredByArtisan(
queryOrder.id
); ==============> THE MUTATION FN IS NOW IN THE PARENT COMPONENT
const queryClient = useQueryClient();
const orderQueries = queryClient.getQueryData(["orders"]);
const queryOrder = orderQueries?.find((ord) => ord.id === orderId);
// more code
First child
: 第一个子类
const Assigned = ({ artisan, queryOrder, index, confirmOrderDelivered }) => {
// THE IMPORTANT PART HERE IS THE PROP BEING PASSED DOWN.
<Modal
isOpen={isModalOpened}
toggleModal={setIsModalOpened}
// className="w312"
width="80%"
height="fit-content"
justifyCont="unset"
alignItems="unset"
>
<Deliver
setIsModalOpened={setIsModalOpened}
artisan={artisan}
queryQuantity={quantity}
queryOrder={queryOrder}
index={index}
confirmOrderDelivered={confirmOrderDelivered} => HERE
/>
</Modal>
Component that actually needs the mutation fn
:
"实际上需要 mutation 函数的组件:"
const Deliver = ({
setIsModalOpened,
artisan,
index,
queryQuantity,
queryOrder,
confirmOrderDelivered,
}) => {
const [quantity, setQuantity] = useState(() => queryQuantity);
const onSubmit = () => {
confirmOrderDelivered( => HERE.
{
id: queryOrder.artisan_production_orders[index].id,
artisan: artisan.user,
items: [
{
quantity_delivered: quantity,
},
],
}
);
};