Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Since v4.0.2 getting error "Storybook preview hooks can only be called inside decorators and story functions." #282

Open
bcbailey-torus opened this issue Jun 25, 2024 · 12 comments

Comments

@bcbailey-torus
Copy link

bcbailey-torus commented Jun 25, 2024

Summary

After upgrading to version 4.0.2 I am getting the error Storybook preview hooks can only be called inside decorators and story functions.

Downgrading to 4.0.1 makes the error goes away.

This is likely related to the changes in #279

Is there something I need to change on my end?

Full error

Error: Storybook preview hooks can only be called inside decorators and story functions.
    at invalidHooksError (http://localhost:6006/sb-preview/runtime.js:97:11863)
    at getHooksContextOrThrow (http://localhost:6006/sb-preview/runtime.js:97:12123)
    at useHook (http://localhost:6006/sb-preview/runtime.js:97:12204)
    at useMemoLike (http://localhost:6006/sb-preview/runtime.js:99:209)
    at useRefLike (http://localhost:6006/sb-preview/runtime.js:99:505)
    at useStateLike (http://localhost:6006/sb-preview/runtime.js:99:940)
    at useState (http://localhost:6006/sb-preview/runtime.js:99:1201)
    at useDarkMode (http://localhost:6006/node_modules/.cache/storybook/ee6bf224cd156d2a19aeb1406fef9897dbfe874b3eae8c467c24b2f62e4ecd1a/sb-vite/deps/storybook-dark-mode.js?v=2c4e06dd:8560:51)
    at DarkModeObserver (http://localhost:6006/.storybook/preview.tsx:11:18)
    at renderWithHooks (http://localhost:6006/node_modules/.cache/storybook/ee6bf224cd156d2a19aeb1406fef9897dbfe874b3eae8c467c24b2f62e4ecd1a/sb-vite/deps/chunk-ILFQ7OTE.js?v=2c4e06dd:11548:26)

Reference

My storybook .storybook/preview.tsx for reference:

import { useDarkMode } from 'storybook-dark-mode'
import { useColorScheme } from '@mui/material/styles'
import { useEffect } from 'react'
import { Experimental_CssVarsProvider as CssVarsProvider } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'

const DarkModeObserver = () => {
    const isDark = useDarkMode()
    const { setMode } = useColorScheme()
    useEffect(() => {
        if (isDark) {
            setMode('dark')
        } else {
            setMode('light')
        }
    }, [isDark])
    return null
}

const preview: Preview = {
    // ...
    decorators: [
        (Story) => (
            <CssVarsProvider theme={myThemeV1}>
                <DarkModeObserver />
                <CssBaseline />
                <Story />
            </CssVarsProvider>
        ),
    ],
}

export default preview

Versions

storybook: 8.1.10
storybook-dark-mode: 4.0.2

@bcbailey-torus bcbailey-torus changed the title 4.0.2 "Storybook preview hooks can only be called inside decorators and story functions." Since v4.0.2 getting error "Storybook preview hooks can only be called inside decorators and story functions." Jun 25, 2024
@grubersjoe
Copy link

grubersjoe commented Jun 26, 2024

If I understand #279 correctly this change was necessary to be compatible with @storybook/preview-api? My use case was to dynamically change the theme of the docs container in preview.tsx:

// .storybook/preview.tsx
const Container = (props: DocsContainerProps) => (
  <DocsContainer
    {...props}
    theme={{
      ...common,
      ...(useDarkMode() ? themes.dark : themes.light), // <<<
    }}
  />
);

Unfortunately, this broke with storybook-dark-mode@4.0.2. Does someone have an idea what to do about this? Is this hook simply no longer supported inside preview.tsx? Had the terrible idea to use a MutationObserver on the body class to work around this issue:

https://github.com/grubersjoe/react-activity-calendar/blob/main/.storybook/preview.tsx

I don't know why Storybook did these changes but they feel quite limiting =/.

@valpioner
Copy link

valpioner commented Jul 1, 2024

We can also reproduce this runtime error. It completely breaks our storybook docs theming approach. Can someone please fix it, or maybe suggest another way to change docs theme?

Meanwhile downgraded to 4.0.1 due to this breaking change.

// preview.ts
...
import { MyDocsContainer2 } from './docsContainer';

const preview: Preview = {
  parameters: {
    docs: {
      container: myDocsContainer
// myDocsContainer.tsx
...
import { useDarkMode } from 'storybook-dark-mode';

export const MyDocsContainer = (props: PropsWithChildren<DocsContainerProps>) => (
  <DocsContainer context={props.context} theme={useDarkMode() ? themes.dark : themes.light}>
    {props.children}
  </DocsContainer>
);
image

@skaneprime
Copy link

Facing same problem for dark mode in docs

    docs: {
      container: (props) => {
        const isDark = useDarkMode();
        const currentProps = { ...props };
        currentProps.theme = isDark ? themes.dark : themes.light;
        return React.createElement(DocsContainer, currentProps);
      },
    },

image

@skaneprime
Copy link

Found workaround for dark mode

const preview: Preview = {
    docs: {
      container: (props: {
        children: React.ReactNode;
        context: DocsContextProps;
        theme?: ThemeVars;
      }) => {
        // eslint-disable-next-line react-hooks/rules-of-hooks
        const [isDark, setDark] = React.useState(true);
        props.context.channel.on(DARK_MODE_EVENT_NAME, (state) =>
          setDark(state)
        );
        const currentProps = { ...props };
        currentProps.theme = isDark ? themes.dark : themes.light;
        return <DocsContainer {...currentProps} />;
      },
}

@skaneprime
Copy link

More a bit cleaner pragmatic solution
in preview.tsx

const MyDocsContainer = (props: {
        children: React.ReactNode;
        context: DocsContextProps;
        theme?: ThemeVars;
      }) => {
        const [isDark, setDark] = React.useState(true);

        React.useEffect(() => {
          props.context.channel.on(DARK_MODE_EVENT_NAME, setDark);

          return () =>
            props.context.channel.removeListener(DARK_MODE_EVENT_NAME, setDark);
        }, [props.context.channel]);

        return (
          <DocsContainer
            {...props}
            theme={isDark ? themes.dark : themes.light}
          />
        );
      }

@grubersjoe
Copy link

grubersjoe commented Jul 6, 2024

I mean it's a hack, but that's a lot better than what I came up with spontaneously :D. Thanks for sharing!

@TheMikeyRoss
Copy link

How do we access the current color mode in a single story?

@damisparks
Copy link

I am having the same issue.

"storybook": "^8.2.3",
"storybook-dark-mode": "^4.0.2",
Screenshot 2024-07-19 at 11 22 57

Any suggestions here?

@damisparks
Copy link

More a bit cleaner pragmatic solution in preview.tsx

const MyDocsContainer = (props: {
        children: React.ReactNode;
        context: DocsContextProps;
        theme?: ThemeVars;
      }) => {
        const [isDark, setDark] = React.useState(true);

        React.useEffect(() => {
          props.context.channel.on(DARK_MODE_EVENT_NAME, setDark);

          return () =>
            props.context.channel.removeListener(DARK_MODE_EVENT_NAME, setDark);
        }, [props.context.channel]);

        return (
          <DocsContainer
            {...props}
            theme={isDark ? themes.dark : themes.light}
          />
        );
      }

@skaneprime Although this solution works. However, it creates some flickering when switching between docs.

@valpioner
Copy link

valpioner commented Jul 22, 2024

More a bit cleaner pragmatic solution in preview.tsx

const MyDocsContainer = (props: {
        children: React.ReactNode;
        context: DocsContextProps;
        theme?: ThemeVars;
      }) => {
        const [isDark, setDark] = React.useState(true);

        React.useEffect(() => {
          props.context.channel.on(DARK_MODE_EVENT_NAME, setDark);

          return () =>
            props.context.channel.removeListener(DARK_MODE_EVENT_NAME, setDark);
        }, [props.context.channel]);

        return (
          <DocsContainer
            {...props}
            theme={isDark ? themes.dark : themes.light}
          />
        );
      }

@skaneprime Although this solution works. However, it creates some flickering when switching between docs.

Is there maybe a way to cache the current value and use it instead of a hardcoded true? Or keep this listener at a higher lvl, so it won't be refreshed every time (if DARK_MODE_EVENT_NAME is accessible outside)?

@Mookiies
Copy link

By using the channel from preview you can make a hook which can be used in both docs container and decorator

import { addons } from '@storybook/preview-api';

const channel = addons.getChannel();

function useDarkMode() {
  const [isDark, setDark] = React.useState(false);

  React.useEffect(() => {
    channel.on(DARK_MODE_EVENT_NAME, setDark);
    return () => channel.off(DARK_MODE_EVENT_NAME, setDark);
  }, [channel, setDark]);

  return isDark;
}

Based off this section of the readme

@martinsik
Copy link

Based on your suggestions, to avoid the initial blinking when switching to dark mode or reloading a page already in dark mode, you can store the setting in localStorage and then use the value to initialise useState. It's an ugly hack but it works and hopefully this will be resolved soon.

const [isDark, setDark] = useState(isDarkModeEnabled());

useEffect(() => {
	channel.on(DARK_MODE_EVENT_NAME, state => {
		setDarkModeEnabled(state);
		setDark(state);
	});

	return () => channel.removeListener(DARK_MODE_EVENT_NAME, setDark);
}, []);

// ...

const LOCAL_STORAGE_KEY = '_tmp-dark-mode-hack';

export const isDarkModeEnabled = () => Boolean(localStorage.getItem(LOCAL_STORAGE_KEY));

export const setDarkModeEnabled = (enabled: boolean) =>
	enabled
		? localStorage.setItem(LOCAL_STORAGE_KEY, '1')
		: localStorage.removeItem(LOCAL_STORAGE_KEY);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants