question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

argTypes table is not generated for components with React.forwardRef and index type in their interface (with typescript)

See original GitHub issue

Describe the bug Hello, dear Storybook dev team, please take my sincere appreciation and gratitude toward the top-notch tool that you’ve created.

I have started using Storybook recently in my existing project and bumped into this problem right away, with the very first component. Frankly speaking, I hesitate regarding the importance of this bug, but I personally have spent around 10h trying to figure out the problem and would like to save some time for anyone else who could stumble in the same situation.

The problem is that if you have a React component wrapped in React.forwardRef and with index type in its interface, the arcTypes table in Storybook won’t be rendered. You may see the screenshots of my code below. I’ve also created a repo in my GitHub, so feel free to fork it.

So far, I’ve figured out that the issue happens only with React.forwardRef, but it’s possible that some other react utilities may have the same effect.

As a workaround I also have found that if you will use React.FC<ComponentProps> typing for your component, this will remove the problem, even if the index type is still there.

To Reproduce

  1. Clone or fork with repository - https://github.com/JoyTailor-1775/storybook-bug

  2. Run npm install and npm run storybook

  3. See that no doc is created for the Button component;

  4. Go to src/types/Button.ts and uncomment the 54th line (with index type);

  5. Restart the project and see the doc rendered;

System System: OS: macOS 11.2.3 CPU: (8) x64 Intel® Core™ i7-4770HQ CPU @ 2.20GHz Binaries: Node: 14.16.1 - /usr/local/bin/node Yarn: 1.22.4 - /usr/local/bin/yarn npm: 7.18.1 - /usr/local/bin/npm Browsers: Chrome: 91.0.4472.114 Safari: 14.0.3 npmPackages: @storybook/addon-actions: ^6.3.0-rc.11 => 6.3.0-rc.11 @storybook/addon-essentials: ^6.3.0-rc.11 => 6.3.0-rc.11 @storybook/addon-links: ^6.3.0-rc.11 => 6.3.0-rc.11 @storybook/builder-webpack5: ^6.3.0-rc.11 => 6.3.0-rc.11 @storybook/manager-webpack5: ^6.3.0-alpha.41 => 6.3.0-rc.11 @storybook/react: ^6.3.0-rc.11 => 6.3.0-rc.11

Additional context

Screenshot 2021-06-23 at 17 12 58 Screenshot 2021-06-23 at 17 20 57

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:3
  • Comments:8

github_iconTop GitHub Comments

2reactions
tuptoncommented, Dec 16, 2021

@honohunter I’m not sure this is the exact same workaround that @JoyTailor-1775 found, but I have a workaround that sounds similar. Assuming you have a component called Button that uses React.forwardRef:

// Button.stories.tsx
import Button, {ButtonPropsType} from './Button';

// eslint-disable-next-line react/jsx-props-no-spreading
export const ButtonProps: React.FC<ButtonPropsType> = (props) => <Button {...props} />;

//👇 This default export determines where your story goes in the story list
export default {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'Components/Button',
  component: ButtonProps,
  args: {
    children: 'Button'
  }
} as ComponentMeta<typeof ButtonProps>;

A few things to note here:

  • Import both the component and the props type PropsType from the component file.
  • Create a new component that wraps the storied component in React.FC<PropsType>.
  • Make sure this is the first named export.
  • Pass this component to the component property of the default export, as well as the type generic for ComponentMeta.

What this gets you is a new story under your component that lists all the props and a main entry in the Docs tab that shows the correct and complete props table. Also, stories that use the Template: ComponentStory<T> pattern will have the full props list in their controls tab.

It’s worth noting that wrapping the new component in ComponentStory does not work:

// eslint-disable-next-line react/jsx-props-no-spreading
const ButtonProps: React.FC<ButtonPropsType> = (props) => <Button {...props} />;
// eslint-disable-next-line react/jsx-props-no-spreading
export const ButtonPropsStory: ComponentStory<typeof ButtonProps> = (args) => <ButtonProps {...args} />;

// …the same default export as above

Hopefully this workaround helps you, and hopefully it also provides some info to anyone else about how to track down the bug that requires this workaround in the first place!

1reaction
SpiderQshkacommented, Jul 27, 2022

Made a little investigation and here’s what I found

TL;DR

While declaring button component, set it’s type this way:

export const FancyButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
  ...
}) as React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>

Explanation

Based on React types forwardRef function looks this way:

function forwardRef<T, P = {}>(
  render: ForwardRefRenderFunction<T, P>
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

Basically it means that component returned by forwardRef should accept props of generic type P, ref attribute of generic type T and few other props (for example key) The most interesting part for us is PropsWithoutRef<P>. Here’s it’s realization:

type PropsWithoutRef<P> =
    'ref' extends keyof P
        ? Pick<P, Exclude<keyof P, 'ref'>>
        : P;

What TS does here is:

  1. Checks is ref string a valid property of P type ('ref' extends keyof P)
  2. If yes - returns type that contains all fields of P, except ref (Pick<P, Exclude<keyof P, 'ref'>>)
  3. If no - returns P type without changes

As long as ref is a string, it’s valid value for index type {[key: string]: unknown}, therefore in our case PropsWithoutRef<P> is the same with Pick<P, Exclude<keyof P, 'ref'>>. Furthermore, as long as index type doesn’t have explicit declaration of ref field, Pick<P, Exclude<keyof P, 'ref'>> is the same with Pick<P, keyof P>

And here’s a punchline: keyof operator handles types/interfaces with index signature differently that it handles types/interfaces without index signature. Based on TS docs:

The keyof operator takes an object type and produces a string or numeric literal union of its keys.
If the type has a string or number index signature, keyof will return those types instead

In our case it means that for interface Props { x: string, y: string } expression Pick<Props, keyof Props> will return Props interface without changes, while for interface Props { x: string, y: string, [key: string]: unknown } the same expression will return {[key: string]: unknown}

So suggested workaround from the beginning of this answer fixes this issue. Iit simply copies return type from source code and replaces this: ForwardRefExoticComponent<Pick<ButtonProps, keyof ButtonProps> & RefAttributes<HTMLButtonElement>> …with this: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>>

But it anyway looks like dirty hack, going to create an issue in @types/react repo (if there’s no such)

Read more comments on GitHub >

github_iconTop Results From Across the Web

storybook argTypes control type - Stack Overflow
I want to 2 type(=string | number )in storybook argTypes... export default { title: 'Core/Text', component: TextComponent, argTypes: { w: {} ...
Read more >
TypeScript + React: Typing Generic forwardRefs - fettblog.eu
forwardRef is usually pretty straightforward. The types shipped by @types/react have generic type variables that you can set upon calling React.
Read more >
@cmpsr/components - npm
Start using @cmpsr/components in your project by running `npm i ... "@chakra-ui/react"; // // Do not define a new _empty_ type import ...
Read more >
Args - Storybook - JS.ORG
Storybook is a frontend workshop for building UI components and pages in isolation ... You do not need to modify your underlying component...
Read more >
Design Patterns with React Easy State | by Bertalan Miklos
Easy State is a practical Proxy-based state management library for React. Learn how to create applications with it through an app, which pairs...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found