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

useArgs in render function breaks Storybook #67

Open
ai212983 opened this issue Jun 28, 2024 · 15 comments
Open

useArgs in render function breaks Storybook #67

ai212983 opened this issue Jun 28, 2024 · 15 comments
Labels
bug Something isn't working upstream An issue with Storybook that flows down to the addon

Comments

@ai212983
Copy link

ai212983 commented Jun 28, 2024

Storybook crashes with Error: Storybook preview hooks can only be called inside decorators and story functions. when attempting to use the useArgs hook with storybook-addon-remix-react-router.

To Reproduce

  1. Open the MultipleStoryInjection story from Basic stories.
  2. Modify the render function to include the useArgs hook:
  render: () => {
    const location = useLocation();
    const [updateArgs] = useArgs();
    return (
      <div>
        <p>{location.pathname}</p>
        <Link to={'/login'}>Login</Link> | <Link to={'/signup'}>Sign Up</Link>
      </div>
    );
  },
  1. If the story doesn't crash immediately, click on any link in the rendered component.

Additional context

Although SB hooks are not permitted directly inside components, they are allowed within render functions. The inability to use useArgs prevents testing configurable component behavior in response to navigation events.

In my specific case, it affects the useBlocker hook in my custom form component.

Environment

npx sb info

Storybook Environment Info:
(node:31405) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)

  System:
    OS: macOS 14.5
    CPU: (12) arm64 Apple M2 Max
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.2.0 - /opt/homebrew/bin/node
    Yarn: 1.22.19 - /opt/homebrew/bin/yarn
    npm: 10.7.0 - /opt/homebrew/bin/npm <----- active
  Browsers:
    Chrome: 126.0.6478.127
    Safari: 17.5
  npmPackages:
    @storybook/addon-essentials: ^8.1.10 => 8.1.10
    @storybook/addon-interactions: ^8.1.10 => 8.1.10
    @storybook/addon-links: ^8.1.10 => 8.1.10
    @storybook/blocks: ^8.1.10 => 8.1.10
    @storybook/react: ^8.1.10 => 8.1.10
    @storybook/react-vite: ^8.1.10 => 8.1.10
    @storybook/test: ^8.1.10 => 8.1.10
    eslint-plugin-storybook: ^0.8.0 => 0.8.0
    storybook: ^8.1.10 => 8.1.10
    storybook-addon-remix-react-router: ^3.0.0 => 3.0.0
@ai212983 ai212983 added the bug Something isn't working label Jun 28, 2024
@JesusTheHun JesusTheHun changed the title `useArgs in render function breaks Storybook useArgs in render function breaks Storybook Jun 28, 2024
@JesusTheHun
Copy link
Owner

Hello @ai212983 👋

Sadly this is true with any decorator. In the example below, as soon as you click on the button to increment the value, it will break.

export const RawStory = {
  decorators: [
    (Story: any) => {
      return (
        <section>
          decorated:
          <Story />
        </section>
      );
    },
  ],
  render: (args: { foo: string }) => {
    const [updateArgs] = useArgs();
    const [count, setCount] = useState(0);

    return (
      <>
        <h1>Args</h1>
        <p>{JSON.stringify(args)}</p>

        <button onClick={() => setCount((count) => count + 1)}>Increase</button>
        <div role={'status'}>{count}</div>
      </>
    );
  },
  args: {
    foo: 'bar',
  },
};

The reason for that is SB clears the storyContext as soon as the story is rendered. So during the first render it works, but on the next render, it breaks.
This has already been reported in #33, and sadly things have not changed on the SB side.

You can see storybookjs/storybook#12006 .

Now, if what you want is only to read the story args, you should known that the first argument received by the story function is the args object.

export const RawStory = {
 render: ({ foo }) => { // <== the args are accessible here
    const location = useLocation();
    const [updateArgs] = useArgs();
    return (
      <div>
        <p>{location.pathname}</p>
        <Link to={'/login'}>Login</Link> | <Link to={'/signup'}>Sign Up</Link>
      </div>
    );
  },
  args: { foo: 'bar' }
}

@ai212983
Copy link
Author

@JesusTheHun Hey there! :)

You're right, the story context does reset on every re-render. I'm using useArgs to restore my component state, so it kinda works.

I did see both of those tickets but thought maybe there was some way to work around this.

Thanks so much for explaining. I guess you can go ahead and close the ticket now.

@JesusTheHun
Copy link
Owner

I'm using useArgs to restore my component state
What do you mean exactly ?

@ai212983
Copy link
Author

@JesusTheHun well, the usual. Something like this:

const defaultRender = function Render(args: any) {
    const [{ onChange }, updateArgs] = useArgs();
    return (
            <ComboList
                {...args}
                onChange={onChange
                    ? (items: ListItems[]) => updateArgs({ items })
                    : undefined}
            />
    );
};

@JesusTheHun
Copy link
Owner

well, the usual

@ai212983 I've never used it like that. That's not a bad idea, but yeah, it cannot work. A dedicated decorator with a context and hook would work though !

@ai212983
Copy link
Author

...but yeah, it cannot work.

@JesusTheHun Not sure if I follow you here :) The example above works fine for me. items are part of the args:

export const DefaultUsage: Story = {
    // @ts-ignore
    args: {
        // @ts-ignore
        onChange: false,
        items: generateItems(5),
        layers: generateLayerInfos(50),
    },
    render: defaultRender,
};

they are passed to my component by the render function. updateArgs in the example above works as a state setter.

If we can use useArgs inside the decorator, probably it is possible to parametrize withRouter so it returns useArgs? Not sure how decorators work though (maybe I should lol).

@JesusTheHun
Copy link
Owner

The example above works fine for me

Yes, but it doesn't use a decorator.

If we can use useArgs inside the decorator, probably it is possible to parametrize withRouter so it returns useArgs? Not sure how decorators work though (maybe I should lol).

We could do some reference passing, but that's a bit dirty. Maybe I'll create such decorator as a standalone package, it should be straight forward. Maybe next week, I'll keep you posted ;)

@ai212983
Copy link
Author

ai212983 commented Jul 1, 2024

@JesusTheHun Actually it does. Here's the full code for ComboList.stories.tsx:

import "../styles/tailwind.css";
import "../styles/index.scss";
import { ComboList } from "@/Keyboard/Combos/ComboList";
import { ZMKCombo } from "@/localResources";
import { SearchContext } from "@/providers";
import { faker } from "@faker-js/faker";
import { useArgs } from "@storybook/preview-api";
import { Meta, StoryObj } from "@storybook/react";
import { createFnMapping, createTestSearchContext, generateItemDescription, generateLayerInfos } from "./common";

const meta = {
    title: "Keyboard/Combo/List",
    component: ComboList,
    parameters: {
        layout: "padded",
    },
    argTypes: {
        onItemUpdate: {
            control: "boolean",
            name: "Read Only",
            mapping: createFnMapping(false),
        },
        onItemDelete: { table: { disable: true } },
        onItemCreate: { table: { disable: true } },
    },
    decorators: [
        (Story) => (
            <div className="tailwind" style={{ height: "calc(100vh - 3rem)" }}>
                <div className="h-full">
                    <Story />
                </div>
            </div>
        ),
    ],
} satisfies Meta<typeof ComboList>;

// noinspection JSUnusedGlobalSymbols
export default meta;
type Story = StoryObj<typeof meta>;

const searchContext = createTestSearchContext(20);

const defaultRender = function Render(args: any) {
    const [{ items, onItemUpdate }, updateArgs] = useArgs();
    const setItems = (items: ZMKCombo[]) => {
        updateArgs({ items });
    };

    return (
        <SearchContext.Provider value={searchContext}>
            <ComboList
                {...args}
                onItemDelete={(name: string) => {
                    setItems(items.filter((m: ZMKCombo) => m.name !== name));
                }}
                onItemUpdate={onItemUpdate
                    ? (name: string, item: ZMKCombo) => {
                        setItems(items.map((m: ZMKCombo) => (m.name === name ? item : m)));
                    }
                    : undefined}
                onsItemCreate={(item: ZMKCombo) => {
                    setItems([...items, item]);
                }}
            />
        </SearchContext.Provider>
    );
};

// noinspection JSUnusedGlobalSymbols
export const DefaultUsage: Story = {
    // @ts-ignore
    args: {
        items: generateItems(5),
        layers: generateLayerInfos(50),
        // @ts-ignore
        onItemUpdate: false,
    },
    render: defaultRender,
};

There's no any logic in this decorator, but still.
I've ended with special render function which uses useState for state management. Linter is complaining, but it works.
Can't wait to test a custom decorator though ;)

@JesusTheHun
Copy link
Owner

@ai212983 can you provide a repro inside a stackblitz for example ? or a git repo

@ai212983
Copy link
Author

ai212983 commented Jul 1, 2024

@JesusTheHun Here you go - also I've updated your project so Stackblitz displays Stories by default.

Navigate to Demo/useArgs/Default Usage story and click increase.

@JesusTheHun
Copy link
Owner

@ai212983 yes it does work in this case, because the args update do not trigger the decorator function to run again.

@ai212983
Copy link
Author

ai212983 commented Jul 3, 2024

@JesusTheHun Not exactly so. I've updated the example, you can trigger re-run decorator function with local state counter.

@JesusTheHun
Copy link
Owner

@ai212983 Indeed. I've also tried to create an addon to have a custom hook. It's harder than it looks like !

@helloint
Copy link

I hope I can use useArgs() and useLocation() together.

    const { pathname} = useLocation();
    const [updateArgs] = useArgs();

Any workaround?

@JesusTheHun
Copy link
Owner

@helloint well really it's a limitation of Storybook addons in React. It's not specific to this addon. I've started to work on something to workaround it but it's a mess. I really hope SB will fix this on their end.

@JesusTheHun JesusTheHun added the upstream An issue with Storybook that flows down to the addon label Oct 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working upstream An issue with Storybook that flows down to the addon
Projects
None yet
Development

No branches or pull requests

3 participants