storybookjs / storybook

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

Cannot use SB hooks (`useArgs`) in React component rendered inside decorator or render function. #12006

Open suparngp opened 4 years ago

suparngp commented 4 years ago

Describe the bug

To Reproduce Steps to reproduce the behavior:

  1. Create a sample app using CRA (I am using typescript)
  2. Run npx sb init to setup storyboard
  3. Install @storybook/api to get useArgs hook.
  4. Use the code snippet below in the Button.stories.tsx.
  5. Notice the error on Hello component.
Storybook preview hooks can only be called inside decorators and story functions.
Error: Storybook preview hooks can only be called inside decorators and story functions.
    at invalidHooksError (http://localhost:6007/vendors~main.38cdb582067630c38066.bundle.js:18317:10)
    at getHooksContextOrThrow (http://localhost:6007/vendors~main.38cdb582067630c38066.bundle.js:18328:11)
    at useStoryContext (http://localhost:6007/vendors~main.38cdb582067630c38066.bundle.js:18521:31)
    at useArgs (http://localhost:6007/vendors~main.38cdb582067630c38066.bundle.js:18549:27)
    at Object.Template (http://localhost:6007/main.86e3cb3589897769072c.hot-update.js:136:96)
    at renderWithHooks (webpack://storybook_docs_dll//Users/shilman/projects/baseline/storybook/node_modules/react-dom/cjs/react-dom.development.js?:2546:153)
    at mountIndeterminateComponent (webpack://storybook_docs_dll//Users/shilman/projects/baseline/storybook/node_modules/react-dom/cjs/react-dom.development.js?:2831:885)
    at beginWork (webpack://storybook_docs_dll//Users/shilman/projects/baseline/storybook/node_modules/react-dom/cjs/react-dom.development.js?:3049:101)
    at HTMLUnknownElement.callCallback (webpack://storybook_docs_dll//Users/shilman/projects/baseline/storybook/node_modules/react-dom/cjs/react-dom.development.js?:70:102)
    at Object.invokeGuardedCallbackDev (webpack://storybook_docs_dll//Users/shilman/projects/baseline/storybook/node_modules/react-dom/cjs/react-dom.development.js?:90:45)

Expected behavior It should work

Screenshots

Screen Shot 2020-08-13 at 3 15 15 PM

Code snippets

const Template: Story<ButtonProps> = (args) => {

  // removing this works everywhere
  const [_, updateArgs] = useArgs();
  // use hook
  return <Button {...args} />;
};

// works without any issue
export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

// throws error
export const Hello = () => {
  return <Small {...(Small.args as any)} />;
};

System: Environment Info:

System: OS: macOS 10.15.5 CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz Binaries: Node: 13.10.1 - ~/.nvm/versions/node/v13.10.1/bin/node Yarn: 1.22.4 - /usr/local/bin/yarn npm: 6.13.7 - ~/.nvm/versions/node/v13.10.1/bin/npm Browsers: Chrome: 84.0.4147.125 Firefox: 77.0.1 Safari: 13.1.1 npmPackages: @storybook/addon-actions: ^6.0.5 => 6.0.5 @storybook/addon-essentials: ^6.0.5 => 6.0.5 @storybook/addon-links: ^6.0.5 => 6.0.5 @storybook/client-api: ^6.0.6 => 6.0.6 @storybook/node-logger: ^6.0.5 => 6.0.5 @storybook/preset-create-react-app: ^3.1.4 => 3.1.4 @storybook/react: ^6.0.5 => 6.0.5

Additional context Add any other context about the problem here.

suparngp commented 4 years ago

Just to add a bit more context, I am facing this problems with render props. I have a complex component which accepts a renderDetails function. Until v5, I was able to reuse the story function as a render prop.

For example

export const Details = () => <MyDetails />

export const Complex = () => <ComplexComponent renderDetails={Details} />

I can still do this as long as I don't use useArgs hook.

If this is not a common / supported use case, then is there any other recommendation / workaround to solve this use case?

shilman commented 4 years ago

@tmeasday any suggestions here?

suparngp commented 4 years ago

I looked into this further. With the following snippet, the React Components hierarchy looks like this

export const Template: Story<ButtonProps> = (args) => {
  // useArgs()
  return <Button {...args} />;
};
export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};
Screen Shot 2020-08-14 at 4 34 44 PM

So when I uncomment useArgs, it fails with with a different error

Cannot read property 'getCurrentStoryData' of undefined
TypeError: Cannot read property 'getCurrentStoryData' of undefined
    at useArgs (http://localhost:6007/vendors~main.966dbf3aae43103c532f.bundle.js:19546:46)
    at Object.Template (http://localhost:6007/main.966dbf3aae43103c532f.hot-update.js:135:65)

because the story function is not rendered inside the ManagerContext provider which sets the api (I think). So that is why, I get an undefined value for api.

This is a very simple use case of useArgs which I found on the Proposal's Google Doc and #11657. So do you think that this could be a bug?

shilman commented 4 years ago

@suparngp Good sleuthing. You need a different useArgs:

import { useArgs } from '@storybook/client-api';
suparngp commented 4 years ago

@shilman Thanks! Changing to @storybook/client-api atleast fixed the second error Cannot read property 'getCurrentStoryData' of undefined that I was seeing. So at this point, the only thing blocking me from upgrading to v6 is my use case of wrapping a component using useArgs hook in another function / component / story.

I further looked into this for the following example

const Template: Story<ButtonProps> = (args) => {
  const [_, updateArgs] = useArgs();
  // use hook
  return <Button {...args} />;
};
// # 1 works without any issue
export const Small = Template.bind({});

// # 2 fails
export const MyStory = () => <Small {...Small.args} />

In case of # 1, getHooksContextOrNull is called just before line 4. But in case of # 2, for some reason getHooksContextOrNull is called after 4 is executed. So the STORYBOOK_HOOKS_CONTEXT is always undefined in that case.

1. var prevContext = _global["default"].STORYBOOK_HOOKS_CONTEXT;
2. _global["default"].STORYBOOK_HOOKS_CONTEXT = hooks;
3. var result = fn.apply(void 0, arguments);
4. _global["default"].STORYBOOK_HOOKS_CONTEXT = prevContext;
function getHooksContextOrNull() {
  return _global["default"].STORYBOOK_HOOKS_CONTEXT || null;
}

I am not sure why wrapping Small in a function would cause this because the Story itself or a render function seems to be just like a react component.

suparngp commented 4 years ago

I think I now understand the root cause of this issue.

I believe we decorate the stories lazily. So when the component which is using useArgs is wrapped in another function, only the top level function is decorated. So if I use useArgs in the wrapping function MyStory, it works because it is decorated. But the internal component Small is not and thus it throws this error. Just using Small works because it is also decorated automatically when stories are added.

That also explains why the calls happen in different order I think.

Recursively decorating the components sounds weird though, so I'd understand if this is not something that we can support. But if there is a way to create a decorated component manually, then probably my use case will be solved. For example, if I could do something like this:

const Template: Story<ButtonProps> = (args) => {
  const [_, updateArgs] = useArgs();
  return <Button {...args} />;
};

export const Small = Template.bind({});

// decorates `Small` so that it has access to the context and not exported from file to avoid double decoration.
const DecoratedSmall = decorated(Small);

export const MyStory = () => < DecoratedSmall {... DecoratedSmall.args} />

These are just guesses though because I am way to excited to migrate to v6 and trying to figure out some workaround. 😸

shilman commented 4 years ago

Does this work?

const Template: Story<ButtonProps> = (args) => {
  const [_, updateArgs] = useArgs();
  return <Button {...args} />;
};

// works without any issue
export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

// throws error
export const Hello = Template.bind({});
Hello.args = { ...Small.args };
Hello.decorators = [(StoryFn) => <><StoryFn /></>]; // some wrapper
tmeasday commented 4 years ago

Hi,

The issue here is that useArgs and related are intended to be used directly in decorators. The system is not designed to carry through the React render stack. I guess you'd call it a limitation of the Storybook hooks system.

A short explanation:

const C () => { useArgs() };

f() {
  return <C/>
}

At the point of calling f, C is not called. Instead a React element is created that boxes C. C is actually run later in the React rendering cycle, by which point SB thinks the story has already rendered.

Demonstration of the concept: https://codesandbox.io/s/quirky-mahavira-f89hn?file=/src/App.js

export const MyStory = () => <Small {...Small.args} />

I don't really get this bit though. Why are you writing it like that? Or was this just to show the problem?

suparngp commented 4 years ago

@shilman , Yes that example kind of works with useArgs as well, but only if it is used without wrapping, and so the render props still don't work.

suparngp commented 4 years ago

@tmeasday yes, your explanation makes sense.

export const MyStory = () => <Small {...Small.args} />

I wrote this just as an example of how I am trying to use stories as render props functions. Previously, I was using useState to react to event handlers. I assumed that I could replace that with useArgs.

The following example may be more clear:

export const Template = (args) => {
  // previously, I was using `useState` here.
  const [args, updateArgs] = useArgs();
  const handleClick = () => {
    updateArgs({ color: 'red' });
  }
  return <Component {...args} />;
};

// PART 1: up to this point, everything works
export const Details = Template.bind({});
Details.args = {...};

// PART 2: using Details as a render prop
export const Complex = () => <ComplexComponent renderDetails={(props) => <Details {...props} {...Details.args} />} />

The renderDetails breaks and it makes sense why it would break with my current code as you explained. While rendering Complex story, I don't really care about modifying the args of Details at this moment as none of the patterns mentioned on this page can be used without some refactoring in my code.

My only goal is to reuse Details. Otherwise, I have to create an args based Details for part 1 so that I can play with controls and a state based Details for part 2 so that I can render Details in a render prop.

Does this help understand my problem?

tmeasday commented 4 years ago

I wonder if a better solution here would be to pass updateArgs on the context?

export default {
  //  ...
 decorators: [(story, context) => {
   const [, updateArgs] = useArgs();
   story({...context, updateArgs});
};

const Template = (args, { useArgs }) => // ...

Not 100% sure that'll work, maybe it needs tweaking. Or maybe we should just pass updateArgs on the render context always (i.e. so that decorator is unnecessary).

suparngp commented 4 years ago

If I pass updateArgs as a prop down the tree, I think it updates the args of the component where useArgs was invoked which makes sense. For now, I think I ll just wrap up the migration with the useState so that at least I am on v6. May be this is something that you might wish to consider for the next releases. I ll keep an eye.

stale[bot] commented 4 years ago

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

asteurba commented 4 years ago

Hi, I was wondering if there was any update on this issue, as I just ran in to the same problem :)

stale[bot] commented 3 years ago

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

stale[bot] commented 3 years ago

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

justgeek commented 3 years ago

@suparngp Thanks so much for this question as it help me resolve an issue for dynamic assign of story args πŸŽ‰

You can find detailed implementation here: https://stackoverflow.com/questions/63708208/how-to-dynamically-mutate-args-in-storybook-v6-from-the-components-action/67424836#67424836

anthonyma94 commented 2 years ago

I'm on Storybook v6, and the client-api library is deprecated. I'm trying to use the @storybook/api library instead, but it's throwing TypeError: Cannot read properties of undefined (reading 'getCurrentStoryData') when I use useArgs() inside of a template:

const ToggleTemplate: ComponentStory<typeof Button> = (args) => {
  const [_, updateArgs] = useArgs();

  return <Button {...args} />;
};

can anyone help me out?

justgeek commented 2 years ago

@anthonyma94 can you show full code example? nothing wrong with your snippet

anthonyma94 commented 2 years ago

@justgeek sure.

Button.stories.tsx

import React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { useArgs } from "@storybook/api";
import Button from "./Button";

export default {
  title: "Button",
  component: Button,
} as ComponentMeta<typeof Button>;

const ToggleTemplate: ComponentStory<typeof Button> = (args) => {
  const [_, updateArgs] = useArgs();

  return <Button {...args} />;
};
export const Toggle: typeof Template = ToggleTemplate.bind({});

Toggle.args = {
  children: "Toggle me!",
  toggle: {
    toggleFunc: () => {},
    value: false,
  },
};

main.js

const path = require("path");
const toPath = (filePath) => path.join(process.cwd(), filePath);
module.exports = {
    addons: ["@storybook/addon-docs", "@storybook/addon-essentials"],
    typescript: {
        reactDocgen: "react-docgen-typescript",
    },
    webpackFinal: async (config, { configType }) => {
        config.resolve = {
            ...config.resolve,
            alias: {
                ...config.resolve.alias,
                "@emotion/core": toPath("node_modules/@emotion/react"),
                "emotion-theming": toPath("node_modules/@emotion/react"),
            },
        };
        return config;
    },
    core: { builder: "webpack5" },
    stories: [
        "../src/ui/**/*.stories.mdx",
        "../src/ui/**/*.stories.@(js|jsx|ts|tsx)",
    ],
};

preview.js

import React from "react";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import theme from "../src/ui/theme";
import { addDecorator } from "@storybook/react";

export const parameters = {
    controls: { expanded: true },
    actions: { argTypesRegex: "^on.*" },
};

addDecorator((story) => (
    <ThemeProvider theme={theme}>
        <CssBaseline />
        {story()}
    </ThemeProvider>
));
justgeek commented 2 years ago

@anthonyma94 import { useArgs } from '@storybook/client-api'; in v6 this is still supported

ArkenStorm commented 2 years ago

Is there an equivalent for Vue? I'm running into basically the same thing but I need it in vue.

SassNinja commented 2 years ago

I've the same problem as @ArkenStorm

I'm using storybook/vue3 and the useArgs hook seems to be react-only. I hope there's a way to (programmatically) change args for vue as well

mellunar commented 2 years ago

I'm facing the same issue, but on Angular.

prashantpalikhe commented 2 years ago

@mellunar Does my comment at https://github.com/storybookjs/storybook/issues/18358 apply to your Angular project too?

jmcgavin commented 2 years ago

Same issue but with Web Components. useArgs appears to be a React hook and offers no implementations for other frameworks

jawinn commented 1 year ago

This has been marked as "has workaround", but what is the workaround? The problem still exists and I'm not seeing a working alternative in the thread.

If updateArgs is not intended to be used within story functions, and only within decorators, could the docs be updated? There are widespread examples of using things like an onclick or onchange to trigger an updateArgs. Which sometimes works in simple cases but not like in the example presented in the original issue where you are nesting templates.

Note: I'm also dealing with web components, and Lit. In one particular case, the error was triggering with use of the repeat directive. Using map instead was a workaround that stopped the error.

jonthenerd commented 1 year ago

I'd also like to know the workaround. The goal is to have the state of the logical parent of a storybook component be reflected back in the args.

Simple use case: Have a modal and a button. Button click should cause modal to be shown. In a normal React SPA, this state would be handled on the page containing the button and the modal. Storybook is acting as this, and previous to v7 the useArgs hook allowed this to work very effectively.

tmeasday commented 1 year ago

@jonthenerd @jawinn the workaround is the comment above: use useArgs in a decorator and then pass the update function (or a new function derived from it) into the story either on the context or even as an arg.

Simple use case: Have a modal and a button. Button click should cause modal to be shown. In a normal React SPA, this state would be handled on the page containing the button and the modal.

You can just use React state to do this of course, with a "story component". To me it seems a little confusing that you can trigger the modal both by clicking the button inside the story but also by changing the arg in the controls panel. Can you talk me through why both is needed?

previous to v7 the useArgs hook allowed this to work very effectively.

I'm not sure what changed in v7 @jonthenerd? Can you tell me more?

jonthenerd commented 1 year ago

@tmeasday I'll look more into the workaround. Previous to v7, we've been using the @storybook/client-api useArgs hook, and the client-api import no longer functions in v7. Closest to it is manager-api that I've found so far, but that doesn't work when used within a story function render method.

Can you use React state for this? Yes. However, it's suboptimal. This is where I disagree with you on what's confusing. Take the example of having a simple Checkbox component. It's a controlled component, so internally it has no state. It's containing page always sends a property for whether it is checked or not. In Storybook, that containing page to the consuming user is the controls panel. Here are what happens in the different Storybook versions:

Prior to v7

Everything is kept in sync

v7

We have confused the user. Properties are not sync'ed from the control panel to the component after interaction automatically anymore.

For reference, here is what worked and the common pattern used prior to v7:

import React from "react";
import { Story, Meta } from "@storybook/react";
import { Switch, SwitchProps } from ".";
import { useArgs } from "@storybook/client-api";

export default {
    title: "Inputs/Switch",
    component: Switch
} as Meta;

const Template: Story<SwitchProps> = (args) => {
    const [{ isChecked }, updateArgs] = useArgs();

    function onChange() {
        updateArgs({ isChecked: !isChecked });
    }

    return <Switch {...args} onChange={onChange} isChecked={isChecked} />;
};

export const Standard = Template.bind({});
Standard.args = {
    isChecked: false,
    label: "Switch Me!"
} as SwitchProps;

Hope this helps explain it better. Thanks for helping :)

Update found a better fix. Look at next post.

jonthenerd commented 1 year ago

Of course after I write up a better description of the issue, I go and find a solution. The solution seems to be to switch from the @storybook/manager-api import to @storybook/preview-api. This has the necessary useArgs hook.

Here is a full working example in v7 format:

With Template Story

import { StoryObj, Meta } from "@storybook/react";
import { useArgs as UseArgs } from "@storybook/preview-api";
import { Switch, SwitchProps } from "./index";

const meta: Meta<typeof Switch> = {
    title: "Inputs/Switch",
    component: Switch,
    argTypes: {
        isDisabled: {
            control: {
                type: "boolean"
            }
        },
        isRequired: {
            control: {
                type: "boolean"
            }
        }
    }
};
export default meta;

type Story = StoryObj<typeof Switch>;

const TemplateComponent = (args: SwitchProps) => {
    const [{ isChecked }, updateArgs] = UseArgs();

    function onChange() {
        updateArgs({ isChecked: !isChecked });
    }

    return <Switch {...args} onChange={onChange} isChecked={isChecked} />;
}

const templateStory: Story = {
    render: (args) => <TemplateComponent {...args} />
}

export const Standard: Story = {
    ...templateStory,
    args: {
        isChecked: false,
        label: "Switch Me!"
    }
};

Without template story

This example doesn't have a templateStory, but still separates the render JSX into a normal looking React component function so that it will pass eslint

import { StoryObj, Meta } from "@storybook/react";
import { useArgs } from "@storybook/preview-api";
import { Switch, SwitchProps } from ".";

const meta: Meta<typeof Switch> = {
    title: "Inputs/Switch",
    component: Switch
};
export default meta;
type Story = StoryObj<typeof Switch>;

// Separate render logic into a new function that will not cause issues with eslint rule `react-hooks/rules-of-hooks`
const WrappedComponent = (args: SwitchProps) => {
  const [{ isChecked }, updateArgs] = UseArgs();
    function onChange() {
        updateArgs({ isChecked: !isChecked });
    }
    return <Switch {...args} onChange={onChange} isChecked={isChecked} />;
}
export const Example = {    
    args: {
        isChecked: false,
        label: "Switch Me!"
    },
    render: (args) => <WrappedComponent {...args} />
};

This would be a good thing to add to the docs, or to automatically do on v7 upgrade. Just changing the import line allowed it to work without changing the rest of the stories file.

tmeasday commented 1 year ago

@jonthenerd so the behaviour hasn't changed in v7, you just need to import it from @storybook/preview-api now.

As for the use case, thanks for explaining, yes that does make sense to me.


However this issue is from 2020, and really I think the title is misleading, if I understand correctly. It should be called "useArgs throws exception when used inside React component rendered in a decorator or story function".

Honestly I think this ticket should be closed and we should just document that limitation[1] of our hooks @shilman @kylegach

[1] Helpfully explained here πŸ˜†

jonthenerd commented 1 year ago

@tmeasday

You're right. I found the closest existing issue I believed was my problem at the time.

I've submitted a PR for updating the docs, but this could likely be added to the automigration script for folks running the v7 upgrade too.

avizmarques commented 1 year ago

I am encountering the same issue, thanks for the reply. I tried using it as suggested by @jonthenerd but I get an error if I try to useArgs in the render as you did it:

React Hook "useArgs" is called in function "render" that is neither a React function component nor a custom React Hook function.

edit: it works, though!

jonthenerd commented 1 year ago

@avizmarques I updated my example to remove an unnecessary export. Not getting any error on my end.

I'm wondering if what you're seeing is due to an eslint rule (see https://bobbyhadz.com/blog/react-hook-usestate-is-called-in-function-that-is-neither). I'm still working on updating my codebase to latest dependencies and may not have that check running.

Try looking at the linked article. There are some simple fixes which may alleviate the issue. I'll see if I can't dupe it as well and update my code sample.

avizmarques commented 1 year ago

Thanks, I'll take a look. It's probably an eslint issue as you say, everything seems to be working well πŸ˜„ Really useful answer, I was trying to do this for the last 2 days without finding an answer. πŸ™ As you mentioned, would be very useful to have it in the documentation.

jonthenerd commented 1 year ago

@avizmarques Confirmed it is eslint rule react-hooks/rules-of-hooks. The simplest workaround I found to bypass the eslint rule is to change the import to cause react not to thing useArgs is a hook at all (e.g. `import { useArgs as UseArgs } from "@storybook/preview-api". However, this isn't great DevX and the eslint rule will likely cause issues any other time any other real hook is used within a render function.

Two possible paths forward I see:

I think the second option above is the better safer option. But it lessens the DevX by a bit. I've updated my code sample above to pass the eslint rule. You'll notice that instead of putting the JSX directly within the render property of the story object, I've moved it to a normal looking React Component function, then referenced that within render.

avizmarques commented 1 year ago

thank you!

unfortunately the solution you suggested gives a storyblok error for me: Storybook preview hooks can only be called inside decorators and story functions.

I went with a lazy solution for now: // eslint-disable-next-line πŸ˜†

jonthenerd commented 1 year ago

I see it now. Error is happing in dev console of browser.

avizmarques commented 1 year ago

@jonthenerd sure, this is what I have right now (gives the eslint error with v 7.0.2) but it works πŸ‘

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

const meta = {
    title: 'Atoms/Modal',
    component: ModalComponent,
    args: { ...
    },
} satisfies Meta<typeof ModalComponent>;

type Story = StoryObj<typeof meta>;

export const Modal: Story = {
    render: (args) => {
        // eslint-disable-next-line
        const [{ open }, updateArgs] = useArgs();

        return (
            <Box>
                <Button onClick={() => updateArgs({ open: true })}>
                    Open modal
                </Button>
                <ModalComponent
                    {...args}
                    open={open}
                    onOpenChange={() => updateArgs({ open: false })}
                >
                Some modal content
                </ModalComponent>
            </Box>
        );
    },
};

export default meta;
avizmarques commented 1 year ago

I see it now. Error is happing in dev console of browser.

I actually get an error running storybook. see below:

const meta = {
    title: 'Atoms/Modal',
    component: ModalComponent,
    args: {
        ...
    },
} satisfies Meta<typeof ModalComponent>;

type Story = StoryObj<typeof meta>;

const WrappedModal = (args: ModalProps) => {
    const [{ open }, updateArgs] = useArgs();

    function onChange() {
        updateArgs({ open: !open });
    }
    return <ModalComponent {...args} open={open} onOpenChange={onChange} />;
};

export const Modal: Story = {
    render: (args) => <WrappedModal {...args} />,
};
Screenshot 2023-05-17 at 17 18 34
jonthenerd commented 1 year ago

What's strange about it is that my changes worked... then after a refresh in browser the error appeared. Wouldn't have suggested to follow the code if it had made the error. It's definitely Storybook itself which is throwing the error. I'm going to open a fresh issue for this as we're clearly off the path of the OP's issue.

tmeasday commented 1 year ago

@avizmarques

In your latest example:

export const Modal: Story = {
    render: (args) => <WrappedModal {...args} />,
};

you are doing the thing that this issue is about (a limitation of our hooks). You cannot use SB hooks inside a React component that is rendered (via JSX) in a render function or decorator. You need to call the hooks directly in the render function or decorator. That's why your earlier example works.

If you are just trying to sort out the linter, I'd suggest making the render function appear like a proper SFC would be the easiest way:

export const Modal: Story = {
    render: function Render(args) {
       // now I suspect the linter will be happy
    }
};

If you really do want to separate out the component, then call the hooks in the render function and pass the args/updateArgs into the component as props.

kylegach commented 1 year ago

@tmeasday β€”

Honestly I think this ticket should be closed and we should just document that limitation[1] of our hooks @shilman @kylegach

[1] Helpfully explained here πŸ˜†

I'm going to need a ~little~ lot more help understanding this, if I'm to document it clearly. We can do that outside of this issue, though.

tmeasday commented 1 year ago

@kylegach

The TLDR of the limitation is you cannot use the Storybook Hooks in React components (that are rendered by decorators or render functions). You can use the hooks in the render functions/decorators themselves of course.

So for instance if you wanted to make a complex decorator that renders a bunch of stuff and needs args, you might refactor the UI into a component and render that component in the decorator. In that case you would need to call useArgs in the decorator and pass the result into the component as prop.

Extra notes: Some people do this just purely for linting reasons, because Eslint thinks our hooks are actually React hooks, but it doesn’t think the decorator / render function is a React component. The simplest solution to that problem is simply to write the render/function in a way that looks more like a component (which it is actually):

decorators: [function Decorator(..) { }]

render: function Render() { ... }