storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.77k stars 9.34k forks source link

[Bug]: docs container - Storybook preview hooks can only be called inside decorators and story functions #28758

Open unional opened 4 months ago

unional commented 4 months ago

Describe the bug

I'm using storybook-dark-mode to toggle my stories between light and dark mode.

It works in stories, but the theme in the autodocs are not updated automatically.

To address that, I use a custom doc container:

// .storybook/preview.tsx
import type { Preview } from '@storybook/react';
import { DocsContainer, type DocsContextProps } from '@storybook/blocks';
import { themes } from '@storybook/theming';
import React, { type PropsWithChildren } from 'react';
import { useDarkMode } from 'storybook-dark-mode';

export interface DocsContainerProps {
  context: DocsContextProps<any>;
}
function ThemedContainer(props: PropsWithChildren<DocsContextProps>) {
  const dark = useDarkMode();
  return (
    <DocsContainer context={props} theme={dark ? themes.dark : themes.light}>
      {props.children}
    </DocsContainer>
  );
}
const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    docs: {
      container: ThemedContainer,
    },
  },
};

export default preview;

This is working is storybook 8.1, but broken in 8.2.

Reproduction link

https://stackblitz.com/edit/github-2sfagn?file=.storybook%2Fpreview.tsx

Reproduction steps

open the repro

System

Storybook Environment Info:

  System:
    OS: macOS 14.5
    CPU: (10) arm64 Apple M1 Pro
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.12.2 - ~/.nvm/versions/node/v20.12.2/bin/node
    npm: 10.5.0 - ~/.nvm/versions/node/v20.12.2/bin/npm
    pnpm: 9.0.4 - ~/.nvm/versions/node/v20.12.2/bin/pnpm <----- active
  Browsers:
    Chrome: 127.0.6533.72
    Safari: 17.5

Additional context

No response

greptile-apps[bot] commented 4 months ago

Disclaimer This information might be inaccurate, due to it being generated automatically To fix the issue with the theme not updating in autodocs in Storybook 8.2, modify the ExternalDocsContainer component to use the useDarkMode hook and apply the theme dynamically. Here's the updated code:

import React from 'react';
import { ThemeProvider, themes, ensure } from 'storybook/internal/theming';
import { DocsContext } from '../DocsContext';
import { ExternalPreview } from './ExternalPreview';
import { useDarkMode } from 'storybook-dark-mode';

let preview: ExternalPreview<Renderer>;

export const ExternalDocsContainer: React.FC<
  React.PropsWithChildren<{ projectAnnotations: any }>
> = ({ projectAnnotations, children }) => {
  if (!preview) preview = new ExternalPreview(projectAnnotations);

  const dark = useDarkMode();
  const theme = dark ? themes.dark : themes.light;

  return (
    <DocsContext.Provider value={preview.docsContext()}>
      <ThemeProvider theme={ensure(theme)}>{children}</ThemeProvider>
    </DocsContext.Provider>
  );
};

This change ensures that the theme in the autodocs updates automatically based on the dark mode setting.

References

/.github/DISCUSSION_TEMPLATE/help.yml /docs/_snippets/storybook-preview-auto-docs-override-mdx-container.md /.github/DISCUSSION_TEMPLATE/ideas.yml /docs/_snippets/storybook-preview-docs-dark-theme.md /docs/_snippets/configure-mock-provider-with-story-parameter.md /code/lib/blocks/src/blocks/external/ExternalDocsContainer.tsx /code/lib/blocks/src/blocks/external /docs/_snippets/storybook-preview-with-angular-polyfills.md /test-storybooks/external-docs/.storybook /.github/DISCUSSION_TEMPLATE/rfc.yml /code/addons/docs/template/stories/docspage /code/core/src/manager/components/sidebar/Brand.tsx /code/core/scripts/helpers/modifyThemeTypes.ts /code/lib/cli/src/automigrate/fixes/autodocs-tags.test.ts /code/addons/docs/angular /code/addons/docs/ember /code/frameworks/ember/src/client/preview/docs /code/addons/docs/template/stories/docspage/override.stories.ts /code/.storybook/preview.tsx /docs/_snippets/mock-provider-in-preview.md /code/core/src/core-server/utils/mockdata/errors/MetaOfClashingDefaultName.mdx /code/addons/docs/react /docs/_snippets/storybook-preview-empty-sort-object.md /code/core/src/preview-api/modules/preview-web/docs-context /docs/_snippets/storybook-preview-optout-inline.md

#### About Greptile This response provides a starting point for your research, not a precise solution. Help us improve! Please leave a 👍 if this is helpful and 👎 if it is irrelevant. [Ask Greptile](https://app.greptile.com/chat/github/storybookjs/storybook/next) · [Edit Issue Bot Settings](https://app.greptile.com/apps/github)
unional commented 4 months ago

