react-native: [6.0] 6.0.1-alpha.1 does not work with Expo

Describe the bug Attempting to follow the 6.0 alpha test instructions with Expo causes an error:

Failed to compile.
/Users/lauriharpf/Repos/lauriharpf/appName/node_modules/@storybook/react-native/dist/preview/Preview.js 12:14
Module parse failed: Unexpected token (12:14)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| const STORAGE_KEY = 'lastOpenedStory';
| export default class Preview {
>     _clientApi;
|     _storyStore;
|     _addons;

To Reproduce Do these steps (slightly modified from the script version of the 6.0 alpha test instructions).

npm install --global expo-cli
expo init appName
(select blank TypeScript template)
cd appName

yarn add @storybook/react-native@next \
            @react-native-async-storage/async-storage \
            @storybook/addon-ondevice-actions@next \
            @storybook/addon-ondevice-controls@next \
            @storybook/addon-ondevice-backgrounds@next \
            @storybook/addon-ondevice-notes@next \
            @storybook/addon-actions \
            @react-native-community/datetimepicker \
            @react-native-community/slider \
            @storybook/addon-controls

echo "/**
 * Metro configuration for React Native
 * https://github.com/facebook/react-native
 *
 * @format
 */
module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
  resolver: {
    resolverMainFields: ['sbmodern', 'main'],
  },
};" > metro.config.js;

mkdir .storybook;
mkdir components;
echo "module.exports = {
  stories: [
    './components/**/*.stories.?(ts|tsx|js|jsx)'
  ],
   addons: [
    '@storybook/addon-ondevice-notes',
    '@storybook/addon-ondevice-controls',
    '@storybook/addon-ondevice-backgrounds',
    '@storybook/addon-ondevice-actions',
  ],
};" > .storybook/main.js;

echo "import {withBackgrounds} from '@storybook/addon-ondevice-backgrounds';
export const decorators = [withBackgrounds];
export const parameters = {
  backgrounds: [
    {name: 'plain', value: 'white', default: true},
    {name: 'warm', value: 'hotpink'},
    {name: 'cool', value: 'deepskyblue'},
  ],
};" > .storybook/preview.js;

echo "import AsyncStorage from '@react-native-async-storage/async-storage';
import { getStorybookUI } from '@storybook/react-native';
import './storybook.requires';
const StorybookUIRoot = getStorybookUI({
  asyncStorage: AsyncStorage,
});
export default StorybookUIRoot;" > Storybook.tsx;

echo "import StorybookUIRoot from './Storybook';
export { StorybookUIRoot as default };" > App.tsx;

node -e 'const fs = require("fs");
const packageJSON = require("./package.json");
packageJSON.scripts = {
    ...packageJSON.scripts,
    prestart: "sbn-get-stories",
    "storybook-watcher": "sbn-watcher"
};
fs.writeFile("./package.json", JSON.stringify(packageJSON, null, 2), function writeJSON(err) {
  if (err) return console.log(err);
  console.log(JSON.stringify(packageJSON));
  console.log("writing to " + "./package.json");
});';

mkdir components/Button;
echo "import React from 'react';
import {TouchableOpacity, Text, StyleSheet} from 'react-native';
interface MyButtonProps {
  onPress: () => void;
  text: string;
}
export const MyButton = ({onPress, text}: MyButtonProps) => {
  return (
    <TouchableOpacity style={styles.container} onPress={onPress}>
      <Text style={styles.text}>{text}</Text>
    </TouchableOpacity>
  );
};
const styles = StyleSheet.create({
  container: {
    paddingHorizontal: 16,
    paddingVertical: 8,
    backgroundColor: 'violet',
  },
  text: {color: 'black'},
});
" > components/Button/Button.tsx;

echo "import React from 'react';
import {ComponentStory, ComponentMeta} from '@storybook/react-native';
import {MyButton} from './Button';
const MyButtonMeta: ComponentMeta<typeof MyButton> = {
  title: 'MyButton',
  component: MyButton,
  argTypes: {
    onPress: {action: 'pressed the button'},
  },
  args: {
    text: 'Hello world',
  },
};
export default MyButtonMeta;
type MyButtonStory = ComponentStory<typeof MyButton>;
export const Basic: MyButtonStory = args => <MyButton {...args} />;
" > components/Button/Button.stories.tsx;

yarn sbn-get-stories;

yarn web

This causes the error regarding node_modules/@storybook/react-native/dist/preview/Preview.js.

Expected behavior No error should occur.

System:

