Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React: Add support for react18's new root API #17215

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ae474c2
Add possibility of using new React Root Api
Jan 12, 2022
fee8459
Reverse logic of callback div in CallbackWrapper
Jan 13, 2022
fc5128f
Use FRAMEWORK_OPTIONS to use new React Root API conditionally
Jan 13, 2022
983534d
docs: Add documentation to faq page
valentinpalkovic Jan 26, 2022
0479139
Update docs/faq.md
valentinpalkovic Feb 10, 2022
d1d8cff
Update docs/faq.md
valentinpalkovic Feb 10, 2022
b50177d
Rename newREactRootApi to newRootApi
Feb 10, 2022
afed20a
Prevent memory leaks and possible race conditions
Feb 10, 2022
505f99b
Transform class-based CallbackWrapper to function-based component
Feb 10, 2022
ca4bced
Simplify structure of getReactRoot
Feb 10, 2022
4899f37
Use setTimeout instead of CallbackWrapper
Feb 10, 2022
5e3f711
Rename types field from newReactRootApi to newRootApi
Feb 10, 2022
55ec6d4
Merge branch 'next' into render-react-components-with-new-root-api
yannbf Mar 25, 2022
b908cf3
add react 18 config to e2e scripts
yannbf Mar 25, 2022
544dfd5
Adjust loading of new root api to newest React 18.0.0 release
Apr 1, 2022
d1e4a71
Merge branch 'next' into pr/17215
shilman Apr 3, 2022
cd73c30
Merge branch 'next' into render-react-components-with-new-root-api
valentinpalkovic Apr 4, 2022
aedb6f6
Replace opt-in ReactRoot Api flag by opt-out flag
Apr 5, 2022
c0b53a5
Fix dynamic import resolution
Apr 5, 2022
677c189
wip
ndelangen Apr 5, 2022
eb73376
I think this makes michael happy
ndelangen Apr 5, 2022
262409b
Fix lint
shilman Apr 6, 2022
699cd79
cleanup
ndelangen Apr 6, 2022
465dc92
cleanup
ndelangen Apr 6, 2022
4ab777e
Merge branch 'next' into ndelangen/temp-alternative-solution-react-dom
ndelangen Apr 6, 2022
3f2d50a
cleanup
ndelangen Apr 6, 2022
9a8ebd5
cleanup
ndelangen Apr 6, 2022
6bc6913
Merge branch 'next' into ndelangen/temp-alternative-solution-react-dom
ndelangen Apr 6, 2022
b907b9e
Fix corrupted webpack configuration
Apr 6, 2022
0b36354
Merge branch 'next' into pr/17215
shilman Apr 6, 2022
b7422ff
Update snapshots
shilman Apr 6, 2022
0f118a3
Support react experimental versions
Apr 6, 2022
bcb93ec
React: Add react18 new root API migration instructions
shilman Apr 7, 2022
40747e0
Merge branch 'render-react-components-with-new-root-api' of https://g…
shilman Apr 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Expand Up @@ -207,7 +207,7 @@ jobs:
name: Run E2E (core) tests
# Do not test CRA here because it's done in PnP part
# TODO: Remove `web_components_typescript` as soon as Lit 2 stable is released
command: yarn test:e2e-framework vue3 angular130 angular13 angular12 angular11 web_components_typescript web_components_lit2 react vite_react
command: yarn test:e2e-framework vue3 angular130 angular13 angular12 angular11 web_components_typescript web_components_lit2 react react_legacy_root_api vite_react
no_output_timeout: 5m
- store_artifacts:
path: /tmp/cypress-record
Expand Down
19 changes: 16 additions & 3 deletions MIGRATION.md
@@ -1,6 +1,7 @@
<h1>Migration</h1>

