@storybook/react
Version:
Storybook for React: Develop React Component in isolation with Hot Reloading.
154 lines (130 loc) • 4.02 kB
JavaScript
import global from 'global';
import React, { Component as ReactComponent, StrictMode, Fragment, useLayoutEffect, useRef } from 'react';
import ReactDOM, { version as reactDomVersion } from 'react-dom';
const {
FRAMEWORK_OPTIONS
} = global; // A map of all rendered React 18 nodes
const nodes = new Map();
export const render = (args, context) => {
const {
id,
component: Component
} = context;
if (!Component) {
throw new Error(`Unable to render story ${id} as the component annotation is missing from the default export`);
}
return /*#__PURE__*/React.createElement(Component, args);
};
const WithCallback = ({
callback,
children
}) => {
// See https://github.com/reactwg/react-18/discussions/5#discussioncomment-2276079
const once = useRef();
useLayoutEffect(() => {
if (once.current === callback) return;
once.current = callback;
callback();
}, [callback]);
return children;
};
const renderElement = async (node, el) => {
// Create Root Element conditionally for new React 18 Root Api
const root = await getReactRoot(el);
return new Promise(resolve => {
if (root) {
root.render( /*#__PURE__*/React.createElement(WithCallback, {
callback: () => resolve(null)
}, node));
} else {
ReactDOM.render(node, el, () => resolve(null));
}
});
};
const canUseNewReactRootApi = reactDomVersion && (reactDomVersion.startsWith('18') || reactDomVersion.startsWith('0.0.0'));
const shouldUseNewRootApi = (FRAMEWORK_OPTIONS === null || FRAMEWORK_OPTIONS === void 0 ? void 0 : FRAMEWORK_OPTIONS.legacyRootApi) !== true;
const isUsingNewReactRootApi = shouldUseNewRootApi && canUseNewReactRootApi;
const unmountElement = el => {
const root = nodes.get(el);
if (root && isUsingNewReactRootApi) {
root.unmount();
nodes.delete(el);
} else {
ReactDOM.unmountComponentAtNode(el);
}
};
const getReactRoot = async el => {
if (!isUsingNewReactRootApi) {
return null;
}
let root = nodes.get(el);
if (!root) {
// eslint-disable-next-line import/no-unresolved
const reactDomClient = (await import('react-dom/client')).default;
root = reactDomClient.createRoot(el);
nodes.set(el, root);
}
return root;
};
class ErrorBoundary extends ReactComponent {
constructor(...args) {
super(...args);
this.state = {
hasError: false
};
}
static getDerivedStateFromError() {
return {
hasError: true
};
}
componentDidMount() {
const {
hasError
} = this.state;
const {
showMain
} = this.props;
if (!hasError) {
showMain();
}
}
componentDidCatch(err) {
const {
showException
} = this.props; // message partially duplicates stack, strip it
showException(err);
}
render() {
const {
hasError
} = this.state;
const {
children
} = this.props;
return hasError ? null : children;
}
}
const Wrapper = FRAMEWORK_OPTIONS !== null && FRAMEWORK_OPTIONS !== void 0 && FRAMEWORK_OPTIONS.strictMode ? StrictMode : Fragment;
export async function renderToDOM({
storyContext,
unboundStoryFn,
showMain,
showException,
forceRemount
}, domElement) {
const Story = unboundStoryFn;
const content = /*#__PURE__*/React.createElement(ErrorBoundary, {
showMain: showMain,
showException: showException
}, /*#__PURE__*/React.createElement(Story, storyContext)); // For React 15, StrictMode & Fragment doesn't exists.
const element = Wrapper ? /*#__PURE__*/React.createElement(Wrapper, null, content) : content; // In most cases, we need to unmount the existing set of components in the DOM node.
// Otherwise, React may not recreate instances for every story run.
// This could leads to issues like below:
// 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) {
unmountElement(domElement);
}
await renderElement(element, domElement);
}