Environment Info:

  System:
    OS: macOS 11.4
    CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
  Binaries:
    Node: 16.4.0 - ~/.nvm/versions/node/v16.4.0/bin/node
    Yarn: 1.22.10 - ~/.nvm/versions/node/v16.4.0/bin/yarn
    npm: 7.19.0 - ~/.nvm/versions/node/v16.4.0/bin/npm
  Browsers:
    Chrome: 92.0.4515.107
    Safari: 14.1.1
  npmPackages:
    @storybook/addon-actions: ^6.3.6 => 6.3.6 
    @storybook/addon-controls: ^6.3.6 => 6.3.6 
    @storybook/addon-ondevice-actions: ^6.0.1-alpha.0 => 6.0.1-alpha.0 
    @storybook/addon-ondevice-backgrounds: ^6.0.1-alpha.0 => 6.0.1-alpha.0 
    @storybook/addon-ondevice-controls: ^6.0.1-alpha.0 => 6.0.1-alpha.0 
    @storybook/addon-ondevice-notes: ^6.0.1-alpha.0 => 6.0.1-alpha.0 
    @storybook/react-native: ^6.0.1-alpha.1 => 6.0.1-alpha.1 

Additional context The problem looks related to the class properties in node_modules/@storybook/react-native/dist/preview/Preview.js. The 6.0.1-alpha.1 version has class properties. The corresponding 5.3.25 code does not have them.

This might be resolved by removing "target": "ESNext", and "lib": ["ESNext"], from the 6.0 tsconfig.json: https://github.com/storybookjs/react-native/blob/next-6.0/app/react-native/tsconfig.json#L9 . The 5.3.25 version of tsconfig.json does not have those, so Typescript defaults to output ES3 code. That’s probably the best choice for the code we distribute in the NPM packages, as ES3 code should be runnable by pretty much anything without additional transpilation.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 16 (14 by maintainers)

Commits related to this issue

Most upvoted comments

We discussed the need for metro.config.js with @dannyhw. Seems that omitting it (as I suggested above) would cause issues with promises, see https://github.com/storybookjs/react-native/issues/57 .

After some experimenting and reading Expo’s customizing Metro docs , this looks like a promising Expo version of the metro.config.js:

const { getDefaultConfig } = require("expo/metro-config");

const defaultConfig = getDefaultConfig(__dirname);

defaultConfig.resolver.resolverMainFields = [
  "sbmodern",
  ...defaultConfig.resolver.resolverMainFields,
];
defaultConfig.transformer.getTransformOptions = async () => ({
  transform: {
    experimentalImportSupport: false,
    inlineRequires: false,
  },
});
module.exports = defaultConfig;

So, the whole setup for Expo would be

npm install --global expo-cli
expo init appName
(select blank TypeScript template)
cd appName

yarn add @storybook/react-native@next \
            @react-native-async-storage/async-storage \
            @storybook/addon-ondevice-actions@next \
            @storybook/addon-ondevice-controls@next \
            @storybook/addon-ondevice-backgrounds@next \
            @storybook/addon-ondevice-notes@next \
            @storybook/addon-actions \
            @react-native-community/datetimepicker \
            @react-native-community/slider \
            @storybook/addon-controls

echo "const { getDefaultConfig } = require('expo/metro-config');

const defaultConfig = getDefaultConfig(__dirname);

defaultConfig.resolver.resolverMainFields = [
  'sbmodern',
  ...defaultConfig.resolver.resolverMainFields,
];
defaultConfig.transformer.getTransformOptions = async () => ({
  transform: {
    experimentalImportSupport: false,
    inlineRequires: false,
  },
});
module.exports = defaultConfig;
" > metro.config.js;

mkdir .storybook;
mkdir components;
echo "module.exports = {
  stories: [
    './components/**/*.stories.?(ts|tsx|js|jsx)'
  ],
   addons: [
    '@storybook/addon-ondevice-notes',
    '@storybook/addon-ondevice-controls',
    '@storybook/addon-ondevice-backgrounds',
    '@storybook/addon-ondevice-actions',
  ],
};" > .storybook/main.js;

echo "import {withBackgrounds} from '@storybook/addon-ondevice-backgrounds';
export const decorators = [withBackgrounds];
export const parameters = {
  backgrounds: [
    {name: 'plain', value: 'white', default: true},
    {name: 'warm', value: 'hotpink'},
    {name: 'cool', value: 'deepskyblue'},
  ],
};" > .storybook/preview.js;

echo "import AsyncStorage from '@react-native-async-storage/async-storage';
import { getStorybookUI } from '@storybook/react-native';
import './storybook.requires';
const StorybookUIRoot = getStorybookUI({
  asyncStorage: AsyncStorage,
});
export default StorybookUIRoot;" > Storybook.tsx;