- [From version 6.4.x to 6.5.0](#from-version-64x-to-650)
- [React18 new root API](#react18-new-root-api)
- [Deprecated register.js](#deprecated-registerjs)
- [Dropped support for addon-actions addDecorators](#dropped-support-for-addon-actions-adddecorators)
- [Vite builder renamed](#vite-builder-renamed)
Expand Down Expand Up @@ -199,20 +200,32 @@

## From version 6.4.x to 6.5.0

### React18 new root API

React 18 introduces a [new root API](https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis). Starting in 6.5, Storybook for React will auto-detect your react version and use the new root API automatically if you're on React18.

If you wish to opt out of the new root API, set the `reactOptions.legacyRootApi` flag in your `.storybook/main.js` config:

```js
module.exports = {
reactOptions: { legacyRootApi: true },
};
```

### Deprecated register.js

In ancient versions of Storybook, addons were registered by referring to `addon-name/register.js`. This is going away in SB7.0. Instead you should just add `addon-name` to the `addons` array in `.storybook/main.js`.

Before:
Before:

```js
module.exports = { addons: ['my-addon/register.js'] }
module.exports = { addons: ['my-addon/register.js'] };
```

After:

```js
module.exports = { addons: ['my-addon'] }
module.exports = { addons: ['my-addon'] };
```

### Dropped support for addon-actions addDecorators
Expand Down
1 change: 1 addition & 0 deletions app/react/package.json
Expand Up @@ -69,6 +69,7 @@
"babel-plugin-react-docgen": "^4.2.1",
"core-js": "^3.8.2",
"escodegen": "^2.0.0",
"fs-extra": "^9.0.1",
"global": "^4.4.0",
"html-tags": "^3.1.0",
"lodash": "^4.17.21",
Expand Down
64 changes: 58 additions & 6 deletions app/react/src/client/preview/render.tsx
Expand Up @@ -6,15 +6,20 @@ import React, {
StrictMode,
Fragment,
} from 'react';
import ReactDOM from 'react-dom';
import { RenderContext } from '@storybook/store';
import ReactDOM, { version as reactDomVersion } from 'react-dom';
import type { Root as ReactRoot } from 'react-dom/client';

import type { RenderContext } from '@storybook/store';
import { ArgsStoryFn } from '@storybook/csf';

import { StoryContext } from './types';
import { ReactFramework } from './types-6-0';

const { FRAMEWORK_OPTIONS } = global;

// A map of all rendered React 18 nodes
const nodes = new Map<Element, ReactRoot>();

export const render: ArgsStoryFn<ReactFramework> = (args, context) => {
const { id, component: Component } = context;
if (!Component) {
Expand All @@ -26,10 +31,57 @@ export const render: ArgsStoryFn<ReactFramework> = (args, context) => {
return <Component {...args} />;
};

const renderElement = async (node: ReactElement, el: Element) =>
new Promise((resolve) => {
ReactDOM.render(node, el, () => resolve(null));
const renderElement = async (node: ReactElement, el: Element) => {
// Create Root Element conditionally for new React 18 Root Api
const root = await getReactRoot(el);

return new Promise((resolve) => {
if (root) {
root.render(node);
setTimeout(() => {
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
resolve(null);
}, 0);
} else {
ReactDOM.render(node, el, () => resolve(null));
}
});
};

const canUseNewReactRootApi =
reactDomVersion.startsWith('18') || reactDomVersion.startsWith('0.0.0');

const shouldUseNewRootApi = FRAMEWORK_OPTIONS?.legacyRootApi !== true;

const isUsingNewReactRootApi = shouldUseNewRootApi && canUseNewReactRootApi;

const unmountElement = (el: Element) => {
const root = nodes.get(el);
if (root && isUsingNewReactRootApi) {
root.unmount();
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
nodes.delete(el);
} else {
ReactDOM.unmountComponentAtNode(el);
}
};

const getReactRoot = async (el: Element): Promise<ReactRoot | null> => {
if (!isUsingNewReactRootApi) {
return null;
valentinpalkovic marked this conversation as resolved.
Show resolved Hide resolved
}

let root = nodes.get(el);

if (!root) {
// Skipping webpack's static analysis of import paths by defining the path value outside the import statement.
// eslint-disable-next-line import/no-unresolved
const reactDomClient = await import('react-dom/client');
root = reactDomClient.createRoot(el);

nodes.set(el, root);
}

return root;
};

class ErrorBoundary extends ReactComponent<{
showException: (err: Error) => void;
Expand Down Expand Up @@ -92,7 +144,7 @@ export async function renderToDOM(
// https://github.com/storybookjs/react-storybook/issues/81
// (This is not the case when we change args or globals to the story however)
if (forceRemount) {
ReactDOM.unmountComponentAtNode(domElement);
unmountElement(domElement);
}

await renderElement(element, domElement);
Expand Down
22 changes: 22 additions & 0 deletions app/react/src/server/framework-preset-react-dom-hack.ts
@@ -0,0 +1,22 @@
import { readJSON } from 'fs-extra';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you just need JSON.parse(await readFile("filename.json", "utf8")) ...

Add one dependence only for this feature is costly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs-extra is used in other parts of the storybook ecosystem, therefore @ndelangen decided to just use it without thinking too much about the bundle size. For sure, we can remove the fs-extra part, if JSON.parse(await readFile) doesn't introduce a different behavior. @ndelangen What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging this for now to keep things moving. Feel free to address this in a followup PR. I agree we should be trimming down our dependencies wherever possible if it doesn't add complexity, but could go either way on this

import { Configuration, IgnorePlugin } from 'webpack';

// this is a hack to allow importing react-dom/client even when it's not available
// this should be removed once we drop support for react-dom < 18

export async function webpackFinal(config: Configuration) {
const reactDomPkg = await readJSON(require.resolve('react-dom/package.json'));

return {
...config,
plugins: [
...config.plugins,
reactDomPkg.version.startsWith('18') || reactDomPkg.version.startsWith('0.0.0')
? null
: new IgnorePlugin({
resourceRegExp: /react-dom\/client$/,
contextRegExp: /(app\/react|@storybook\/react)/, // TODO this needs to work for both in our MONOREPO and in the user's NODE_MODULES
}),
].filter(Boolean),
};
}
1 change: 1 addition & 0 deletions app/react/src/server/framework-preset-react.ts
Expand Up @@ -87,6 +87,7 @@ export async function webpackFinal(config: Configuration, options: Options) {
...config,
plugins: [
...config.plugins,

// Storybook uses webpack-hot-middleware https://github.com/storybookjs/storybook/issues/14114
new ReactRefreshWebpackPlugin({
overlay: {
Expand Down
1 change: 1 addition & 0 deletions app/react/src/server/preset.ts
Expand Up @@ -8,6 +8,7 @@ export const previewAnnotations: StorybookConfig['previewAnnotations'] = (entrie

export const addons: StorybookConfig['addons'] = [
require.resolve('./framework-preset-react'),
require.resolve('./framework-preset-react-dom-hack'),
require.resolve('./framework-preset-cra'),
require.resolve('./framework-preset-react-docs'),
];
42 changes: 42 additions & 0 deletions app/react/src/typings.d.ts
@@ -1,2 +1,44 @@
declare module '@storybook/semver';
declare module 'global';

// TODO: Replace, as soon as @types/react-dom 17.0.14 is used
// Source: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fb0f14b7a35cde26ffaa82e7536c062e593e9ae6/types/react-dom/client.d.ts
declare module 'react-dom/client' {

import React = require('react');
export interface HydrationOptions {
onHydrated?(suspenseInstance: Comment): void;
onDeleted?(suspenseInstance: Comment): void;
/**
* Prefix for `useId`.
*/
identifierPrefix?: string;
onRecoverableError?: (error: unknown) => void;
}

export interface RootOptions {
/**
* Prefix for `useId`.
*/
identifierPrefix?: string;
onRecoverableError?: (error: unknown) => void;
}

export interface Root {
render(children: React.ReactChild | Iterable<React.ReactNode>): void;
unmount(): void;
}

/**
* Replaces `ReactDOM.render` when the `.render` method is called and enables Concurrent Mode.
*
* @see https://reactjs.org/docs/concurrent-mode-reference.html#createroot
*/
export function createRoot(container: Element | Document | DocumentFragment | Comment, options?: RootOptions): Root;

export function hydrateRoot(
container: Element | Document | DocumentFragment | Comment,
initialChildren: React.ReactChild | Iterable<React.ReactNode>,
options?: HydrationOptions,
): Root;
}
8 changes: 8 additions & 0 deletions app/react/types/index.ts
Expand Up @@ -7,5 +7,13 @@ export interface StorybookConfig extends BaseConfig {
reactOptions?: {
fastRefresh?: boolean;
strictMode?: boolean;
/**
* Use React's legacy root API to mount components
* @description
* React has introduced a new root API with React 18.x to enable a whole set of new features (e.g. concurrent features)
* If this flag is true, the legacy Root API is used to mount components to make it easier to migrate step by step to React 18.
* @default false
*/
legacyRootApi?: boolean;
};
}
22 changes: 17 additions & 5 deletions docs/faq.md
Expand Up @@ -83,6 +83,20 @@ module.exports = {
💡 Fast Refresh only works in development mode with React 16.10 or higher.
</div>

### How do I setup the new React Context Root API with Storybook?

If your installed React Version equals or is higher than 18.0.0, the new React Root API is automatically used and the newest React [concurrent features](https://reactjs.org/docs/concurrent-mode-intro.html) can be used.

You can opt-out from the new React Root API by setting the following property in your `.storybook/main.js` file:

```js
module.exports = {
reactOptions: {
legacyRootApi: true,
},
};
```

### Why is there no addons channel?

A common error is that an addon tries to access the "channel", but the channel is not set. It can happen in a few different cases:
Expand All @@ -97,7 +111,6 @@ A common error is that an addon tries to access the "channel", but the channel i

2. In React Native, it's a special case documented in [#1192](https://github.com/storybookjs/storybook/issues/1192)


### Why aren't Controls visible in the Canvas panel but visible in the Docs panel?

If you're adding Storybook's dependencies manually, make sure you include the [`@storybook/addon-controls`](https://www.npmjs.com/package/@storybook/addon-controls) dependency in your project and reference it in your `.storybook/main.js` as follows:
Expand Down Expand Up @@ -133,7 +146,7 @@ With the release of version 6.0, we updated our documentation as well. That does
We're only covering versions 5.3 and 5.0 as they were important milestones for Storybook. If you want to go back in time a little more, you'll have to check the specific release in the monorepo.

| Section | Page | Current Location | Version 5.3 location | Version 5.0 location |
|------------------|-------------------------------------------|------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| ---------------- | ----------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Get started | Install | [See current documentation](../get-started/install.md) | [See versioned documentation](https://github.com/storybookjs/storybook/tree/release/5.3/docs/src/pages/guides/quick-start-guide) | [See versioned documentation](https://github.com/storybookjs/storybook/tree/release/5.0/docs/src/pages/guides/quick-start-guide) |
| | What's a story | [See current documentation](../get-started/whats-a-story.md) | [See versioned documentation for your framework](https://github.com/storybookjs/storybook/blob/release/5.3/docs/src/pages/guides) | [See versioned documentation for your framework](https://github.com/storybookjs/storybook/blob/release/5.0/docs/src/pages/guides) |
| | Browse Stories | [See current documentation](../get-started/browse-stories.md) | [See versioned documentation for your framework](https://github.com/storybookjs/storybook/blob/release/5.3/docs/src/pages/guides) | [See versioned documentation for your framework](https://github.com/storybookjs/storybook/blob/release/5.0/docs/src/pages/guides) |
Expand Down Expand Up @@ -193,6 +206,7 @@ We're only covering versions 5.3 and 5.0 as they were important milestones for S
| | Stories/StoriesOF format (see note below) | [See current documentation](../../lib/core/docs/storiesOf.md) | [See versioned documentation](https://github.com/storybookjs/storybook/tree/release/5.3/docs/src/pages/formats/storiesof-api) | Non existing feature or undocumented |
| | Frameworks | [See current documentation](../api/new-frameworks.md) | Non existing feature or undocumented | Non existing feature or undocumented |
| | CLI options | [See current documentation](../api/cli-options.md) | [See versioned documentation](https://github.com/storybookjs/storybook/tree/release/5.3/docs/src/pages/configurations/cli-options) | [See versioned documentation](https://github.com/storybookjs/storybook/tree/release/5.0/docs/src/pages/configurations/cli-options) |

<div class="aside">
With the release of version 5.3, we've updated how you can write your stories more compactly and easily. It doesn't mean that the <code>storiesOf</code> format has been removed. For the time being, we're still supporting it, and we have documentation for it. But be advised that this is bound to change in the future.
</div>
Expand Down Expand Up @@ -360,12 +374,10 @@ export default meta;

Although valid, it introduces additional boilerplate code to the story definition. Instead, we're working towards implementing a safer mechanism based on what's currently being discussed in the following [issue](https://github.com/microsoft/TypeScript/issues/7481). Once the feature is released, we'll migrate our existing examples and documentation accordingly.


## Why is Storybook's source loader returning undefined with curried functions?

This is a known issue with Storybook. If you're interested in getting it fixed, open an issue with a [working reproduction](./contribute/how-to-reproduce) so that it can be triaged and fixed in future releases.


## Why are my args no longer displaying the default values?

Before version 6.3, unset args were set to the `argTypes.defaultValue` if specified or inferred from the component's properties (e.g., React's prop types, Angular inputs, Vue props). Starting with version 6.3, Storybook no longer infers default values but instead defines the arg's value as `undefined` when unset, allowing the framework to supply its default value.
Expand Down Expand Up @@ -400,4 +412,4 @@ export default {
},
},
};
```
```
13 changes: 13 additions & 0 deletions lib/cli/src/repro-generators/configs.ts
Expand Up @@ -64,6 +64,19 @@ export const react: Parameters = {
additionalDeps: ['prop-types'],
};

export const react_legacy_root_api: Parameters = {
framework: 'react',
name: 'react_legacy_root_api',
version: 'latest',
generator: fromDeps('react', 'react-dom'),
additionalDeps: ['prop-types'],
mainOverrides: {
reactOptions: {
legacyRootApi: true,
},
},
};

export const react_typescript: Parameters = {
framework: 'react',
name: 'react_typescript',
Expand Down
Expand Up @@ -474,6 +474,7 @@ Object {
"IgnorePlugin",
"ForkTsCheckerWebpackPlugin",
"ESLintWebpackPlugin",
"IgnorePlugin",
"DocgenPlugin",
],
}
Expand Down
Expand Up @@ -491,6 +491,7 @@ Object {
"IgnorePlugin",
"ForkTsCheckerWebpackPlugin",
"ESLintWebpackPlugin",
"IgnorePlugin",
"DocgenPlugin",
],
}
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Expand Up @@ -8136,6 +8136,7 @@ __metadata:
babel-plugin-react-docgen: ^4.2.1
core-js: ^3.8.2
escodegen: ^2.0.0
fs-extra: ^9.0.1
global: ^4.4.0
html-tags: ^3.1.0
lodash: ^4.17.21
Expand Down