diff --git a/addons/docs/src/blocks/useStory.ts b/addons/docs/src/blocks/useStory.ts index 459abb13b27c..e6266744eeb7 100644 --- a/addons/docs/src/blocks/useStory.ts +++ b/addons/docs/src/blocks/useStory.ts @@ -21,11 +21,19 @@ export function useStories( useEffect(() => { Promise.all( storyIds.map(async (storyId) => { + // loadStory will be called every single time useStory is called + // because useEffect does not use storyIds as an input. This is because + // HMR can change the story even when the storyId hasn't changed. However, it + // will be a no-op once the story has loaded. Furthermore, the `story` will + // have an exact equality when the story hasn't changed, so it won't trigger + // any unnecessary re-renders const story = await context.loadStory(storyId); - setStories((current) => ({ ...current, [storyId]: story })); + setStories((current) => + current[storyId] === story ? current : { ...current, [storyId]: story } + ); }) ); - }, storyIds); + }); return storyIds.map((storyId) => storiesById[storyId]); } diff --git a/addons/docs/src/frameworks/angular/prepareForInline.ts b/addons/docs/src/frameworks/angular/prepareForInline.ts index 76be79a3f4ac..3e07c842248a 100644 --- a/addons/docs/src/frameworks/angular/prepareForInline.ts +++ b/addons/docs/src/frameworks/angular/prepareForInline.ts @@ -13,23 +13,31 @@ const limit = pLimit(1); */ export const prepareForInline = ( storyFn: PartialStoryFn, - { id, parameters }: StoryContext + { id, parameters, component }: StoryContext ) => { - return React.createElement('div', { - ref: async (node?: HTMLDivElement): Promise => { - if (!node) { - return null; - } + const el = React.useRef(); - return limit(async () => { - const renderer = await rendererFactory.getRendererInstance(`${id}-${nanoid(10)}`, node); - await renderer.render({ - forced: false, - parameters, - storyFnAngular: storyFn(), - targetDOMNode: node, - }); + React.useEffect(() => { + (async () => { + limit(async () => { + const renderer = await rendererFactory.getRendererInstance( + `${id}-${nanoid(10)}`.toLowerCase(), + el.current + ); + if (renderer) { + await renderer.render({ + forced: false, + component, + parameters, + storyFnAngular: storyFn(), + targetDOMNode: el.current, + }); + } }); - }, + })(); + }); + + return React.createElement('div', { + ref: el, }); }; diff --git a/app/angular/src/client/preview/angular-beta/RendererFactory.ts b/app/angular/src/client/preview/angular-beta/RendererFactory.ts index f3983634e886..d781233c6364 100644 --- a/app/angular/src/client/preview/angular-beta/RendererFactory.ts +++ b/app/angular/src/client/preview/angular-beta/RendererFactory.ts @@ -8,7 +8,17 @@ export class RendererFactory { private rendererMap = new Map(); - public async getRendererInstance(storyId: string, targetDOMNode: HTMLElement) { + public async getRendererInstance( + storyId: string, + targetDOMNode: HTMLElement + ): Promise { + // do nothing if the target node is null + // fix a problem when the docs asks 2 times the same component at the same time + // the 1st targetDOMNode of the 1st requested rendering becomes null 🤷‍♂️ + if (targetDOMNode === null) { + return null; + } + const renderType = getRenderType(targetDOMNode); // keep only instances of the same type if (this.lastRenderType && this.lastRenderType !== renderType) { diff --git a/cypress/generated/addon-docs.spec.ts b/cypress/generated/addon-docs.spec.ts index b3506c0bc86d..fa8505171585 100644 --- a/cypress/generated/addon-docs.spec.ts +++ b/cypress/generated/addon-docs.spec.ts @@ -6,6 +6,11 @@ describe('addon-action', () => { it('should have docs tab', () => { cy.navigateToStory('example-button', 'primary'); cy.viewAddonTab('Docs'); + + // MDX rendering cy.getDocsElement().find('h1').should('contain.text', 'Button'); + + // inline story rendering + cy.getDocsElement().find('button').should('contain.text', 'Button'); }); }); diff --git a/lib/preview-web/src/PreviewWeb.tsx b/lib/preview-web/src/PreviewWeb.tsx index dc565396f63d..3f0393166ffd 100644 --- a/lib/preview-web/src/PreviewWeb.tsx +++ b/lib/preview-web/src/PreviewWeb.tsx @@ -326,7 +326,7 @@ export class PreviewWeb { } async renderDocs({ story }: { story: Story }) { - const { id, title, name } = story; + const { id, title, name, componentId } = story; const element = this.view.prepareForDocs(); const csfFile: CSFFile = await this.storyStore.loadCSFFileByStoryId(id, { sync: false, @@ -368,11 +368,14 @@ export class PreviewWeb { docs.container || (({ children }: { children: Element }) => <>{children}); const Page: ComponentType = docs.page || NoDocs; + // Use `componentId` as a key so that we force a re-render every time + // we switch components const docsElement = ( - + ); + ReactDOM.render(docsElement, element, async () => { await Promise.all(renderingStoryPromises); this.channel.emit(Events.DOCS_RENDERED, id);