echo "import StorybookUIRoot from './Storybook';
export { StorybookUIRoot as default };" > App.tsx;

node -e 'const fs = require("fs");
const packageJSON = require("./package.json");
packageJSON.scripts = {
    ...packageJSON.scripts,
    prestart: "sbn-get-stories",
    "storybook-watcher": "sbn-watcher"
};
fs.writeFile("./package.json", JSON.stringify(packageJSON, null, 2), function writeJSON(err) {
  if (err) return console.log(err);
  console.log(JSON.stringify(packageJSON));
  console.log("writing to " + "./package.json");
});';

mkdir components/Button;
echo "import React from 'react';
import {TouchableOpacity, Text, StyleSheet} from 'react-native';
interface MyButtonProps {
  onPress: () => void;
  text: string;
}
export const MyButton = ({onPress, text}: MyButtonProps) => {
  return (
    <TouchableOpacity style={styles.container} onPress={onPress}>
      <Text style={styles.text}>{text}</Text>
    </TouchableOpacity>
  );
};
const styles = StyleSheet.create({
  container: {
    paddingHorizontal: 16,
    paddingVertical: 8,
    backgroundColor: 'violet',
  },
  text: {color: 'black'},
});
" > components/Button/Button.tsx;

echo "import React from 'react';
import {ComponentStory, ComponentMeta} from '@storybook/react-native';
import {MyButton} from './Button';
const MyButtonMeta: ComponentMeta<typeof MyButton> = {
  title: 'MyButton',
  component: MyButton,
  argTypes: {
    onPress: {action: 'pressed the button'},
  },
  args: {
    text: 'Hello world',
  },
};
export default MyButtonMeta;
type MyButtonStory = ComponentStory<typeof MyButton>;
export const Basic: MyButtonStory = args => <MyButton {...args} />;
" > components/Button/Button.stories.tsx;

yarn sbn-get-stories

With this metro.config.js, promises seemed to work on Expo. Without it, they failed.

Some issues still seem to persist, though. The Promise example https://github.com/storybookjs/react-native/blob/next-6.0/examples/native/components/PromiseTest/Button.stories.tsx wouldn’t run as-is on Expo. Instead, it errored out due to the React hooks:

Warning: React has detected a change in the order of Hooks called by StoryView. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks

   Previous render            Next render
   ------------------------------------------------------
1. useState                   useState
2. useEffect                  useEffect
3. undefined                  useState
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    in StoryView (at OnDeviceUI.js:92)
    in RCTView (at View.js:34)
    in View (created by Context.Consumer)
    in emotion(View) (at OnDeviceUI.js:91)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at OnDeviceUI.js:90)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at OnDeviceUI.js:89)
    in RCTView (at View.js:34)
    in View (at absolute-positioned-keyboard-aware-view.js:13)
    in RCTView (at View.js:34)
    in View (at absolute-positioned-keyboard-aware-view.js:12)
    in AbsolutePositionedKeyboardAwareView (at OnDeviceUI.js:88)
    in RCTView (at View.js:34)
    in View (at KeyboardAvoidingView.js:204)
    in KeyboardAvoidingView (at OnDeviceUI.js:87)
    in RCTSafeAreaView (at SafeAreaView.js:51)
    in ForwardRef(SafeAreaView) (at OnDeviceUI.js:86)
    in OnDeviceUI (at Preview.js:59)
    in ThemeProvider (at Preview.js:58)
    in Unknown (created by ExpoRoot)
    in ExpoRoot (at renderApplication.js:45)
    in RCTView (at View.js:34)
    in View (at AppContainer.js:106)
    in DevAppContainer (at AppContainer.js:121)
    in RCTView (at View.js:34)
    in View (at AppContainer.js:132)
    in AppContainer (at renderApplication.js:39)
at components/Button/ButtonPromise.stories.tsx:9:30 in storiesOf.add$argument_1
at node_modules/@storybook/client-api/dist/modern/story_store.js:505:37 in finalStoryFn
at node_modules/@storybook/addons/dist/modern/hooks.js:116:17 in <anonymous>
at node_modules/@storybook/client-api/dist/modern/decorators.js:47:27 in <anonymous>
at node_modules/@storybook/addon-ondevice-backgrounds/dist/index.js:46:140 in addons_1.makeDecorator$argument_0.wrapper
at node_modules/@storybook/addons/dist/modern/make-decorator.js:18:18 in <anonymous>
at node_modules/@storybook/addons/dist/modern/make-decorator.js:27:13 in <anonymous>
at node_modules/@storybook/addons/dist/modern/hooks.js:116:17 in <anonymous>
at node_modules/@storybook/client-api/dist/modern/decorators.js:19:29 in <anonymous>
at node_modules/@storybook/client-api/dist/modern/decorators.js:53:36 in <anonymous>
at node_modules/@storybook/addons/dist/modern/hooks.js:143:26 in <anonymous>
at node_modules/@storybook/client-api/dist/modern/story_store.js:560:50 in unboundStoryFn
at node_modules/@storybook/react-native/dist/preview/components/StoryView/StoryView.js:27:60 in StoryView
at node_modules/@storybook/react-native/dist/preview/components/StoryView/StoryView.js:17:28 in loadContext