FYI I also tried to make it work with @storybook/addon-themes and here is the ThemedContainer that works:

import { DocsContainer, type DocsContextProps } from '@storybook/blocks'
import { themes } from '@storybook/theming'
import { type PropsWithChildren } from 'react'

export function ThemedContainer(props: PropsWithChildren<{ context: DocsContextProps }>) {
    return (
        <DocsContainer
            context={props.context}
            theme={(props.context as any).store.globals.globals.theme === 'dark' ? themes.dark : themes.light}
        >
            {props.children}
        </DocsContainer>
    )
}

You can see I dive in to the internals to get the theme value which is probably not a good idea. And alternative is parsing the globals query params.

mellm0 commented 3 months ago

We have the same issue for themes, and consequentially found the same solution as @unional, but yes it seems very hacky.

Allowing addon hooks to be used in DocsContainer would be really handy.

massi08 commented 3 months ago

Same issue here, and workaround doesn't seem to work for us on version 8.2.7

mohitkyadav commented 3 months ago

Same issue for me as well, here's the code that breaks:

import React from 'react'
import { DocsContainer as SBDocsContainer } from '@storybook/blocks'
import { useDarkMode } from 'storybook-dark-mode'

import { darkTheme, lightTheme } from '../themes'

const DocsContainer = ({ children, context }) => (
  <SBDocsContainer
    context={context}
    theme={useDarkMode() ? darkTheme : lightTheme}
  >
    {children}
  </SBDocsContainer>
)

export default DocsContainer

For me store.globals.globals.theme is undefined.

giladgray commented 2 months ago

I am experiencing a variant of this with any of my stories that use useState in the story function -- updating the state triggers the error described here. Seems new with v8.3; worked fine in v8.1. Any ideas?

andyford commented 2 months ago

I ran into this issue after upgrading from 8.2.9 to 8.3.1

kyrakwak commented 2 months ago

@unional @mohitkyadav @mellm0 FYI we were previously manually accessing store.globals.globals.theme but had to adjust to store.userGlobals.globals.theme after upgrading to 8.3

pahund commented 1 month ago

I got the error "Storybook preview hooks can only be called inside decorators and story functions" that is mentioned in the title when I navigated to docs pages that had inline story blocks with the dark mode addon.

I found out that the useDarkMode hook is the culprit. I was using it in a custom docs container. I changed my docs container to not use this, this fixed the issue.

DarkModeAwareDocsContainer.jsx:

import React, { useEffect, useState } from 'react'
import { DocsContainer } from '@storybook/blocks'
import { themes } from '@storybook/theming'
import { addons } from '@storybook/preview-api'

const DARK_MODE_EVENT_NAME = 'DARK_MODE'

export default ({ children, ...props }) => {
  const [isDark, setIsDark] = useState(document.body.classList.contains('dark'))

  useEffect(() => {
    const chan = addons.getChannel()
    chan.on(DARK_MODE_EVENT_NAME, setIsDark)
    return () => chan.off(DARK_MODE_EVENT_NAME, setIsDark)
  }, [])

  return (
    <DocsContainer
      {...props}
      theme={isDark ? themes.dark : themes.light}
    >
      {children}
    </DocsContainer>
  )
}
adaam2 commented 3 weeks ago

I noticed with the above that the classList was empty on initial load, which meant that the code above failed to detect dark mode.

I modified it to this:

import React, { useEffect, useState } from 'react'
import { DocsContainer } from '@storybook/blocks'
import { themes } from '@storybook/theming'
import { addons } from '@storybook/preview-api'

const DARK_MODE_EVENT_NAME = 'DARK_MODE'

function useDocumentClassListObserver(
  callback: (classList: DOMTokenList) => void
) {
  useEffect(() => {
    // Define a MutationObserver that listens to changes in classList
    const observer = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        if (mutation.attributeName === 'class') {
          callback(document.documentElement.classList)
        }
      }
    })

    // Start observing the documentElement for attribute changes
    observer.observe(document.documentElement, { attributes: true })

    // Clean up on component unmount
    return () => observer.disconnect()
  }, [callback])
}

export default ({ children, ...props }) => {
  const [isDark, setIsDark] = useState(document.documentElement.classList.contains('dark'))

  useDocumentClassListObserver((classList) => {
    setIsDark(classList.contains('dark'))
  })

  useEffect(() => {
    const chan = addons.getChannel()
    chan.on(DARK_MODE_EVENT_NAME, setIsDark)
    return () => chan.off(DARK_MODE_EVENT_NAME, setIsDark)
  }, [])

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

n.b my class is applied to documentElement instead

kasperpeulen commented 3 weeks ago

Let's see if this might be fixed by: #26243 React: Fix RSC compatibility with addon-themes