swr icon indicating copy to clipboard operation
swr copied to clipboard

optimisticData not working after upgrading to Expo 53

Open marcelofarias opened this issue 6 months ago • 1 comments

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

marcelofarias avatar May 27 '25 11:05 marcelofarias

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 --

  1. turning off unstable_enablePackageExports in metro.config.js, or

  2. aliasing every swr import to a single build (dist/index.mjs or dist/index.js).


What actually went wrong

  1. Metro change: In SDK 53 React-Native (0.79), Metro’s unstable_enablePackageExports flag is now true by default.

  2. SWR ships dual builds:

    • require('swr')dist/index.jsconfig-context-client-*.js (CJS)

    • import useSWR from 'swr'dist/index.mjsconfig-context-client-*.mjs (ESM)

  3. 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 optimisticData never 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.

marcelofarias avatar May 29 '25 09:05 marcelofarias

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.

Nezz avatar Sep 12 '25 08:09 Nezz

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.

Nezz avatar Sep 12 '25 13:09 Nezz

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.

gauthier-th avatar Sep 18 '25 20:09 gauthier-th

A more universal fix is being worked on here: https://github.com/expo/expo/issues/39653

Nezz avatar Sep 19 '25 10:09 Nezz