storybook
storybook copied to clipboard
[Bug]: V7, controls description not showing, TypeScript, Vite, react-docgen-typescript
Describe the bug
Every component in Storybook has TypeScript types where every prop has JSDoc comment.
Previously in Storybook 6 everything worked correctly.
All controls had description and default values shown and they were populated correctly via react-docgen-typescript
.
But after migrating to Storybook 7 and going "full Vite" controls are not showing any description.
I also tried Webpack5 with Storybook 7 and seems that description did show up:
I've also switched between react-docgen
and react-docgen-typescript
and react-docgen
did work better, but still some comments/description were missing.
To Reproduce
The project is private, don't have rights to share it.
System
System:
OS: macOS 13.2.1
CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Binaries:
Node: 16.19.1 - ~/.nvm/versions/node/v16.19.1/bin/node
Yarn: 1.22.19 - ~/.nvm/versions/node/v16.19.1/bin/yarn
npm: 8.19.3 - ~/.nvm/versions/node/v16.19.1/bin/npm
Browsers:
Chrome: 114.0.5735.133
Safari: 16.3
npmPackages:
@storybook/addon-a11y: ^7.1.0-alpha.32 => 7.1.0-alpha.33
@storybook/addon-actions: ^7.1.0-alpha.32 => 7.1.0-alpha.33
@storybook/addon-docs: ^7.1.0-alpha.32 => 7.1.0-alpha.33
@storybook/addon-essentials: ^7.1.0-alpha.32 => 7.1.0-alpha.33
@storybook/addon-links: ^7.1.0-alpha.32 => 7.1.0-alpha.33
@storybook/blocks: ^7.1.0-alpha.32 => 7.1.0-alpha.33
@storybook/client-api: ^7.1.0-alpha.32 => 7.1.0-alpha.33
@storybook/react: ^7.1.0-alpha.32 => 7.1.0-alpha.33
@storybook/react-vite: ^7.1.0-alpha.32 => 7.1.0-alpha.33
Additional context
I have a monorepo setup and Storybook collects all the story files correctly. These are the settings:
// apps/workshop/.storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
import remarkGfm from 'remark-gfm';
const conf: StorybookConfig = {
stories: [
'../src/*.mdx',
'../src/hooks/*.mdx',
'../../../packages/**/*.stories.@(js|jsx|ts|tsx|mdx)'
],
staticDirs: ['./static'],
framework: {
name: '@storybook/react-vite',
options: {}
},
addons: [
{
name: '@storybook/addon-essentials',
options: {
docs: true,
actions: true,
backgrounds: true,
controls: true
}
},
{
name: '@storybook/addon-docs',
options: {
transcludeMarkdown: true,
mdxPluginOptions: {
mdxCompileOptions: {
// Styled tables, etc.
remarkPlugins: [remarkGfm]
}
}
}
},
'@storybook/addon-a11y',
'storybook-addon-designs'
],
typescript: {
check: true,
// react-docgen-typescript not working atm
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
shouldRemoveUndefinedFromOptional: true,
propFilter: (prop) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true
}
},
docs: {
autodocs: true
}
};
export default conf;
// apps/workshop/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
resolve: {
alias: [
{
// https://github.com/vitejs/vite/issues/5764
// this is required for the SCSS modules
find: /^~(.*)$/,
replacement: '$1'
}
]
}
});
I have a component which has subcomponents specified like this:
title: 'Core/Banners/ImageBanner',
component: ImageBanner,
subcomponents: {
ImageBanner,
'ImageBanner.Content': ImageBanner.Content,
'ImageBanner.Image': ImageBanner.Image
},
Storybook 6.5.16 with webpack:
And latest 7.0.22 with Vite and react-docgen
:
@Kureyko hey I just had a similar issue and somehow managed to find a workaround.
Do any of your components (ImageBanner
, ImageBanner.Content
, or ImageBanner.Image
) happen to assign its displayName
after their declarations? For example, like ImageBanner.displayName = 'ImageBanner'
. In my case I was using React.forwardRef
and I was passing the component to be wrapped as an anonymous function, which triggered react/display-name eslint rule, so I just slapped MyComponent.displayName = 'MyComponent'
after the declaration to suppress the red squiggly lines.. which was, to my surprise, interfering with the docgen process (I use react-docgen-typescript
). So after commenting out MyComponent.displayName = 'MyComponent'
, the docgen worked as expected again.
Here are some reproducible code snippets:
// src/stories/MyComponent.tsx
import React from "react";
type Props = {
/**
* This is Prop1
*/
prop1?: string;
/**
* This is Prop2
*/
prop2?: boolean;
};
export default function MyComponent(props: Props) {
return (
<div>
<SubComponent1 />
<SubComponent2 />
</div>
);
}
const SubComponent1 = React.forwardRef(() => {
return <div>sub1</div>;
});
// Toggle on/off the following line and re-start your storybook dev-server to see the effect.
// SubComponent1.displayName = "SubComponent1";
const SubComponent2 = React.forwardRef(() => {
return <div>sub2</div>;
});
// Toggle on/off the following line and re-start your storybook dev-server to see the effect.
// SubComponent2.displayName = "SubComponent2";
// src/stories/MyComponent.stories.ts
import type { Meta, StoryObj } from "@storybook/react";
import MyComponent from "./MyComponent";
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta = {
component: MyComponent,
tags: ["autodocs"],
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {
prop1: "1",
prop2: true,
},
};
For core maintainers
@shilman Hey guys, this issue seems to be caused by generateDocgenCodeBlock()
in vite-plugin-react-docgen-typescript
. Specifically, it seems like when displayName
prop of a React component is assigned manually (like in the example above), generateDocgenCodeBlock()
produces code that attaches __docgenInfo
to the wrong component (e.g., produces SubComponent1.__docgenInfo = {...}
instead of MyComponent.__docgenInfo = {...}
).
I'll share the generated code below:
No manual displayName
assignment (see the try
block at the end)
'import { jsxDEV } from "react/jsx-dev-runtime";
import RefreshRuntime from "/@react-refresh";
const inWebWorker = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
let prevRefreshReg;
let prevRefreshSig;
if (import.meta.hot && !inWebWorker) {
if (!window.__vite_plugin_react_preamble_installed__) {
throw new Error("@vitejs/plugin-react can't detect preamble. Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201");
}
prevRefreshReg = window.$RefreshReg$;
prevRefreshSig = window.$RefreshSig$;
window.$RefreshReg$ = (type, id) => {
RefreshRuntime.register(type, "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx " + id);
};
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
}
import React from "react";
export default function MyComponent(props) {
return /* @__PURE__ */ jsxDEV("div", { children: [
/* @__PURE__ */ jsxDEV(SubComponent1, {}, void 0, false, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 15,
columnNumber: 7
}, this),
/* @__PURE__ */ jsxDEV(SubComponent2, {}, void 0, false, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 16,
columnNumber: 7
}, this)
] }, void 0, true, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 14,
columnNumber: 10
}, this);
}
_c = MyComponent;
const SubComponent1 = React.forwardRef(_c2 = () => {
return /* @__PURE__ */ jsxDEV("div", { children: "sub1" }, void 0, false, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 21,
columnNumber: 10
}, this);
});
_c3 = SubComponent1;
const SubComponent2 = React.forwardRef(_c4 = () => {
return /* @__PURE__ */ jsxDEV("div", { children: "sub2" }, void 0, false, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 28,
columnNumber: 10
}, this);
});
_c5 = SubComponent2;
var _c, _c2, _c3, _c4, _c5;
$RefreshReg$(_c, "MyComponent");
$RefreshReg$(_c2, "SubComponent1$React.forwardRef");
$RefreshReg$(_c3, "SubComponent1");
$RefreshReg$(_c4, "SubComponent2$React.forwardRef");
$RefreshReg$(_c5, "SubComponent2");
if (import.meta.hot && !inWebWorker) {
window.$RefreshReg$ = prevRefreshReg;
window.$RefreshSig$ = prevRefreshSig;
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
RefreshRuntime.registerExportsForReactRefresh("/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx", currentExports);
import.meta.hot.accept((nextExports) => {
if (!nextExports)
return;
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(currentExports, nextExports);
if (invalidateMessage)
import.meta.hot.invalidate(invalidateMessage);
});
});
}
try {
// @ts-ignore
MyComponent.displayName = "MyComponent";
// @ts-ignore
MyComponent.__docgenInfo = { "description": "", "displayName": "MyComponent", "props": { "prop1": { "defaultValue": null, "description": "This is Prop1", "name": "prop1", "required": false, "type": { "name": "string" } }, "prop2": { "defaultValue": null, "description": "This is Prop2", "name": "prop2", "required": false, "type": { "name": "boolean" } } } };
}
catch (__react_docgen_typescript_loader_error) { }'
Manual displayName
assignment (see the try
block at the end)
'import { jsxDEV } from "react/jsx-dev-runtime";
import RefreshRuntime from "/@react-refresh";
const inWebWorker = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
let prevRefreshReg;
let prevRefreshSig;
if (import.meta.hot && !inWebWorker) {
if (!window.__vite_plugin_react_preamble_installed__) {
throw new Error("@vitejs/plugin-react can't detect preamble. Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201");
}
prevRefreshReg = window.$RefreshReg$;
prevRefreshSig = window.$RefreshSig$;
window.$RefreshReg$ = (type, id) => {
RefreshRuntime.register(type, "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx " + id);
};
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
}
import React from "react";
export default function MyComponent(props) {
return /* @__PURE__ */ jsxDEV("div", { children: [
/* @__PURE__ */ jsxDEV(SubComponent1, {}, void 0, false, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 15,
columnNumber: 7
}, this),
/* @__PURE__ */ jsxDEV(SubComponent2, {}, void 0, false, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 16,
columnNumber: 7
}, this)
] }, void 0, true, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 14,
columnNumber: 10
}, this);
}
_c = MyComponent;
const SubComponent1 = React.forwardRef(_c2 = () => {
return /* @__PURE__ */ jsxDEV("div", { children: "sub1" }, void 0, false, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 21,
columnNumber: 10
}, this);
});
_c3 = SubComponent1;
SubComponent1.displayName = "SubComponent1";
const SubComponent2 = React.forwardRef(_c4 = () => {
return /* @__PURE__ */ jsxDEV("div", { children: "sub2" }, void 0, false, {
fileName: "/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx",
lineNumber: 28,
columnNumber: 10
}, this);
});
_c5 = SubComponent2;
SubComponent2.displayName = "SubComponent2";
var _c, _c2, _c3, _c4, _c5;
$RefreshReg$(_c, "MyComponent");
$RefreshReg$(_c2, "SubComponent1$React.forwardRef");
$RefreshReg$(_c3, "SubComponent1");
$RefreshReg$(_c4, "SubComponent2$React.forwardRef");
$RefreshReg$(_c5, "SubComponent2");
if (import.meta.hot && !inWebWorker) {
window.$RefreshReg$ = prevRefreshReg;
window.$RefreshSig$ = prevRefreshSig;
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
RefreshRuntime.registerExportsForReactRefresh("/Users/sook/Desktop/storybook-1/sandbox/react-vite-default-ts/src/stories/MyComponent.tsx", currentExports);
import.meta.hot.accept((nextExports) => {
if (!nextExports)
return;
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(currentExports, nextExports);
if (invalidateMessage)
import.meta.hot.invalidate(invalidateMessage);
});
});
}
try {
// @ts-ignore
SubComponent1.displayName = "SubComponent1";
// @ts-ignore
SubComponent1.__docgenInfo = { "description": "", "displayName": "SubComponent1", "props": { "prop1": { "defaultValue": null, "description": "This is Prop1", "name": "prop1", "required": false, "type": { "name": "string" } }, "prop2": { "defaultValue": null, "description": "This is Prop2", "name": "prop2", "required": false, "type": { "name": "boolean" } } } };
}
catch (__react_docgen_typescript_loader_error) { }'
I had the same issue, where the argsTypes are not properly inferred. I have storybook running in a independent package inside a mono-repository. So the solution has been adding the following to my main.ts
configuration.
{
// Other configuration...
typescript: {
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
include: ["../../path/to/your/lib/**/**.tsx"], // <- This is the important line.
}
}
}
@Kureyko hey I just had a similar issue and somehow managed to find a workaround.
Do any of your components (
ImageBanner
,ImageBanner.Content
, orImageBanner.Image
) happen to assign itsdisplayName
after their declarations? For example, likeImageBanner.displayName = 'ImageBanner'
. In my case I was usingReact.forwardRef
and I was passing the component to be wrapped as an anonymous function, which triggered react/display-name eslint rule, so I just slappedMyComponent.displayName = 'MyComponent'
after the declaration to suppress the red squiggly lines.. which was, to my surprise, interfering with the docgen process (I usereact-docgen-typescript
). So after commenting outMyComponent.displayName = 'MyComponent'
, the docgen worked as expected again.Here are some reproducible code snippets:
// src/stories/MyComponent.tsx import React from "react"; type Props = { /** * This is Prop1 */ prop1?: string; /** * This is Prop2 */ prop2?: boolean; }; export default function MyComponent(props: Props) { return ( <div> <SubComponent1 /> <SubComponent2 /> </div> ); } const SubComponent1 = React.forwardRef(() => { return <div>sub1</div>; }); // Toggle on/off the following line and re-start your storybook dev-server to see the effect. // SubComponent1.displayName = "SubComponent1"; const SubComponent2 = React.forwardRef(() => { return <div>sub2</div>; }); // Toggle on/off the following line and re-start your storybook dev-server to see the effect. // SubComponent2.displayName = "SubComponent2";
// src/stories/MyComponent.stories.ts import type { Meta, StoryObj } from "@storybook/react"; import MyComponent from "./MyComponent"; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta = { component: MyComponent, tags: ["autodocs"], } satisfies Meta<typeof MyComponent>; export default meta; type Story = StoryObj<typeof meta>; export const Basic: Story = { args: { prop1: "1", prop2: true, }, };
I have the exact issue with this, and what I did was reference back to the same display name as the component like below:
const Content = React.forwardRef(({ children }) => {
return <div>{children}</div>;
});
// Eslint will throw error "Component definition is missing display name eslint(react/display-name)"
Content.displayName = 'Content'; // <-- this will cause issue to react-docgen-typescript.
Content.displayName = Content.displayName; // <-- this won't cause issue to react-docgen-typescript and Eslint is happy.
But I still really hope I can manipulate the display name like
Content.displayName = 'Popover.Content';
So that in Storybook <Source />
it will display <Popover.Content>
instead of <Content>
Same issue here, don't have any of the forwardRef stuff above. Description / controls not auto populated from my comments in the component.
react-docgen-typescript
recommends turning off allowSyntheticDefaultImports
and esModuleInterop
in your configs for faster builds.
If you indeed have these turned off, but are doing something like:
import React from 'react';
In your source, you may hit some funny bugs as react
is not an ES lib.
I found I get more consistent results from react-docgen-typescript
by sticking to:
import * as React from 'react';
Any updates on it? I'm having the same issue on monorepo with pnpm, turborepo the ui repo is separated from storybook host repo.
You can reproduce with this template
Even if I add some jsdoc on component props interface like below, the description is not written on storybook
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* children element
*/
children: React.ReactNode;
/**
* additional text
*/
text: string;
}
/** Button component */
export function Button({ children, text, ...other }: ButtonProps): JSX.Element {
return (
<button type="button" {...other}>
{children}{text}
</button>
);
}
Button.displayName = "Button";
@Yangseungchan, not resolved yet. I get some better result with react-docgen
instead of a react-docgen-typescript
, but it still missing out a lot of comments and some types can't be resolve at all. I found another thread that has similar discussion, but looks like this is a common issue with the monorepos at the moment.
Using NX monorepo, React lib with Storybook v7.5.3 and Vite to build it. This worked for me:
const config: StorybookConfig = {
...
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
...
};
in main.ts
file and within each component I got rid of Component.displayName = 'Component'
line and exposed both named and default export of Component from the same file.
This same happened in my monorepo. It also happened in clean React + Vite + Storybook setup project. If you have following in your Storybook config then properties are not getting picked up. Using displayName or forwardRef made no difference.
typescript: {
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
compilerOptions: {
allowSyntheticDefaultImports: false,
esModuleInterop: false,
},
},
},
Also if compilerOptions
is just empty {}
then it also doesn't work. Removing compilerOptions field fixes everything. This issue was not present in Storybook 6. This is problematic because react-docgen-typescript
specifically recommends to set those options for better speed but currently that completely breaks everything.
This issue is recurring to me as well. Is the cause of this issue being identified and a fix being made? @shilman @vanessayuenn
@siosio34 We have improved react-docgen
and are recommending that as the default in 8.0. As for react-docgen-typescript
the "help wanted" label means that we'd be happy to accept fixes from the community from this issue but have not prioritized it for the core team.
@shilman However, even in version 8.0 react-docgen, we are having a lot of difficulty migrating using react-docgen due to the following issue. This issue is a fatal drawback for developers who customize UI libraries or frequently use forwardRef. Still, I can feel that performance has become faster after migrating to react-docgen, so I hope that this issue is resolved as soon as possible.
This is still happening with the latest v8.0.5 version. I'm using the nx mono repo, which still doesn't show any props table. Is any one of you able to find a solution for this? Kindly share it, please.
In the latest version ^8.0.8, the forwardRef situation still exists, and it cannot automatically create automatic documents for me.
I use storybook in Nextjs.
This is my component:
I'm using Storybook V8 with the default react-docgen
and typing the forwardref like this works for me:
type ButtonProps = {
/**
* Disables the button.
*/
disabled?: boolean;
};
/**
* Button component.
*/
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
function Button(props: ButtonProps, ref: Ref<HTMLButtonElement>) {
return <button {...props} ref={ref} />;
}
);