You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React-Admin创建/更新后列表不刷新,缓存读取异常求助

React-Admin 创建/更新记录后列表不自动刷新问题

创建或更新Office记录后,列表组件无法自动刷新,必须手动硬刷新才能看到最新数据。尽管创建/更新请求返回成功,但List组件始终读取缓存数据,而非重新请求API。

已尝试的排查操作:

  • 配置Nginx禁用缓存
  • 调用react-query的queryClient.invalidateQueries()方法
  • 在创建成功回调中调用refresh()

相关代码

1. OfficeList.tsx

export const OfficeList = () => {
    const refresh = useRefresh();
    const location = useLocation();

    const matchCreate = matchPath({
        path: '/offices/create',
        end: true
    }, location.pathname);

    const matchUpdate = matchPath({
        path: '/offices/:id',
        end: false
    }, location.pathname);

    const isCreateOpen = !!matchCreate;
    const isEditOpen = !!matchUpdate && location.pathname !== '/offices/create';
    const recordId = isEditOpen ? matchUpdate?.params.id : null;

    return (
        <>
            <List
                sort={{ field: "login", order: "ASC" }}
                perPage={10}
                actions={<OfficeActions/>}
                sx={{ marginTop: '30px', marginRight: '10px' }}
            >
                <Datagrid bulkActionButtons={false} onSubmit={refresh}>
                    <TextField source="name" label={'Nom'}/>
                    <EmailField source="email" />
                    <TextField label="Adresse" source="address"  />
                    <NumberField label="Dentistes" source="users.length" textAlign="center" sx={{fontSize: '1.1em', fontWeight: '500'}}/>
                    <TextField source="phoneNumber" fontStyle={'italic'} />
                    <TextField source="city" />
                    <DateField label="Date MàJ" source="updatedAt" textAlign="center"  fontStyle={'italic'}/>
                    <CustomActionsField />
                </Datagrid>
            </List>
            { isCreateOpen && <OfficeCreate open={isCreateOpen} /> }
            { isEditOpen && <OfficeEdit open={isEditOpen} recordId={recordId} /> }

        </>
    );
};

2. OfficeCreate.tsx

interface OfficeCreateProps {
    open: boolean;
}

export const OfficeCreate = ({ open } : OfficeCreateProps) => {
    const notify = useNotify();
    const refresh = useRefresh();
    const redirect = useRedirect();

    const handleClose = () => redirect('/offices');

    const onSuccess = () => {
        notify(`Le cabinet a été créé avec succès.`, {type: 'success'});
        redirect('/offices');
        refresh();
    };

    const onError = () => {
        notify(`Une erreur est survenue, vérifiez les données.`, {type: 'error'});
    };

    const validateName = [required(), minLength(3, 'Nom: 3 caractères minimum.'), maxLength(50, 'Nom: 50 caractères maximum.')];
    const validateEmail = email('Email: Format non valide.');
    const validatePhoneNumber = regex(/^((\+|00)33\s?|0)[3467](\s?\d{2}){4}$/, 'Téléphone: Format non valide commencez par 04, 06 ou 07 suivis de de 8 chiffres.');
    const validateZipCode = regex(/^(?:0[1-9]|[1-9]\d|9[0-8])\d{3}$/, 'Code postal: 2 chiffres minimum et 5 chiffres maximum.');

    return (
        <Dialog open={open} onClose={handleClose} >
            <DialogTitle>Créer un cabinet :</DialogTitle>
            <Create<Office>
                mutationOptions={{onSuccess, onError}}
                resource="offices"
                title=" "
                sx={{ width: 500, '& .RaCreate-main': { mt: 0 } }}
            >
                <SimpleForm toolbar={<SaveButton label="Enregistrer" sx={{ margin: 2 , borderRadius: '40px'}}/>}>
                    <TextInput
                        label='Nom'
                        source="name"
                        validate={validateName}
                        fullWidth
                    />
                    <TextInput
                        label='Email'
                        source="email"
                        validate={validateEmail}
                        fullWidth
                    />
                    <TextInput
                        label='Adresse'
                        source="address"
                        fullWidth
                    />
                    <Box display={{ xs: 'block', sm: 'flex', width: '100%' }}>
                        <Box flex={1} mr={{ xs: 0, sm: '0.5em' }}>
                            <TextInput
                                label='Téléphone'
                                source="phone"
                                validate={validatePhoneNumber}
                                fullWidth
                            />
                        </Box>
                        <Box flex={1} mr={{ xs: 0, sm: '0.5em' }}>
                            <TextInput
                                label='Ville'
                                source="city"
                                fullWidth
                            />
                        </Box>
                    </Box>
                    <TextInput
                        label='Code postal'
                        source="zipcode"
                        validate={validateZipCode}
                    />
                </SimpleForm>
            </Create>
        </Dialog>
    );
};

