From edead64817843d81c1752c7aa52af12eacff8240 Mon Sep 17 00:00:00 2001 From: "Sinyoung \"Divinespear\" Kang" Date: Wed, 21 Jun 2023 19:19:26 +0000 Subject: [PATCH 1/4] poc: isolate stores per component --- packages/renderers/solid/src/render.tsx | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/renderers/solid/src/render.tsx b/packages/renderers/solid/src/render.tsx index ef1f89f..5cc158f 100644 --- a/packages/renderers/solid/src/render.tsx +++ b/packages/renderers/solid/src/render.tsx @@ -1,6 +1,6 @@ import type { Component } from 'solid-js'; import { ErrorBoundary, onMount } from 'solid-js'; -import { createStore, reconcile } from 'solid-js/store'; +import { SetStoreFunction, createStore, reconcile } from 'solid-js/store'; import { render as solidRender } from 'solid-js/web'; import type { RenderContext, @@ -14,7 +14,15 @@ import type { ComponentsData, SolidRenderer, StoryContext } from './types'; * SolidJS store for handling fine grained updates * of the components data as f.e. story args. */ -const [store, setStore] = createStore({} as ComponentsData); +const componentStores: { + [id: string]: [get: ComponentsData, set: SetStoreFunction]; +} = {}; +// const [store, setStore] = createStore({} as ComponentsData); +let store: ComponentsData; +let setStore: SetStoreFunction; + +const getStore = (componentId: string): typeof componentStores[string] => + componentStores[componentId] ||= createStore({} as ComponentsData); // Delay util fn const delay = async (ms: number = 20) => { @@ -67,6 +75,7 @@ export const solidReactivityDecorator = ( storyFn: StoryFn, context: StoryContext ) => { + const [store] = getStore(context.componentId); let storyId = context.canvasElement.id; context.args = store[storyId].args; return storyFn(context.args as Args & StoryContext, context); @@ -164,6 +173,7 @@ const renderSolidApp = ( ) => { const { storyContext, unboundStoryFn, showMain, showException } = renderContext; + const [_, setStore] = getStore(storyContext.componentId); setStore(storyId, 'rendered', true); @@ -202,9 +212,12 @@ export async function renderToCanvas( let storyId = storyContext.canvasElement.id; // Initializes global default values for checking remounting. - if (viewMode === undefined) viewMode = storyContext.viewMode; - if (globals === undefined) globals = storyContext.globals; - if (componentId === undefined) componentId = storyContext.componentId; + viewMode = storyContext.viewMode; + globals = storyContext.globals; + componentId = storyContext.componentId; + + // init store/setStore + [store, setStore] = getStore(componentId); // Story is remounted given the conditions. if (remount(forceRemount, storyContext)) { From 9b2be81cabdba431b8d8cba3bd823def86ff62a2 Mon Sep 17 00:00:00 2001 From: "Sinyoung \"Divinespear\" Kang" Date: Wed, 21 Jun 2023 20:02:22 +0000 Subject: [PATCH 2/4] encapsule stores per context --- packages/renderers/solid/src/render.tsx | 217 ++++++++++++------------ packages/renderers/solid/src/types.ts | 11 +- 2 files changed, 121 insertions(+), 107 deletions(-) diff --git a/packages/renderers/solid/src/render.tsx b/packages/renderers/solid/src/render.tsx index 5cc158f..c0d6641 100644 --- a/packages/renderers/solid/src/render.tsx +++ b/packages/renderers/solid/src/render.tsx @@ -1,6 +1,6 @@ import type { Component } from 'solid-js'; import { ErrorBoundary, onMount } from 'solid-js'; -import { SetStoreFunction, createStore, reconcile } from 'solid-js/store'; +import { createStore, reconcile } from 'solid-js/store'; import { render as solidRender } from 'solid-js/web'; import type { RenderContext, @@ -8,63 +8,124 @@ import type { Args, StoryFn, } from '@storybook/types'; -import type { ComponentsData, SolidRenderer, StoryContext } from './types'; +import type { ContextStore, ComponentsData, SolidRenderer, StoryContext } from './types'; /** - * SolidJS store for handling fine grained updates - * of the components data as f.e. story args. + * encapsule store per context */ -const componentStores: { - [id: string]: [get: ComponentsData, set: SetStoreFunction]; +const CONTEXT_STORES: { + [id: string]: ContextStore; } = {}; -// const [store, setStore] = createStore({} as ComponentsData); -let store: ComponentsData; -let setStore: SetStoreFunction; -const getStore = (componentId: string): typeof componentStores[string] => - componentStores[componentId] ||= createStore({} as ComponentsData); +function createContextStore(context: StoryContext): ContextStore { + /** + * SolidJS store for handling fine grained updates + * of the components data as f.e. story args. + */ + const [store, setStore] = createStore({} as ComponentsData); + + let { globals, componentId, viewMode } = context; + + /** + * Checks when the story requires to be remounted. + * Elements outside the story requires a whole re-render. + * e.g. dark theme, show grid, etc... + */ + const remount: ContextStore['remount'] = (force, context) => { + let flag = false; + + // Story view mode has changed + if (viewMode !== context.viewMode) flag = true; + + // Force flag is set to true. + if (force) flag = true; + + // Globals refers to storybook visualization options. + if (!Object.is(globals, context.globals)) flag = true; + + // Story main url id has changed + if (componentId !== context.componentId) { + flag = true; + unmountAll(); + } + + // Global values are updated when remount is true + if (flag === true) { + viewMode = context.viewMode; + globals = context.globals; + componentId = context.componentId; + } + + return flag; + }; -// Delay util fn -const delay = async (ms: number = 20) => { - await new Promise((resolve) => setTimeout(resolve, ms)); -}; + /** + * All stories are disposed. + */ + const disposeAllStories = () => { + Object.keys(store).forEach((storyId) => { + store[storyId]?.disposeFn?.(); + }); + }; -// Global variables -let globals: StoryContext['globals']; //Storybook view configurations. -let componentId: string; //Unique component story id. -let viewMode: string; //It can be story or docs. + /** + * Resets reactive store + */ + const cleanStore = () => { + setStore(reconcile({})); + }; -/** - * Checks when the story requires to be remounted. - * Elements outside the story requires a whole re-render. - * e.g. dark theme, show grid, etc... - */ -const remount = (force: boolean, context: StoryContext) => { - let flag = false; + /** + * Unmounts all the store and rendered solid apps + */ + const unmountAll = () => { + disposeAllStories(); + cleanStore(); + }; - // Story view mode has changed - if (viewMode !== context.viewMode) flag = true; + /** + * Resets an specific story store. + */ + const cleanStoryStore = (storeId: string) => { + setStore({ [storeId]: { args: {}, rendered: false, disposeFn: () => {} } }); + }; - // Force flag is set to true. - if (force) flag = true; + /** + * Disposes an specific story. + */ + const disposeStory = (storeId: string) => { + store[storeId]?.disposeFn?.(); + }; - // Globals refers to storybook visualization options. - if (!Object.is(globals, context.globals)) flag = true; + /** + * This function resets the canvas and reactive store for an specific story. + */ + const remountStory = (storyId: string) => { + disposeStory(storyId); + cleanStoryStore(storyId); + }; - // Story main url id has changed - if (componentId !== context.componentId) { - flag = true; - unmountAll(); - } + /** + * Checks if the story store exists + */ + const storyIsRendered = (storyId: string) => Boolean(store[storyId]?.rendered); + + return { + store, + setStore, + remount, + remountStory, + storyIsRendered, + }; +} - // Global values are updated when remount is true - if (flag === true) { - viewMode = context.viewMode; - globals = context.globals; - componentId = context.componentId; - } +function contextStore(context: StoryContext): ContextStore { + return (CONTEXT_STORES[context.componentId] ||= createContextStore(context)); +} - return flag; +// Delay util fn +const delay = async (ms: number = 20) => { + await new Promise((resolve) => setTimeout(resolve, ms)); }; /** @@ -75,7 +136,7 @@ export const solidReactivityDecorator = ( storyFn: StoryFn, context: StoryContext ) => { - const [store] = getStore(context.componentId); + const { store } = contextStore(context); let storyId = context.canvasElement.id; context.args = store[storyId].args; return storyFn(context.args as Args & StoryContext, context); @@ -106,57 +167,6 @@ export const render: ArgsStoryFn = (_, context) => { return ; }; -/** - * All stories are disposed. - */ -let disposeAllStories = () => { - Object.keys(store).forEach((storyId) => { - store[storyId]?.disposeFn?.(); - }); -}; - -/** - * Resets reactive store - */ -const cleanStore = () => { - setStore(reconcile({})); -}; - -/** - * Unmounts all the store and rendered solid apps - */ -const unmountAll = () => { - disposeAllStories(); - cleanStore(); -}; - -/** - * Resets an specific story store. - */ -const cleanStoryStore = (storeId: string) => { - setStore({ [storeId]: { args: {}, rendered: false, disposeFn: () => {} } }); -}; - -/** - * Disposes an specific story. - */ -const disposeStory = (storeId: string) => { - store[storeId]?.disposeFn?.(); -}; - -/** - * This function resets the canvas and reactive store for an specific story. - */ -const remountStory = (storyId: string) => { - disposeStory(storyId); - cleanStoryStore(storyId); -}; - -/** - * Checks if the story store exists - */ -const storyIsRendered = (storyId: string) => Boolean(store[storyId]?.rendered); - /** * Checks if the story is in docs mode. */ @@ -169,11 +179,11 @@ const isDocsMode = (context: StoryContext) => const renderSolidApp = ( storyId: string, renderContext: RenderContext, - canvasElement: SolidRenderer['canvasElement'] + canvasElement: SolidRenderer['canvasElement'], + setStore: ContextStore['setStore'], ) => { const { storyContext, unboundStoryFn, showMain, showException } = renderContext; - const [_, setStore] = getStore(storyContext.componentId); setStore(storyId, 'rendered', true); @@ -211,13 +221,8 @@ export async function renderToCanvas( let forceRemount = renderContext.forceRemount; let storyId = storyContext.canvasElement.id; - // Initializes global default values for checking remounting. - viewMode = storyContext.viewMode; - globals = storyContext.globals; - componentId = storyContext.componentId; - - // init store/setStore - [store, setStore] = getStore(componentId); + // Initializes + const { setStore, remount, remountStory, storyIsRendered } = contextStore(storyContext); // Story is remounted given the conditions. if (remount(forceRemount, storyContext)) { @@ -233,7 +238,7 @@ export async function renderToCanvas( // for rendering all the stories in docs mode when global changes. if (isDocsMode(storyContext)) await delay(); - const disposeFn = renderSolidApp(storyId, renderContext, canvasElement); + const disposeFn = renderSolidApp(storyId, renderContext, canvasElement, setStore); setStore(storyId, (prev) => ({ ...prev, disposeFn })); } } diff --git a/packages/renderers/solid/src/types.ts b/packages/renderers/solid/src/types.ts index 3bf3ca0..7ee9661 100644 --- a/packages/renderers/solid/src/types.ts +++ b/packages/renderers/solid/src/types.ts @@ -1,5 +1,6 @@ import type { Component, JSXElement } from 'solid-js'; -import type { Args, WebRenderer } from '@storybook/types'; +import type { SetStoreFunction } from 'solid-js/store'; +import type { Args, StoryContext, WebRenderer } from '@storybook/types'; export type { RenderContext } from '@storybook/types'; export type { StoryContext } from '@storybook/types'; @@ -22,3 +23,11 @@ export type StoryFnSolidReturnType = JSXElement; export type ComponentsData = { [key: string]: { args: Args; rendered?: Boolean; disposeFn?: () => void }; }; + +export interface ContextStore { + readonly store: ComponentsData; + readonly setStore: SetStoreFunction; + remount(force: boolean, context: StoryContext): boolean; + remountStory(storyId: string): void; + storyIsRendered(storyId: string): boolean; +} From 5448a22479b6adf78e3dde55a1814c282b9cc035 Mon Sep 17 00:00:00 2001 From: "Sinyoung \"Divinespear\" Kang" Date: Thu, 22 Jun 2023 06:05:15 +0000 Subject: [PATCH 3/4] add global story disposeFn array to avoid the problem of prev stories remaining --- packages/renderers/solid/src/render.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/renderers/solid/src/render.tsx b/packages/renderers/solid/src/render.tsx index c0d6641..d3ec40c 100644 --- a/packages/renderers/solid/src/render.tsx +++ b/packages/renderers/solid/src/render.tsx @@ -123,6 +123,9 @@ function contextStore(context: StoryContext): ContextStore return (CONTEXT_STORES[context.componentId] ||= createContextStore(context)); } +// prev story disposeFn list +const STORY_DISPOSER: (() => void)[] = []; + // Delay util fn const delay = async (ms: number = 20) => { await new Promise((resolve) => setTimeout(resolve, ms)); @@ -221,6 +224,11 @@ export async function renderToCanvas( let forceRemount = renderContext.forceRemount; let storyId = storyContext.canvasElement.id; + // dispose if prev stories exists + while (STORY_DISPOSER.length) { + STORY_DISPOSER.shift()?.(); + } + // Initializes const { setStore, remount, remountStory, storyIsRendered } = contextStore(storyContext); @@ -240,5 +248,6 @@ export async function renderToCanvas( const disposeFn = renderSolidApp(storyId, renderContext, canvasElement, setStore); setStore(storyId, (prev) => ({ ...prev, disposeFn })); + STORY_DISPOSER.push(disposeFn); } } From e2561f2125c9261f54642e7e965c002678742839 Mon Sep 17 00:00:00 2001 From: "Sinyoung \"Divinespear\" Kang" Date: Thu, 22 Jun 2023 15:29:21 +0000 Subject: [PATCH 4/4] prevent dispose current story * disposeFn store is now object instead array, and key is storyContext.id/storyId. * dispose every story except current story key. --- packages/renderers/solid/src/render.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/renderers/solid/src/render.tsx b/packages/renderers/solid/src/render.tsx index d3ec40c..da2335f 100644 --- a/packages/renderers/solid/src/render.tsx +++ b/packages/renderers/solid/src/render.tsx @@ -124,7 +124,9 @@ function contextStore(context: StoryContext): ContextStore } // prev story disposeFn list -const STORY_DISPOSER: (() => void)[] = []; +const STORY_DISPOSERS: { + [id: string]: (() => void) | undefined; +} = {}; // Delay util fn const delay = async (ms: number = 20) => { @@ -224,10 +226,14 @@ export async function renderToCanvas( let forceRemount = renderContext.forceRemount; let storyId = storyContext.canvasElement.id; - // dispose if prev stories exists - while (STORY_DISPOSER.length) { - STORY_DISPOSER.shift()?.(); - } + // dispose every story except current story + const contextStoryId = [storyContext.id, storyId].join('/'); + Object.keys(STORY_DISPOSERS).forEach((key) => { + if (key !== contextStoryId) { + STORY_DISPOSERS[key]?.(); + delete STORY_DISPOSERS[key]; + } + }); // Initializes const { setStore, remount, remountStory, storyIsRendered } = contextStore(storyContext); @@ -248,6 +254,6 @@ export async function renderToCanvas( const disposeFn = renderSolidApp(storyId, renderContext, canvasElement, setStore); setStore(storyId, (prev) => ({ ...prev, disposeFn })); - STORY_DISPOSER.push(disposeFn); + STORY_DISPOSERS[contextStoryId] = disposeFn; } }