swr
swr copied to clipboard
optimisticData not working after upgrading to Expo 53
Bug report
Description / Observed Behavior
The same code that worked with Expo 52 is not working with Expo 53
Repro Steps / Code Example
void createPost({
post: dto,
}, {
optimisticData: data && [{
...dto,
createdAt: now,
updatedAt: now,
userId: userId,
id: 'optimistic-id',
}, ...data],
rollbackOnError: true,
onError: (error) => {
console.error('Error creating post:', error);
},
});
export function useCreatePost(vacationId: string | null) {
const { mutate } = usePosts(vacationId);
return useSWRMutation(`/vacations/${vacationId}/posts`, createPost, {
onSuccess: (post) => mutate(addTrailing(post), false),
onError: (error) => console.error('Failed to create post:', error),
});
}
A console.log in a component that depends on posts reveals that it is not re-rendered until after the server-side mutation completes.
const { data: posts } = usePosts(vacationId);
console.log('rendered', posts?.length);
Additional Context
SWR version 2.3.3
TL;DR
Expo 53 / RN 0.79 lets Metro resolve the exports field, so both the CommonJS and ESM builds of SWR end up in the bundle. Each build keeps its own cache (= “dual-package hazard”), so optimistic updates written by one copy aren’t visible to the other. Fix by either --
-
turning off
unstable_enablePackageExportsinmetro.config.js, or -
aliasing every
swrimport to a single build (dist/index.mjsordist/index.js).
What actually went wrong
-
Metro change: In SDK 53 React-Native (0.79), Metro’s
unstable_enablePackageExportsflag is now true by default. -
SWR ships dual builds:
-
require('swr')→dist/index.js→config-context-client-*.js(CJS) -
import useSWR from 'swr'→dist/index.mjs→config-context-client-*.mjs(ESM)
-
-
Both builds get bundled, each with its own module-scoped cache. My mutation hook (compiled against CJS) wrote to cache-A, while my list hook (compiled against ESM) read from cache-B – so
optimisticDatanever surfaced.
I confirmed the duplication via hashed filenames in the bundle:
config-context-client-BXAm5QZy.js ← CJS
config-context-client-v7VOFo66.mjs ← ESM
Quick ways to unblock
| Option | Snippet | Notes |
|---|---|---|
| Disable exports resolution (fastest) | js // metro.config.js const { getDefaultConfig } = require('expo/metro-config'); const cfg = getDefaultConfig(__dirname); cfg.resolver.unstable_enablePackageExports = false; module.exports = cfg; |
Restores pre-53 behaviour. Run with expo start -c to clear Metro’s cache. |
| Force a single build | js cfg.resolver.alias = { swr: require.resolve('swr/dist/index.mjs') }; | Keeps exports on but guarantees one SWR copy. Use .js instead if you prefer CJS. |
Long-term solution
The safest path is for SWR (and other stateful libs) to publish one format only so there’s never more than one copy in a React-Native bundle. Until then, be wary of mixed require/import patterns or keep unstable_enablePackageExports disabled.
I just came across this issue and tried to apply your workaround:
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
// See https://github.com/vercel/swr/issues/4129
config.resolver.alias = { swr: require.resolve('swr/dist/index/index.mjs') };
module.exports = config;
However, it fails with this error:
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/index/index.mjs' is not defined by "exports" in [...]/node_modules/swr/package.json
I tried swr/dist/index.mjs too but that doesn't exist.
In my case the problem is that I import both swr and swr/infinite. While swr bundles the mjs files (as expected), infinite bundles js files for some reason.
Just coming here to say that the workaround above fixed my issue where the Global Configuration with <SWRConfig> was not being applied properly, making my whole app unusable after upgrading to Expo 53.
A more universal fix is being worked on here: https://github.com/expo/expo/issues/39653