UNPKG

@storybook/react

Version:

Storybook for React: Develop React Component in isolation with Hot Reloading.

154 lines (130 loc) 4.02 kB
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); }