storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
83.97k stars 9.23k forks source link

Maximum update depth exceeded depending on order of decorators #12306

Open slowselfip opened 4 years ago

slowselfip commented 4 years ago

Describe the bug I think that this bug is an effect of a combination between the react-final-form and storybook implementations, possibly it's a problem when the decorator contain a component that uses render prop. It's also possible that this is entirely a problem with react-final-form but I've never seen this issue in any other context that I've used that library. The example below reproduce the error message. By switching the order of the decorators, i.e. having the final-form decorator first and the div-wrapper decorator last, makes it go away.

To Reproduce Init CRA project, npx create-react-app example --template typescript cd example

Init storybook npx sb init

Install final-form and react-final-form yarn add final-form react-final-form

Add the following story in the stories directory

// Test.stories.tsx
import React from 'react'
//eslint-disable-next-line
import { Story, Meta } from '@storybook/react/types-6-0'
import { Form, Field } from 'react-final-form'

export default {
  title: 'Test',
} as Meta

export const FinalFormTest = () => <Field name="test" component="input" />

const noop = () => {};

/**
 * Change the order of these decorators to make maximum update depth 
 * exceeded error dissappear.
 */
FinalFormTest.decorators = [
  (Story: Story) => (
    <div>
      <Story />
    </div>
  ),
  (Story: Story) => (
    <Form onSubmit={noop}>
      {({ handleSubmit }) => (
          <form onSubmit={handleSubmit}>
            <Story />
          </form>
      )}
    </Form>
  ),
]

Expected behavior The decorators should not depend on the order they are defined. Would expect to be able to have the final-form decorator wrapped by the div decorator without getting an exception.

System: System: OS: macOS 10.15.6 CPU: (4) x64 Intel(R) Core(TM) i5-4670 CPU @ 3.40GHz Binaries: Node: 14.8.0 - /usr/local/bin/node Yarn: 1.22.4 - /usr/local/bin/yarn npm: 6.14.7 - /usr/local/bin/npm Browsers: Chrome: 84.0.4147.135 Safari: 13.1.2 npmPackages: @storybook/addon-actions: ^6.0.20 => 6.0.20 @storybook/addon-essentials: ^6.0.20 => 6.0.20 @storybook/addon-links: ^6.0.20 => 6.0.20 @storybook/node-logger: ^6.0.20 => 6.0.20 @storybook/preset-create-react-app: ^3.1.4 => 3.1.4 @storybook/react: ^6.0.20 => 6.0.20

stale[bot] commented 3 years ago

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!

gerardo-navarro commented 3 years ago

Hi @slowselfip, I think that you are right. Where you able to solve this?

@shilman Have you and the team had the chance to dig deeper into this.

I am experiencing the same issue with react-hook-form. I isolated the story / stories for easy testing. Embedding a react component with a render prop inside the story is fine when there are no decorators involved.

import React from 'react'
// also exported from '@storybook/react' if you can deal with breaking changes in 6.1
import { Story, Meta } from '@storybook/react/types-6-0'

import {
  Controller,
  FormProvider,
  useForm,
  useFormContext
} from 'react-hook-form'

export default {
  title: 'Forms/Input',
  component: <input />,
  argTypes: {},
  // This is a workaround for the issue regarding "Maximum call stack size exceeded"
  // See https://github.com/storybookjs/storybook/issues/12747
  parameters: { docs: { source: { type: 'code' } } }
} as Meta

const reactHookFormProviderDecorator = (Story: Story) => {
  const rhFormMethods = useForm()
  return (
    <FormProvider {...rhFormMethods}>
      <form
        noValidate
        onSubmit={rhFormMethods.handleSubmit((v) => console.log(v))}
      >
        <Story />
        <input type='submit' />
      </form>
    </FormProvider>
  )
}

