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

Addon-docs: forwardRef trips up Props block #8881

Closed
jonathanloske opened this issue Nov 19, 2019 · 21 comments
Closed

Addon-docs: forwardRef trips up Props block #8881

jonathanloske opened this issue Nov 19, 2019 · 21 comments

Comments

@jonathanloske
Copy link

jonathanloske commented Nov 19, 2019

Describe the bug
When implementing a Component in the way described below, the Props block only shows the following text:

"Cannot read property 'appearance' of undefined"

To Reproduce
Declare a component in way mentioned under "Code snippets".

Expected behavior
The Props block should display the corresponding Props.

Screenshots
Screenshot 2019-11-19 at 09 21 10

Code snippets
Button.tsx

type ButtonProps = {
  appearance?: "primary" | "secondary" | "ghost";
} & JSX.IntrinsicElements["button"];

const Button: RefForwardingComponent<HTMLButtonElement, ButtonProps> = React.forwardRef(({ appearance, ...otherProps}, ref) => (
  <button>...</button>
))

Button.displayName = "Button";

Button.defaultProps = {
  appearance: "primary"
};

Button.stories.tsx

import React from "react";
import { storiesOf } from "@storybook/react";
import { Button } from "./Button";

storiesOf("Button", module)
  .addParameters({
    component: Button
  })
  .add("primary", () => (
    <Button appearance="primary">
      My primary button!
    </Button>
  ))
;

System:
Tried out with both Storybook 5.2.5 and Storybook 5.3.0-beta.1, using react-docgen-typescript-loader 3.6.0.

Using stories in the TSX file format but encountered the error also when using the JSX file format.

Component is in TSX format.

Please paste the results of npx -p @storybook/cli@next sb info here.

  System:
    OS: macOS 10.15.1
    CPU: (4) x64 Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz
  Binaries:
    Node: 10.16.0 - ~/.nvm/versions/node/v10.16.0/bin/node
    npm: 6.9.0 - ~/.nvm/versions/node/v10.16.0/bin/npm
  Browsers:
    Chrome: 78.0.3904.97
    Safari: 13.0.3
  npmPackages:
    @storybook/cli: ^5.3.0-beta.1 => 5.3.0-beta.1

Additional context
Props block used to work before introducing RefForwardingComponent and React.forwardRef

Possibly related to #7933, #8445, and #4787.

@jonathanloske
Copy link
Author

Mirror issue on react-docgen-typescript-loader side: strothj/react-docgen-typescript-loader#76

@stale
Copy link

stale bot commented Dec 23, 2019

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 stale bot added the inactive label Dec 23, 2019
@Zunaib
Copy link

Zunaib commented Dec 24, 2019

Any Leads on this, I'm Stuck too

@stale stale bot removed the inactive label Dec 24, 2019
@jonathanloske
Copy link
Author

@Zunaib this was fixed for me in Storybook 5.3

@Zunaib
Copy link

Zunaib commented Dec 27, 2019

Any specific Version like 5.3.0rc ?

@jonathanloske
Copy link
Author

Tried it out with one of the late betas (.28 or so) and it still worked with the RC

@atanasster
Copy link
Member

@Zunaib I believe you need to install https://github.com/atanasster/webpack-react-docgen-typescript

@jonathanherdt - can you please confirm your configuration here.

@Zunaib
Copy link

Zunaib commented Dec 27, 2019

Got it working 👌

@atanasster
Copy link
Member

Great - can you post your config and we can close this issue.

@Zunaib
Copy link

Zunaib commented Dec 27, 2019

Okay sure.

Here is my config.js

import { withKnobs } from '@storybook/addon-knobs';
import { addDecorator, addParameters, configure } from '@storybook/react';
import requireContext from 'require-context.macro';
import '../src/styles/airokit.scss';
import airotheme from './airotheme';

addParameters({
  options: {
    theme: airotheme,
    showSearchBox: true,
    sidebarAnimations: true
  },
});

addDecorator(withKnobs);

const loader = () => {

  const allExports = [require('../src/GetStarted/GetStarted.stories.mdx')];
  const req = requireContext('../src/', true, /\.stories\.(tsx|mdx)$/);
  req.keys().forEach(fname => allExports.push(req(fname)));
  return allExports;

}
configure(loader, module);

Here are my presets.js

const path = require("path");

module.exports = [
    {
        name: "@storybook/preset-typescript",
        options: {
            tsDocgenLoaderOptions: {
                tsconfigPath: path.resolve(__dirname, "../tsconfig.json")
            },
            tsLoaderOptions: {
                configFile: path.resolve(__dirname, '../tsconfig.json'),
            },
            include: [path.resolve(__dirname, "../src")]
        }
    },
    {
        name: '@storybook/preset-scss'
    },
    {
        name: '@storybook/addon-docs/preset'
    }
];

Here is a component using ForwardRef