3. dataProvider中的httpClient和getList方法

const httpClient = (url: string, options: RequestInit = {}) => {
    if (!options.headers) {
        options.headers = new Headers({ Accept: 'application/ld+json' }) as Headers;
    }
    if (!(options.body instanceof FormData)) {
        options.headers = new Headers({ 'Content-Type': 'application/json'}) as Headers;
    }
    const token = localStorage.getItem('token');
    if (token) {
        (options.headers as Headers).set('Authorization', `Bearer ${token}`);
    }
    return fetchUtils.fetchJson(url, options);
};

getList: async (
        resource: string,
        params: {
            pagination: { page: any; perPage: any; };
            sort: { field: any; order: any; };
            filter: any; }
    ) => {
        const { page, perPage } = params.pagination;
        const { field, order } = params.sort;
        let query = {
            ...fetchUtils.flattenObject(params.filter),
            page: page,
            itemsPerPage: perPage
        };

        if (order) {
            query[`order[${field}]`] = order.toLowerCase();
        }

        const url = `${import.meta.env.VITE_API_BASE}/${resource}?${stringify(query)}`;

        return httpClient(url).then(({ json }) => {
            if (!json['hydra:member']) {
                throw new Error('No hydra:member in response');
            }
            return {
                data: json['hydra:member'],
                total: json['hydra:totalItems'],
            };
        });
    },

解决方案

1. 为记录添加唯一id字段

React-Admin依赖每个记录的id字段管理缓存,Api Platform返回的hydra:member使用@id而非id,需在getList中映射:

getList: async (resource, params) => {
    // ... 原有代码
    return httpClient(url).then(({ json }) => {
        if (!json['hydra:member']) {
            throw new Error('No hydra:member in response');
        }
        // 从@id中提取id
        const data = json['hydra:member'].map(item => ({
            ...item,
            id: item['@id'].split('/').pop()
        }));
        return {
            data,
            total: json['hydra:totalItems'],
        };
    });
},

2. 手动触发缓存失效

在Create的onSuccess中调用dataProvider的invalidate方法,强制失效offices资源的缓存:

export const OfficeCreate = ({ open } : OfficeCreateProps) => {
    const notify = useNotify();
    const refresh = useRefresh();
    const redirect = useRedirect();
    const dataProvider = useDataProvider(); // 新增

    const handleClose = () => redirect('/offices');

    const onSuccess = () => {
        notify(`Le cabinet a été créé avec succès.`, {type: 'success'});
        dataProvider.invalidate('offices'); // 新增:失效缓存
        redirect('/offices');
        refresh();
    };

    // ... 原有代码
};

3. 配置List组件的查询选项

显式设置List的queryOptions,确保缓存及时失效:

<List
    sort={{ field: "login", order: "ASC" }}
    perPage={10}
    actions={<OfficeActions/>}
    sx={{ marginTop: '30px', marginRight: '10px' }}
    queryOptions={{
        staleTime: 0,
        cacheTime: 5 * 60 * 1000
    }}
>
    {/* ... Datagrid内容 */}
</List>

4. 优化httpClient请求头

添加禁止缓存的请求头,避免浏览器缓存响应:

const httpClient = (url: string, options: RequestInit = {}) => {
    if (!options.headers) {
        options.headers = new Headers({ 
            Accept: 'application/ld+json',
            'Cache-Control': 'no-cache, no-store, must-revalidate' // 新增
        }) as Headers;
    }
    if (!(options.body instanceof FormData)) {
        (options.headers as Headers).set('Content-Type', 'application/json');
    }
    const token = localStorage.getItem('token');
    if (token) {
        (options.headers as Headers).set('Authorization', `Bearer ${token}`);
    }
    return fetchUtils.fetchJson(url, options);
};

内容的提问来源于stack exchange,提问作者Lucien Arbieu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 09:29:51