React中GraphQL订阅异常:路由跳转后事件列表不更新
我用GraphQL客户端搭配React开发应用,主页面展示事件列表,/new路径下设有添加事件的表单。通过GraphQL Playground或新标签页打开/new添加事件时,主页面列表会自动更新;但通过React Router导航至/new添加事件后,主页面的事件列表却无法更新。
通过React Router从主页面跳转到/new时,EventList组件会被卸载,之前用subscribeToMore建立的GraphQL订阅会断开。回到主页面时,组件重新挂载并重新订阅,但此时新事件已经创建完成,订阅无法捕获到之前的创建动作,导致列表不更新。另外,当前的CREATE_EVENT mutation没有主动更新Apollo缓存,完全依赖订阅同步数据,这就导致了路由导航场景下的同步失效。
可以通过两种方式解决这个问题,推荐优先用方案1,能从根源上确保缓存同步:
方案1:在Mutation中手动更新缓存
修改FormComponent里的useMutation配置,添加update函数,事件创建成功后直接更新GET_EVENTS查询的缓存。不管是哪种方式创建事件,缓存都会即时更新,组件就能渲染最新的事件列表。
同时注意修正onCompleted的写法——之前直接写form.resetFields()会立即执行,要改成箭头函数,确保只有在mutation完成后才重置表单。
方案2:优化组件挂载时的数据获取逻辑
在EventList组件的useQuery中添加fetchPolicy: "cache-and-network",让组件每次挂载时都从网络拉取最新数据,同时用缓存作为兜底,确保回到主页面时能拿到最新事件列表。
EventList.js
import React, { useEffect } from "react"; import { useQuery } from "@apollo/client"; import { EVENT_SUBSCRIPTION, GET_EVENTS } from "./queries"; import { List, Divider } from "antd"; import InfiniteScroll from "react-infinite-scroll-component"; import { Link } from "react-router-dom"; import styles from "./styles.module.css"; import Loading from "../../components/Loading"; import EventCounter from "../../components/EventCounter"; function EventList() { // 添加fetchPolicy确保每次挂载都获取最新数据 const { loading, error, data, subscribeToMore } = useQuery(GET_EVENTS, { fetchPolicy: "cache-and-network", }); useEffect(() => { subscribeToMore({ document: EVENT_SUBSCRIPTION, updateQuery: (prev, { subscriptionData }) => { console.log(prev, subscriptionData); if (!subscriptionData.data) return prev; return { events: [...prev.events, subscriptionData.data.eventCreated], }; }, }); }, [subscribeToMore]); if (loading) { return <Loading />; } if (error) { return <div>Error: {error.message}</div>; } return ( <> <div className="counter"> <EventCounter /> </div> <div className={styles.scrollableDiv} id="scrollableDiv"> <List> <InfiniteScroll dataLength={data.events.length} scrollableTarget="scrollableDiv" endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}> <List dataSource={data.events} renderItem={(item) => ( <List.Item key={item.id}> <List.Item.Meta title={ <Link className={styles.listItemTitle} to={`/event/${item.id}`}> {item.title} </Link> } description={ <Link className={styles.listItemDesc} to={`/event/${item.id}`}> {item.desc} </Link> } /> <div className={styles.date}>{`${item.date} / ${item.from}-${item.to}`}</div> </List.Item> )}></List> </InfiniteScroll> </List> </div> </> ); } export default EventList;
FormComponent.js
import React from "react"; import { Button, DatePicker, Form, Input, Row, Select, TimePicker } from "antd"; import { useMutation, useQuery } from "@apollo/client"; import moment from "moment"; import styles from "./style.module.css"; import { CREATE_EVENT, GET_LOCATIONS, GET_USERS, GET_EVENTS } from "./queries"; function FormComponent() { // 添加update函数手动更新缓存 const [addEvent, { data, loading, error }] = useMutation(CREATE_EVENT, { update(cache, { data: { eventCreated } }) { const { events } = cache.readQuery({ query: GET_EVENTS }); cache.writeQuery({ query: GET_EVENTS, data: { events: [...events, eventCreated] }, }); }, }); const { loading: locationsLoading, error: locationsError, data: locationsData } = useQuery(GET_LOCATIONS); const locationsArray = []; if (!locationsLoading && !locationsError && locationsData) { locationsData.locations.forEach((item) => { const data = { value: item.id, label: item.name, title: "location_id", }; locationsArray.push(data); }); } const { loading: userLoading, error: userError, data: userData } = useQuery(GET_USERS); const usersArray = []; if (!userLoading && !userError && userData) { userData.users.forEach((item) => { const data = { value: item.id, label: item.username, title: "user_id", }; usersArray.push(data); }); } const [form] = Form.useForm(); const disabledDate = (current) => { return current && current <= moment().startOf("day"); }; const handleFormSubmit = async (values) => { const data = { title: values.title, desc: values.desc, date: `${values.date.$y}-${values.date.$M + 1}-${values.date.$D}`, from: `${values.hours[0].$H < 9 ? "0" + values.hours[0].$H : values.hours[0].$H}:${values.hours[0].$m < 9 ? "0" + values.hours[0].$m : values.hours[0].$m}`, to: `${values.hours[1].$H < 9 ? "0" + values.hours[1].$H : values.hours[1].$H}:${values.hours[1].$m < 9 ? "0" + values.hours[1].$m : values.hours[1].$m}`, location_id: values.location_id, user_id: values.user_id, }; await addEvent({ variables: { data: data, }, onCompleted: () => form.resetFields(), // 修正为箭头函数,确保mutation完成后再执行 }); }; return ( <Row> <Form className={styles.form} labelCol={{ span: 4 }} wrapperCol={{ span: 24 }} form={form} onFinish={handleFormSubmit}> <Form.Item label="Title" name="title" required rules={[{ required: true, message: "Please enter title" }]}> <Input placeholder="Please enter title" /> </Form.Item> <Form.Item label="Description" name="desc" required rules={[{ required: true, message: "Please enter description" }]}> <Input.TextArea placeholder="Please enter description" /> </Form.Item> <Form.Item label="Date" name="date" required rules={[{ required: true, message: "Please enter date" }]}> <DatePicker placeholder="Please select a date" style={{ width: "100%" }} disabledDate={disabledDate} /> </Form.Item> <Form.Item name="hours" label="Event Hours" required> <TimePicker.RangePicker style={{ width: "100%" }} placeholder={["Select start time", "Select end time"]} format="HH:mm" /> </Form.Item> <Form.Item label="Location" name="location_id" required rules={[{ required: true, message: "Please select a location" }]}> <Select placeholder="Please select a location" options={locationsArray} /> </Form.Item> <Form.Item label="User" name="user_id" required rules={[{ required: true, message: "Please select a user" }]}> <Select placeholder="Please select a user" options={usersArray} /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit"> Submit </Button> </Form.Item> </Form> </Row> ); } export default FormComponent;
内容的提问来源于stack exchange,提问作者Enes SEVAL

