Skip to content

Commit

Permalink
api: adding useSharedState, useStoryState (#9566)
Browse files Browse the repository at this point in the history
api: adding useSharedState, useStoryState
  • Loading branch information
ndelangen authored Jan 21, 2020
2 parents 5bac51c + d4a3e30 commit 6cdfb53
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 36 deletions.
10 changes: 9 additions & 1 deletion examples/dev-kits/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { PropTypes } from 'prop-types';
import { Button } from '@storybook/react/demo';
import { addons } from '@storybook/addons';
import { useAddonState } from '@storybook/api';
import { useAddonState, useStoryState } from '@storybook/api';
import { themes } from '@storybook/theming';
import { AddonPanel } from '@storybook/components';

Expand All @@ -15,6 +15,7 @@ addons.setConfig({
const StatePanel = ({ active, key }) => {
const [managerState, setManagerState] = useAddonState('manager', 10);
const [previewState, setPreviewState] = useAddonState('preview');
const [storyState, setstoryState] = useStoryState(10);
return (
<AddonPanel key={key} active={active}>
<div>
Expand All @@ -30,6 +31,13 @@ const StatePanel = ({ active, key }) => {
<Button onClick={() => previewState && setPreviewState(previewState - 1)}>decrement</Button>
<Button onClick={() => previewState && setPreviewState(previewState + 1)}>increment</Button>
</div>
<br />
<div>
Story counter: {storyState}
<br />
<Button onClick={() => storyState && setstoryState(storyState - 1)}>decrement</Button>
<Button onClick={() => storyState && setstoryState(storyState + 1)}>increment</Button>
</div>
</AddonPanel>
);
};
Expand Down
33 changes: 33 additions & 0 deletions examples/dev-kits/stories/addon-usestorystate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Button } from '@storybook/react/demo';
import { useStoryState } from '@storybook/client-api';

export default {
title: 'addons|useAddonState',
};

export const storyState1 = () => {
const [state, setState] = useStoryState<number>(10);

return (
<div style={{ color: 'white' }}>
Story counter: {state}
<br />
<Button onClick={() => setState(state - 1)}>decrement</Button>
<Button onClick={() => setState(state + 1)}>increment</Button>
</div>
);
};

export const storyState2 = () => {
const [state, setState] = useStoryState<number>(10);

return (
<div style={{ color: 'white' }}>
Story counter: {state}
<br />
<Button onClick={() => setState(state - 1)}>decrement</Button>
<Button onClick={() => setState(state + 1)}>increment</Button>
</div>
);
};
49 changes: 29 additions & 20 deletions lib/api/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactElement, Component, useContext, useEffect, useMemo } from 'react';
import React, { ReactElement, Component, useContext, useEffect, useMemo, useRef } from 'react';
import memoize from 'memoizerific';
// @ts-ignore shallow-equal is not in DefinitelyTyped
import shallowEqualObjects from 'shallow-equal/objects';
Expand All @@ -8,8 +8,8 @@ import {
STORY_CHANGED,
SET_STORIES,
SELECT_STORY,
ADDON_STATE_CHANGED,
ADDON_STATE_SET,
SHARED_STATE_CHANGED,
SHARED_STATE_SET,
NAVIGATE_URL,
} from '@storybook/core-events';
import { RenderData as RouterData } from '@storybook/router';
Expand Down Expand Up @@ -345,42 +345,42 @@ const addonStateCache: {
} = {};

// shared state
export function useAddonState<S>(addonId: string, defaultState?: S) {
export function useSharedState<S>(stateId: string, defaultState?: S) {
const api = useStorybookApi();
const existingState = api.getAddonState<S>(addonId);
const existingState = api.getAddonState<S>(stateId);
const state = orDefault<S>(
existingState,
addonStateCache[addonId] ? addonStateCache[addonId] : defaultState
addonStateCache[stateId] ? addonStateCache[stateId] : defaultState
);
const setState = (s: S | StateMerger<S>, options?: Options) => {
// set only after the stories are loaded
if (addonStateCache[addonId]) {
addonStateCache[addonId] = s;
if (addonStateCache[stateId]) {
addonStateCache[stateId] = s;
}
api.setAddonState<S>(addonId, s, options);
api.setAddonState<S>(stateId, s, options);
};
const allListeners = useMemo(() => {
const stateChangeHandlers = {
[`${ADDON_STATE_CHANGED}-client-${addonId}`]: (s: S) => setState(s),
[`${ADDON_STATE_SET}-client-${addonId}`]: (s: S) => setState(s),
[`${SHARED_STATE_CHANGED}-client-${stateId}`]: (s: S) => setState(s),
[`${SHARED_STATE_SET}-client-${stateId}`]: (s: S) => setState(s),
};
const stateInitializationHandlers = {
[STORIES_CONFIGURED]: () => {
if (addonStateCache[addonId]) {
if (addonStateCache[stateId]) {
// this happens when HMR
setState(addonStateCache[addonId]);
api.emit(`${ADDON_STATE_SET}-manager-${addonId}`, addonStateCache[addonId]);
setState(addonStateCache[stateId]);
api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, addonStateCache[stateId]);
} else if (defaultState !== undefined) {
// if not HMR, yet the defaults are form the manager
setState(defaultState);
// initialize addonStateCache after first load, so its available for subsequent HMR
addonStateCache[addonId] = defaultState;
api.emit(`${ADDON_STATE_SET}-manager-${addonId}`, defaultState);
addonStateCache[stateId] = defaultState;
api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, defaultState);
}
},
[STORY_CHANGED]: () => {
if (api.getAddonState(addonId) !== undefined) {
api.emit(`${ADDON_STATE_SET}-manager-${addonId}`, api.getAddonState(addonId));
if (api.getAddonState(stateId) !== undefined) {
api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, api.getAddonState(stateId));
}
},
};
Expand All @@ -389,14 +389,23 @@ export function useAddonState<S>(addonId: string, defaultState?: S) {
...stateChangeHandlers,
...stateInitializationHandlers,
};
}, [addonId]);
}, [stateId]);

const emit = useChannel(allListeners);
return [
state,
(newStateOrMerger: S | StateMerger<S>, options?: Options) => {
setState(newStateOrMerger, options);
emit(`${ADDON_STATE_CHANGED}-manager-${addonId}`, newStateOrMerger);
emit(`${SHARED_STATE_CHANGED}-manager-${stateId}`, newStateOrMerger);
},
] as [S, (newStateOrMerger: S | StateMerger<S>, options?: Options) => void];
}

export function useAddonState<S>(addonId: string, defaultState?: S) {
return useSharedState<S>(addonId, defaultState);
}

export function useStoryState<S>(defaultState?: S) {
const { storyId } = useStorybookState();
return useSharedState<S>(`story-state-${storyId}`, defaultState);
}
31 changes: 20 additions & 11 deletions lib/client-api/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ADDON_STATE_CHANGED, ADDON_STATE_SET } from '@storybook/core-events';
import { SHARED_STATE_CHANGED, SHARED_STATE_SET } from '@storybook/core-events';

