Open suparngp opened 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?
@tmeasday any suggestions here?
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',
};
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?
@suparngp Good sleuthing. You need a different useArgs
:
import { useArgs } from '@storybook/client-api';
@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.
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. πΈ
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
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?
@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.
@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?
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).
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.
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!
Hi, I was wondering if there was any update on this issue, as I just ran in to the same problem :)
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!
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!
@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
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?
@anthonyma94 can you show full code example? nothing wrong with your snippet
@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>
));
@anthonyma94 import { useArgs } from '@storybook/client-api';
in v6 this is still supported
Is there an equivalent for Vue? I'm running into basically the same thing but I need it in vue.
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
I'm facing the same issue, but on Angular.
@mellunar Does my comment at https://github.com/storybookjs/storybook/issues/18358 apply to your Angular project too?
Same issue but with Web Components. useArgs
appears to be a React hook and offers no implementations for other frameworks
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.
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.
@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?
@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:
isChecked
property is now true
isChecked
toggle and it unchecks the checkboxEverything is kept in sync
isChecked
toggle. It goes from isChecked = false
to isChecked = true
first. The component does not change (as it shouldn't). isChecked = true
to isChecked = false
. Now the component updates back to not being checked.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.
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:
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!"
}
};
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.
@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 π
@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.
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!
@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.
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.
@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.
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
π
I see it now. Error is happing in dev console of browser.
@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;
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} />,
};
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.
@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.
@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.
@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() { ... }
Describe the bug
To Reproduce Steps to reproduce the behavior:
npx sb init
to setup storyboard@storybook/api
to getuseArgs
hook.Button.stories.tsx
.Hello
component.Expected behavior It should work
Screenshots
Code snippets
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.