storybook: Addon-docs: forwardRef trips up Props block

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.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 11
  • Comments: 21 (3 by maintainers)

Most upvoted comments

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 😃

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).

@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>
  )
);

Got it working 👌

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.

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

@Zunaib this was fixed for me in Storybook 5.3

Mirror issue on react-docgen-typescript-loader side: https://github.com/strothj/react-docgen-typescript-loader/issues/76