import {
addons,
Expand Down Expand Up @@ -29,38 +29,47 @@ export {
useParameter,
};

export function useAddonState<S>(addonId: string, defaultState?: S): [S, (s: S) => void] {
export function useSharedState<S>(sharedId: string, defaultState?: S): [S, (s: S) => void] {
const channel = addons.getChannel();

const [lastValue] =
channel.last(`${ADDON_STATE_CHANGED}-manager-${addonId}`) ||
channel.last(`${ADDON_STATE_SET}-manager-${addonId}`) ||
channel.last(`${SHARED_STATE_CHANGED}-manager-${sharedId}`) ||
channel.last(`${SHARED_STATE_SET}-manager-${sharedId}`) ||
[];

const [state, setState] = useState<S>(lastValue || defaultState);

const allListeners = useMemo(
() => ({
[`${ADDON_STATE_CHANGED}-manager-${addonId}`]: (s: S) => setState(s),
[`${ADDON_STATE_SET}-manager-${addonId}`]: (s: S) => setState(s),
[`${SHARED_STATE_CHANGED}-manager-${sharedId}`]: (s: S) => setState(s),
[`${SHARED_STATE_SET}-manager-${sharedId}`]: (s: S) => setState(s),
}),
[addonId]
[sharedId]
);

const emit = useChannel(allListeners, [addonId]);
const emit = useChannel(allListeners, [sharedId]);

useEffect(() => {
// init
if (defaultState !== undefined && !lastValue) {
emit(`${ADDON_STATE_SET}-client-${addonId}`, defaultState);
emit(`${SHARED_STATE_SET}-client-${sharedId}`, defaultState);
}
}, [addonId]);
}, [sharedId]);

return [
state,
s => {
setState(s);
emit(`${ADDON_STATE_CHANGED}-client-${addonId}`, s);
emit(`${SHARED_STATE_CHANGED}-client-${sharedId}`, s);
},
];
}

export function useAddonState<S>(addonId: string, defaultState?: S): [S, (s: S) => void] {
return useSharedState<S>(addonId, defaultState);
}

export function useStoryState<S>(defaultState?: S): [S, (s: S) => void] {
const { id: storyId } = useStoryContext();
return useSharedState<S>(`story-state-${storyId}`, defaultState);
}
8 changes: 4 additions & 4 deletions lib/core-events/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ enum events {
STORIES_COLLAPSE_ALL = 'storiesCollapseAll',
STORIES_EXPAND_ALL = 'storiesExpandAll',
DOCS_RENDERED = 'docsRendered',
ADDON_STATE_CHANGED = 'addonStateChanged',
ADDON_STATE_SET = 'addonStateSet',
SHARED_STATE_CHANGED = 'sharedStateChanged',
SHARED_STATE_SET = 'sharedStateSet',
NAVIGATE_URL = 'navigateUrl',
}

Expand Down Expand Up @@ -53,7 +53,7 @@ export const {
STORIES_EXPAND_ALL,
STORY_THREW_EXCEPTION,
DOCS_RENDERED,
ADDON_STATE_CHANGED,
ADDON_STATE_SET,
SHARED_STATE_CHANGED,
SHARED_STATE_SET,
NAVIGATE_URL,
} = events;

0 comments on commit 6cdfb53

Please sign in to comment.