mswjs / mswjs.io

Official website and documentation for the Mock Service Worker library.
https://mswjs.io
151 stars 176 forks source link

MSW loading in react native is not happening #389

Open turjoy-real opened 4 months ago

turjoy-real commented 4 months ago

Preloading MSW fails to detect if App is registered.

Extra line needs to be added which loads data without MSW first, then recognises MSW.

import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

import Config from 'react-native-config';

async function enableMocking() {
  if (Config.APP_ENV !== 'test') {
    return;
  }

  await import('./msw.polyfills');
  const {server} = await import('./src/__mock__/server');
  server.listen();
}

AppRegistry.registerComponent(appName, () => App); // <----

enableMocking().then(() => {
  AppRegistry.registerComponent(appName, () => App);
});
kettanaito commented 4 months ago

Hi, @turjoy-real.

Not sure I fully understand the issue, could you describe it a bit more for me?

The reason AppRegistry.registerComponent() is deferred untul the enableMocking() promise resolves is so your app wouldn't render a component that relies on an HTTP request while MSW hasn't activated yet. In RN case, it mostly comes to the dynamic import promise, which still takes time (the interception itself is instantaneous).

If we call .registerComponent() straight away, wouldn't it start rendering the app?

turjoy-real commented 4 months ago

Yes it starts rendering the app with default APIs. After MSW loads, it then uses MSW mocks.

MSW is supposed to load before AppRegistry. But react native doesn't wait for the promise to resolve loading MSW and throws error saying there's no AppRegistry.

kettanaito commented 4 months ago

But react native doesn't wait for the promise to resolve loading MSW and throws error saying there's no AppRegistry.

Yeah, this is the culprit. RN expects you to call .registerComponent on the same tick.

Do you know if we can defer the promise await to the () => App closure? Does RN support something like this?

App.registerComponent(async () => {
  await enableMocking()
  return App
})

I suppose not but I have zero clue about how RN behaves.

turjoy-real commented 4 months ago

No this is not working too. React Native doesn't expect a async callback function.

This is the new error:

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

This is happening even if I remove the await step.

It's taking the unresolved promise object.

Used this:

AppRegistry.registerComponent(appName, async () => { await enableMocking(); return App; });

Instead of:

AppRegistry.registerComponent(appName, () => App);

kettanaito commented 4 months ago

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

This error suggests the second argument is expected to be a React component. This means we can create a wrapper component and delegate the promise resolution to the state.

App.registerComponent(appName, () => {
  const [isMockingEnabled, setMockingEnabled] = React.useState(false)

  React.useEffect(() => {
    enableMocking().then(() => setMockingEnabled(true))
  }, [])

  if (!isMockingEnabled) return null
  return <App />
})

Can you please give this a try, @turjoy-real? Thanks.

turjoy-real commented 4 months ago

Hi @kettanaito,

Thanks for the idea.

Successfully made MSW APIs the default for all E2E testing API calls.

The changes had to be made in the App.tsx(or App.js) to be able to use hooks.

function App() {
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    enableMocking().then(() => setLoading(false));
  }, []);

  return (
    !loading && <Component/>
  );
}

And for the dynamic imports of msw.polyfills and mock server in typescript,

updated tsconfig.json to use 'ES2022':


{
  "extends": "@react-native/typescript-config/tsconfig.json",
  "compilerOptions": {
    "typeRoots": ["./types"],
    "module": "ES2022"
  }
}

Leaving index.js as it was:


import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

Here is the App commit with the changes.

Thank you for your support.

It was exciting to see it resolved finally with such a simple solution.

Let me know if I can contribute with you in any other issues.

kettanaito commented 4 months ago

That's awesome! 🎉 We need to update the [React Native integration]() docs with this so it's clear to everyone.

turjoy-real commented 4 months ago

Yes, that'll be helpful for everyone!

Let me know if and how I can help.

Should I prepare a draft and post in this thread?