// This story fails because of the controller and its render method.
const TemplateFailsWithController: Story = (args) => {
  const { control } = useFormContext()
  return (
    <Controller
      contro={control}
      name='test'
      defaultValue='testabc'
      render={({ name, value, onChange, ref }) => {
        return (
          <input
            name={name}
            value={value}
            onChange={onChange}
            ref={ref}
            {...args}
          />
        )
      }}
    />
  )
}
export const TemplateFailsWithControllerStory = TemplateFailsWithController.bind(
  {}
)
TemplateFailsWithControllerStory.decorators = [reactHookFormProviderDecorator]
TemplateFailsWithControllerStory.args = {}

// This story works but with another structure ...
const TemplateWorksWithoutController: Story = (args) => {
  const { register } = useFormContext()
  return <input name='test' defaultValue='testabc' ref={register} {...args} />
}
export const TemplateWorksWithoutControllerStory = TemplateWorksWithoutController.bind(
  {}
)
TemplateWorksWithoutControllerStory.decorators = [
  reactHookFormProviderDecorator
]
TemplateWorksWithoutControllerStory.args = {}

const TemplateWorksWithControllerInside: Story = (args) => {
  const rhFormMethods = useForm()
  return (
    <FormProvider {...rhFormMethods}>
      <form
        noValidate
        onSubmit={rhFormMethods.handleSubmit((v) => console.log(v))}
      >
        <input
          name='test'
          defaultValue='testabc'
          ref={rhFormMethods.register}
          {...args}
        />
        <input type='submit' />
      </form>
    </FormProvider>
  )
}

export const TemplateWorksWithControllerInsideStory = TemplateWorksWithControllerInside.bind(
  {}
)
TemplateWorksWithControllerInsideStory.args = {}

BTW: Thank you very much for this awesome tool ;-)

shilman commented 3 years ago

@gerardo-navarro Instead of <Story/> try {Story()} in your decorator?

gerardo-navarro commented 3 years ago

@gerardo-navarro Instead of <Story/> try {Story()} in your decorator?

@shilman Thank you for your quick response.

Unfortunately, It does not work. The method const { control } = useFormContext() returns null which is not suppose to be considering that the FormProvider was wrapped around it. Given that null is returned, the following code breaks => TypeError: Cannot read property 'control' of null

Any other ideas?

jdortegar commented 3 years ago

Hey guys i have the same issue with other decorator:

import { gql } from "@apollo/client";
import { Args } from "@storybook/addons";
import React, { ReactElement } from "react";

import {
  FetchStatus,
  HeadlessStoryContext,
  Loader,
  pack,
  Prompt,
  withHeadless,
} from "storybook-addon-headless";

export default {
  title: "Examples/GraphQL",
  decorators: [
    withHeadless({
      graphql: {
        uri: "https://alameda-staging.herokuapp.com/graphql",
      },
    }),
  ],
  parameters: {
    headless: {
      Artworks: {
        query: pack(gql`
          {
            products {
              id
              title
              enable
              slug
              variants {
                title
                outOfStock
              }
            }
          }
        `),
        autoFetchOnInit: true,
      },
    },
  },
};

export const Artworks = (args: Args, { status, data }) => {
  console.log(args, status, data);
  return <h1>Hello</h1>;
};
KarafiziArtur commented 1 year ago

I have the same issue with react-hook-form FormProvider in the last version of Storybook

import { ReactElement } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { SnackbarProvider } from "notistack";
import { Story } from "@storybook/react";

import { PhotoInput, IPhotoInputProps } from "./index";

export default {
  title: "Photo",
  component: PhotoInput,
  decorators: [
    (Story: Story): ReactElement => (
      <SnackbarProvider>
          <FormProvider {...useForm({ defaultValues: { photo: [] } })}>
            <Story />
          </FormProvider>
      </SnackbarProvider>
    ),
  ],
};

const Template: Story<IPhotoInputProps> = args => <PhotoInput {...args} />;

export const Simple = Template.bind({});
Simple.args = {
  name: "photo",
};

By the way, SnackbarProvider is working fine and doesn't throw any error

But for the context of FormProvider - it comes as null as well as in previous comment