Error: Rendered more hooks than during the previous render.

This error is located at:
    in StoryView (at OnDeviceUI.js:92)
    in RCTView (at View.js:34)
    in View (created by Context.Consumer)
    in emotion(View) (at OnDeviceUI.js:91)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at OnDeviceUI.js:90)
    in RCTView (at View.js:34)
    in View (at createAnimatedComponent.js:165)
    in AnimatedComponent (at createAnimatedComponent.js:215)
    in ForwardRef(AnimatedComponentWrapper) (at OnDeviceUI.js:89)
    in RCTView (at View.js:34)
    in View (at absolute-positioned-keyboard-aware-view.js:13)
    in RCTView (at View.js:34)
    in View (at absolute-positioned-keyboard-aware-view.js:12)
    in AbsolutePositionedKeyboardAwareView (at OnDeviceUI.js:88)
    in RCTView (at View.js:34)
    in View (at KeyboardAvoidingView.js:204)
    in KeyboardAvoidingView (at OnDeviceUI.js:87)
    in RCTSafeAreaView (at SafeAreaView.js:51)
    in ForwardRef(SafeAreaView) (at OnDeviceUI.js:86)
    in OnDeviceUI (at Preview.js:59)
    in ThemeProvider (at Preview.js:58)
    in Unknown (created by ExpoRoot)
    in ExpoRoot (at renderApplication.js:45)
    in RCTView (at View.js:34)
    in View (at AppContainer.js:106)
    in DevAppContainer (at AppContainer.js:121)
    in RCTView (at View.js:34)
    in View (at AppContainer.js:132)
    in AppContainer (at renderApplication.js:39)
at node_modules/react-native/Libraries/Core/ExceptionsManager.js:104:6 in reportException
at node_modules/react-native/Libraries/Core/ExceptionsManager.js:171:19 in handleException
at node_modules/react-native/Libraries/Core/ReactFiberErrorDialog.js:43:2 in showErrorDialog
at node_modules/@storybook/react-native/dist/preview/components/StoryView/StoryView.js:17:28 in loadContext

[Unhandled promise rejection: Error: Rendered more hooks than during the previous render.]
at components/Button/ButtonPromise.stories.tsx:9:30 in storiesOf.add$argument_1
at node_modules/@storybook/client-api/dist/modern/story_store.js:505:37 in finalStoryFn
at node_modules/@storybook/addons/dist/modern/hooks.js:116:17 in <anonymous>
at node_modules/@storybook/client-api/dist/modern/decorators.js:47:27 in <anonymous>
at node_modules/@storybook/addon-ondevice-backgrounds/dist/index.js:46:140 in addons_1.makeDecorator$argument_0.wrapper
at node_modules/@storybook/addons/dist/modern/make-decorator.js:18:18 in <anonymous>
at node_modules/@storybook/addons/dist/modern/make-decorator.js:27:13 in <anonymous>
at node_modules/@storybook/addons/dist/modern/hooks.js:116:17 in <anonymous>
at node_modules/@storybook/client-api/dist/modern/decorators.js:19:29 in <anonymous>
at node_modules/@storybook/client-api/dist/modern/decorators.js:53:36 in <anonymous>
at node_modules/@storybook/addons/dist/modern/hooks.js:143:26 in <anonymous>
at node_modules/@storybook/client-api/dist/modern/story_store.js:560:50 in unboundStoryFn
at node_modules/@storybook/react-native/dist/preview/components/StoryView/StoryView.js:27:60 in StoryView
at node_modules/@storybook/react-native/dist/preview/components/StoryView/StoryView.js:17:28 in loadContext

After removing the hooks (replacing the text changes with Alert notifications) the example ran fine on Expo.

In case anyone else ends up here while searching for a solution to React has detected a change in the order of Hooks called by StoryView, add this to .ondevice/preview.js:

const withHooks = (Story) => <Story />

export const decorators = [..., withHooks]