export const Col = forwardRef<HTMLDivElement, ColProps>(function (
  {
    ...props
  },
  ref
) {
  return (
    <div
      {...props}
      ref={ref}
    >
      {children}
    </div>
  );
});

export default Col;

@atanasster
Copy link
Member

Thanks - closing

@Paosder
Copy link

Paosder commented Mar 6, 2020

Modified: I found forwardRef must write its type similar to FC in functional component, if not, it always broken like below. So I added ForwardRefExoticComponent<ButtonProps> to RefButton, the error disappears. The above example used RefForwardingComponent<HTMLButtonElement, ButtonProps> would cause type error in typescript as I suffered, may not bothering you in storybook but finally blow up in bundling, use ForwardRefExoticComponent to prevent errors with defaultProps.


I have stuck in exact same problem in v5.3.14(latest release).

image

Here's my code:

// refbutton.tsx
import React, { forwardRef } from 'react';

type ButtonProps = {
  hello?: 'primary' | 'secondary' | 'ghost';
} & JSX.IntrinsicElements['button'];


const RefButton = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ hello, ...props }, ref) => (
    <button type="button" ref={ref} {...props}>
      {props.children}
      {hello}
    </button>
  ),
);

RefButton.defaultProps = {
  hello: 'primary',
};

export default RefButton;
// refbutton.stories.tsx
import React from 'react';
import { withKnobs } from '@storybook/addon-knobs';
import { withA11y } from '@storybook/addon-a11y';
import RefButton from './refbutton';


export default {
  title: 'components|buttons/RefButton',
  component: RefButton,
  decorators: [withKnobs, withA11y],
  parameters: {
    info: { inline: true },
    notes: 'test node.',
    componentSubtitle: 'forwardRef example',
  },
};

export const ButtonExample = () => <RefButton>test code</RefButton>;

dependencies:

    "@storybook/addon-a11y": "^5.3.14",
    "@storybook/addon-actions": "^5.3.14",
    "@storybook/addon-docs": "^5.3.14",
    "@storybook/addon-info": "^5.3.14",
    "@storybook/addon-knobs": "^5.3.14",
    "@storybook/addon-links": "^5.3.14",
    "@storybook/addon-notes": "^5.3.14",
    "@storybook/addons": "^5.3.14",
    "@storybook/react": "^5.3.14",

I tried to change type to interface and extends props, but still happens in forwardRef only.
(FC works gracefully!)
Is there any way to solve this? Please help.~~

@justincy
Copy link

justincy commented Apr 7, 2020

Removing react-docgen-typescript-loader and adding babel-plugin-react-docgen worked for me, as documented here: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#react-prop-tables-with-typescript

@gius
Copy link

gius commented May 4, 2021

I had the same problem with the 6.1.20 version. It turned out you should not use React.xxx reference and directly use the decomposed member xxx:

// does not work
export const Button: React.FunctionComponent<ButtonProps> = props => {...}

// works
export const Button: FunctionComponent<ButtonProps> = props => {...}


// does not work
export const Button3 = React.forwardRef<RawButton, ButtonProps>((props, ref) => {...}

// works
export const Button = forwardRef<RawButton, ButtonProps>((props, ref) => {...}

Moreover, just changing the type while Storybook is running in watch mode does not work. You need to restart the storybook (or force a full recompilation of the file).

@souporserious
Copy link

Explicitly setting the tsconfig path is what worked for me:

{
  reactDocgenTypescriptOptions: {
    tsconfigPath: path.resolve(__dirname, '../packages/package/tsconfig.json')
  }
}

@nicollecastrog
Copy link

If it helps anyone coming from Google there was a subtle distinction that was needed for my case:

Storybook version: 6.4.20

I was already destructuring forwardRef upon import (as many people in various Github issues have suggested), explicitly setting the types upon invoking forwardRef<A, B>(), and ensuring my component function was set to a const:

import React, { forwardRef, ForwardedRef } from 'react';

export type CodeFieldInputProps = {
 ...
};

const CodeFieldInput = (
  props: CodeFieldInputProps,
  ref: ForwardedRef<HTMLInputElement>
) => (
  <input
    ...
    ref={ref}
  />
);

export default forwardRef<HTMLInputElement, CodeFieldInputProps>(CodeFieldInput);

However the key appears to be that you need to do the invocation of forwardRef within the const and not upon export default, like I did above. Here's the working code:

import React, { forwardRef, ForwardedRef } from 'react';

export type CodeFieldInputProps = {
 ...
};

const CodeFieldInput = forwardRef<HTMLInputElement, CodeFieldInputProps>(
  (
    props: CodeFieldInputProps,
    ref: ForwardedRef<HTMLInputElement>
  ) => (
    <input
      ...
      ref={ref}
    />
  )
);

export default CodeFieldInput;

Hope this saves someone some time 😃

@RodrigoTomeES
Copy link

RodrigoTomeES commented Apr 19, 2022

Hi,

I have the same issue, but I don't know how to use the solution with generics. This is my button:

import { forwardRef } from 'react';

import styles from './Button.module.css';

import { ButtonProps } from './types';

/**
 * Primary UI component for user interaction
 */
const ButtonBase = <T extends React.ElementType = 'button'>(
  {
    className = '',
    as,
    style = 'primary',
    disabled = false,
    size = 'normal',
    negative = false,
    icon = null,
    label,
    ...props
  }: ButtonProps<T> &
    Omit<React.ComponentPropsWithRef<T>, keyof ButtonProps<T>>,
  ref: React.Ref<any>
) => {
  const HTMLTag = as || 'button';

  return (
    <HTMLTag
      className={`
        ${styles['o-button']} \
        ${styles[`o-button--${size}`]} \
        ${styles[`o-button--${style}${negative ? '-negative' : ''}`]} \
        ${disabled ? styles['o-button--disabled'] : ''} \
        ${className} \
      `}
      disabled={disabled}
      ref={ref}
      {...props}
    >
      <>
        {label}
        {icon}
      </>
    </HTMLTag>
  );
};

export const Button = forwardRef(ButtonBase);

I tried several forms but it didn't work. Anyone have the same problem? Thanks!

@BjornFridal
Copy link

Like gius suggested simply changing React.forwardRef into forwardRef worked in my Typescript project.

// does not work
export const Button3 = React.forwardRef<RawButton, ButtonProps>((props, ref) => {...}

// works
export const Button = forwardRef<RawButton, ButtonProps>((props, ref) => {...}

@scottalguire
Copy link

@RodrigoTomeES

I had the exact same situation where I had a polymorphic component that renders a different element type based on an as prop and uses a generic to impose type safety.

I redeclared the forwardRef definition using the approach found here and controls work just fine in storybook now.

My final solution looked like this:

/*
 * Augment `forwardRef` only for this module so that storybook can infer controls
 * (despite component being wrapped in forwardRef)
 * https://fettblog.eu/typescript-react-generic-forward-refs/#option-3%3A-augment-forwardref
 * https://github.com/microsoft/TypeScript/pull/30215
 */
declare module 'react' {
  // eslint-disable-next-line @typescript-eslint/ban-types
  function forwardRef<T, P = {}>(
    render: (props: P, ref: Ref<T>) => ReactElement | null
  ): (props: P & RefAttributes<T>) => ReactElement | null;
}

/**
 * A polymorphic component used to render html elements that contain text.
 * The visual appearance of the text and the underlying html element can be controlled independently.
 */
export const Text = forwardRef(
  <C extends ElementType = 'span'>(
    {
      as,
      children,
      value,
      textStyle = 'P1',
      textColor,
      enableEllipsis = false,
      enableUnderline = false,
      className,
      ...props
    }: TextProps<C>,
    ref?: PolymorphicRef<C>
  ) => (
    <TextStyledComponent
      ref={ref}
      as={as as keyof JSX.IntrinsicElements}
      textStyle={textStyle}
      enableEllipsis={enableEllipsis}
      enableUnderline={enableUnderline}
      textColor={textColor}
      className={className}
      {...props}
    >
      {value || children || ''}
    </TextStyledComponent>
  )
);

@mikeaustin
Copy link

mikeaustin commented Dec 7, 2022

The only solution that worked for me is

const Foo = React.forwardRef(({
  children,
}: FooProps, ref: React.Ref<HTMLDivElement>) => {
  return (
    <div ref={ref}>
      {children}
    </div>
  );
});

export default Foo;

Supplying a type to forwardRef such as forwardRef<HTMLDivElement> broke. With the ref type added, it seems to work. It also doesn't like export default forwardRef(Foo) as others have stated.

@Stikoun
Copy link

Stikoun commented Apr 4, 2024

When you pass MUI component (e.g. Divider) to component, Storybook will not inherit any props.

import React from 'react';
import { Divider, DividerProps, Box, Typography, Stack } from '@mui/material';
import { StoryFn, Meta } from '@storybook/react';

export default {
  title: 'Web Library/Material UI/Divider',
  component: Divider,
  },
} as Meta;

If you add this code below to other file and import it to code above, use the component in component, the props appear.

import { Divider, DividerProps } from '@mui/material';

export const DividerTest: React.FC<DividerProps> = (props: DividerProps) => <Divider {...props} />;

If you move that component to same file where you declare component, props disapper.

import React from 'react';
import { Divider, DividerProps, Box, Typography, Stack } from '@mui/material';
import { StoryFn, Meta } from '@storybook/react';

export const DividerTest: React.FC<DividerProps> = (props: DividerProps) => <Divider {...props} />;

export default {
  title: 'Web Library/Material UI/Divider',
  component: DividerTest,
  },
} as Meta;

Why is this happening?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests