Storybook: 不能直接在故事中使用React挂钩

创建于 2019-02-22  ·  53评论  ·  资料来源: storybookjs/storybook

描述错误

无法直接在故事中使用挂钩,失败的原因是Hooks can only be called inside the body of a function component

重现

示例代码:

import React from 'react'

import { storiesOf } from '@storybook/react'

const stories = storiesOf('Hooks test', module)

const TestComponent: React.FC = () => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
}

stories.add('this story works', () => <TestComponent />)

stories.add('this story fails', () => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
})

预期行为
第一个故事正常,第二个故事在初始渲染时失败

版本号
@storybook/[email protected]
[email protected]
[email protected]

react has workaround question / support

最有用的评论

我们也遇到过同样的问题,这是我们要解决的一个简单技巧。

stories.add('this story fails', () => React.createElement(() => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
}))

我同意,如果没有黑客的支持,那么对其进行原生支持会更好。 如果维护者也同意,那么也许我可以花些时间提出PR🙂。

所有53条评论

不确定这是一个错误。 我相信并且我可能是错的, stories.add的第二个参数期望函数返回一个组件而不是实际的React组件。 尝试将功能组件移到外部,应该会成功。

function SomeComponent() {
    const [blah] = React.useState('blah');
    return <div> {blah}</div>;
}
stories.add('BlahComponent', () => <SomeComponent />);

我相信并且我可能是错的, stories.add的第二个参数期望函数返回一个组件而不是实际的React组件。

AFAIK并不重要,但始终值得尝试。 另外, @ sargant会引发什么错误? 故事书还是类型系统?

@Keraito不,我很确定错误是正确的。

这是因为我们是在react上下文之外调用该函数,因此故事书将这样调用该函数:

const element = storyFn();

const element = <StoryFn />

如果我们像这样启动它,可能会起作用。

现在, @ gabefromutah的建议很合理。

这是实际的代码行:
https://github.com/storybooks/storybook/blob/next/app/react/src/client/preview/render.js#L24

我认为,如果有人想通过实验来完成这项工作,那么这就是开始的地方。

@sargant @gabefromutah的建议对您

@ndelangen @gabefromutah的建议与我相信的最初“这个故事有效”的例子相同吗?

如果这不是错误,那么它仍然是避免使用第三方插件进行状态的有用增强。

如果我们像这样启动它,可能会起作用。

@ndelangen不能完全确定它将如何与其他某些插件(尤其是addon-info交互,这些插件需要诸如底层渲染组件的props之类的信息。

我们也遇到过同样的问题,这是我们要解决的一个简单技巧。

stories.add('this story fails', () => React.createElement(() => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
}))

我同意,如果没有黑客的支持,那么对其进行原生支持会更好。 如果维护者也同意,那么也许我可以花些时间提出PR🙂。

@ kevin940726太棒了👍

将以下内容添加到我的.storybook / config.js中对我有用

addDecorator((Story) => <Story />)

我已经实现了类似的东西,但是我认为这会破坏很多插件,特别是用于输出道具文档的插件信息。 我已经决定使用钩子比该信息更重要(因为无论如何我都会分别生成该信息),但是我怀疑这是否适用于所有故事书。 不知道你如何在反应之外处理像api这样的钩子。

我试图在源代码中实现它,但是我很难使它与storyshot-addon一起使用。 我认为这将是一个重大突破,因为它将在每个快照中生成一个新的父节点。 由于我们无法控制用户选择的渲染器,因此我们无法深入到一个层次。 我认为我们可能不得不考虑其他选择,例如我们可以记录解决方案,并可能提供一些帮助API供用户选择。

将以下内容添加到我的.storybook / config.js中对我有用

addDecorator((Story) => <Story />)

@emjaksa能否请您提供一个

这是我针对此问题的解决方法:

import React, { useState } from 'react';

import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withInfo } from '@storybook/addon-info';

import SelectField from 'component-folder/SelectField';

/**
 * special wrapper that replaces the `value` and `onChange` properties to make
 * the component work hooks
 */
const SelectFieldWrapper = props => {
  const [selectValue, setValue] = useState('');

  return (
    <SelectField
      {...props}
      value={selectValue}
      onChange={e => {
        setValue(e.target.value);
        action('onChange')(e.target.value);
      }}
    />
  );
};
SelectFieldWrapper.displayName = 'SelectField';

const info = {
  text: SelectField.__docgenInfo.description,
  propTables: [SelectField],
  propTablesExclude: [SelectFieldWrapper]
};

storiesOf('Controls/SelectField', module)
  .addDecorator(withInfo)

  // ... some stories

  // this example uses a wrapper component to handle the `value` and `onChange` props, but it should
  // be interpreted as a <SelectField> component
  .add('change handler', () => 
    <SelectFieldWrapper
      id="employment-status"
      placeholder="some placeholder"
      value={//selectValue}
      onChange={e => {
          // setValue(e.target.value);
      }}
    />, { info });

正如我所提到的,这仍然是一种解决方法,但是它确实有效,并且不会破坏info插件。 (我尚未测试其他插件)

我的信息插件没有中断,只要我最后添加了Story装饰器。

import React from 'react'

import { configure, addDecorator } from '@storybook/react'
import { withInfo } from '@storybook/addon-info'
import { withKnobs } from '@storybook/addon-knobs'

const req = require.context('../src', true, /\.stories\.js$/)

function loadStories() {
  req.keys().forEach(filename => req(filename))
}

addDecorator(
  withInfo({
    header: false,
  }),
)
addDecorator(withKnobs)
addDecorator((Story) => (
    <Story />
))

configure(loadStories, module)

另一个解决方法:

我有一个名为UseState的实用程序组件,定义如下:

export const UseState = ({ render, initialValue }) => {
    const [ variable, setVariable ] = useState(initialValue)
    return render(variable, setVariable)
}

我在这样的故事中使用它:

.add('use state example', () => (
    <UseState
        initialValue={0}
        render={(counter, setCounter) => (            
            <button onClick={() => setCounter(counter + 1)} >Clicked {counter} times</button>
        )}
    />
)

但是我最喜欢@ kevin940726的解决方法

我无法让故事根据状态变化重新呈现。 强制重新渲染也不起作用。

虽然此代码对React Hooks很好用

storiesOf("Dropdowns", module).add("Basic", () => <DropdownBasicStory />);

它不适用于@storybook/addon-info
Screenshot 2019-05-13 14 35 10

这使此替代方法不可用。 有任何想法吗? 故事书5.1.0-beta.0

@artyomtrityak您可以在addon-info覆盖propTables选项? 我将在即将到来的addon-docs正确解决此问题: https :

@shilman还将包括<DropdownBasicStory />吗?

@artyomtrityak我会看看我能做什么😆

嗨,大家好! 似乎最近在这个问题上没有发生太多事情。 如果仍有问题,意见或错误,请随时继续讨论。 不幸的是,我们没有时间解决所有问题。 我们随时欢迎捐款,因此,如果您想提供帮助,请向我们发送请求请求。 不活跃的问题将在30天后关闭。 谢谢!

如果还有其他人仍在理解这个问题,我发现问题似乎是在将prop类型添加到功能组件中。

const Dropdown = () => (
  // component content
);

Dropdown.propTypes = {
  ...
}

由于某种原因,注释掉.propTypes似乎可以使其正常工作。 不知道这是道具类型解析文档还是其他问题。

用钩子显示组件的源代码

function WithState({ children }) {
  const [value, setValue] = React.useState([]);

  return React.cloneElement(children, {
    value,
    onChange: event => setValue(event.target.value),
  });
}

storiesOf(`${__dirname}`, module).add('Basic', () => (
  <WithState>
    <select value="[parent state]" onChange="[parent func]">
      <option value="Australia">Australia</option>
      <option value="Cambodia">Cambodia</option>
    </select>
  </WithState>
));

嗨,大家好! 似乎最近在这个问题上没有发生太多事情。 如果仍有问题,意见或错误,请随时继续讨论。 不幸的是,我们没有时间解决所有问题。 我们随时欢迎捐款,因此,如果您想提供帮助,请向我们发送请求请求。 不活跃的问题将在30天后关闭。 谢谢!

请谨慎使用上述React.createElement<Story />方法,在某些情况下,我认为它们不太正确,因为您实际上是在重新创建react组件(故事组件),而不是返回呈现的元素(在文档中使用故事书config.js使用story()时)。

例如,当您的故事中有触发组件本地状态更新的Knobs.button ,单击该按钮后,您可能会创建一个新的故事组件,而不是更新当前组件的状态。

您可以使用此简单的代码段验证我的假设,在该代码段中,单击“旋钮”按钮后该值将不会更新。

storiesOf('Test', module).add('with text', () => {
  return React.createElement(() => {
    const [value, setValue] = React.useState(1);

    Knobs.button('Increase', () => setValue(prev => prev + 1));

    return <span>{value}</span>;
  });
});

要使其生效,您可以执行以下操作:

function MyStory() {
  const [value, setValue] = React.useState(1);

  Knobs.button('Increase', () => setValue(prev => prev + 1));

  return <span>{value}</span>;
}

storiesOf('Test', module).add('with text', () => <MyStory />);

因此,作为一种解决方法,我创建了一个包装器:

import {
  DecoratorParameters,
  Story,
  StoryDecorator,
  storiesOf as origStoriesOf,
} from '@storybook/react';

class ReactStory {
  private readonly story: Story;

  constructor(name: string, module: NodeModule) {
    this.story = origStoriesOf(name, module);
  }

  public add(
    storyName: string,
    Component: React.ComponentType,
    parameters?: DecoratorParameters
  ): this {
    this.story.add(storyName, () => <Component />, parameters);
    return this;
  }

  public addDecorator(decorator: StoryDecorator): this {
    this.story.addDecorator(decorator);
    return this;
  }

  public addParameters(parameters: DecoratorParameters): this {
    this.story.addParameters(parameters);
    return this;
  }
}

new ReactStory('Test', module).add('with text', () => {
  const [value, setValue] = React.useState(1);

  Knobs.button('Increase', () => setValue(prev => prev + 1));

  return <span>{value}</span>;
});

(可选)您可以模仿Storybook API来减少重构开销,并在解决问题后稍后再切换回。

export function storiesOf(name: string, module: NodeModule) {
  return new ReactStory(name, module);
}

我可能是错的,如果是这样,请告诉我。 非常感激!

- 我刚刚发布了https://github.com/storybookjs/storybook/releases/tag/v5.2.0-beta.10,其中包含引用此问题的PR#7571。 立即升级以试用!

您可以在@next NPM标签上找到此预发行版本。

解决此问题。 如果您认为还有更多事情要做,请重新打开。

@shilman谢谢! 您有一个如何使用它的代码片段示例吗?

@shiranZe当然可以: https :

@shilman谢谢!
但仍然出现错误:
在函数“ component”中调用React Hook“ useState”,该函数既不是React函数组件也不是自定义的React Hook函数

@shiranZe您正在使用最新的5.2-beta吗?

@shilman是的...

我在story / components / Menu / index.js上的代码:

const component =()=> {
const [buttonEl,setButtonEl] = useState(null)

const handleClick = (event) => {
    console.log('the event', event)
    setButtonEl(event.currentTarget)
}

return (
    <>
        <IconButton iconName={"menu-hamburger"} size="s" onClick={handleClick} color={"midGray"}></IconButton>

导出默认的[自述文件,组件];

并在story / components / index.js中:
storiesOf('Components', module) .addDecorator(withKnobs) .add('Text', withReadme(...Menu))

@shiranZe我想这是addon-readme的问题-也许在那里提交问题?

谢谢@shilman的回复。 我不知道那是不是问题。 我尝试在没有addon-readme仍然遇到相同的错误。 您是否有5.2-beta故事书的URL?
我试图看一下https://storybooks-official.netlify.com/ (other | demo / Button)
但我在那里找不到带有react hooks的示例。

我仍然遇到5.2.0-beta.30的问题,而这对我来说也没有解决方法,因为我也使用了旋钮插件。

@shiranZe我们的netlify部署已关闭(抄送@ndelangen),但是您可以在next分支上签出存储库并在那里尝试。

@sourcesoft我相信与旋钮相关的“预览挂钩”问题(cc @Hypnosphi)与该问题无关—它们唯一的共同点是挂钩的概念。

@shilman谢谢,我认为你是对的。 顺便说一句,我想出了如何通过反复试验修复它,更改了自定义钩子:

export const useField = (id, updateField) => {
  const onChange = useCallback((event) => {
    const {
      target: { value },
    } = e;
    updateField(id, value);
  };

  return {
    onChange,
  };
}, []);

export const useField = (id, updateField) => {
  const onChange = (event) => {
    const {
      target: { value },
    } = e;
    updateField(id, value);
  };

  return {
    onChange,
  };
};

基本上只是删除了useCallback 。 我不确定第一个版本是否是有效的钩子,但它曾经可以工作。 当错误显示Hooks can only be called inside the body of a function component或具有多个版本的React时,也会有些混乱。

在上面的示例中,删除useCallback ,您是否真的在使用useField中的任何钩子注册? 🤔

听起来好像在使用带有钩子的钩子有一些问题。 如果有人可以提供简单的复制,我很乐意看一下

@shilman您是否尝试过我之前发布的示例? 这是代码段:

storiesOf('Test', module).add('with text', () => {
  return React.createElement(() => {
    const [value, setValue] = React.useState(1);

    Knobs.button('Increase', () => setValue(prev => prev + 1));

    return <span>{value}</span>;
  });
});

它使用旧的API,但应该易于转换为最新的API进行测试。 您可以从原始帖子中找到预期的行为。

@zhenwenc仅供参考,该代码有效,但中断了react-docgen-typescript-webpack-plugin呈现的文档的使用。

解决方法似乎有些脆弱,并且与其他库冲突。 或者,有人有使用经验吗?: https :

对于那些在这里搜索错误消息的人:

将类组件更改为带有挂钩和useState的功能组件时,从@storybook/addons错误地导入时,出现此错误。 我需要它来自react而不是@storybook/addons ...自动导入失败。

只需回答@orpheus的摘录请求

将以下内容添加到我的.storybook / config.js中对我有用
addDecorator((Story) => <Story />)

@emjaksa能否请您提供一个

diff --git a/.storybook/config.js b/.storybook/config.js
--- a/.storybook/config.js
+++ b/.storybook/config.js
@@ -1,6 +1,9 @@
-import { configure } from '@storybook/react';
+import { configure, addDecorator } from '@storybook/react';
+import React from 'react';

 // automatically import all files ending in *.stories.js
 configure(require.context('../stories', true, /\.stories\.js$/), module);
+
+addDecorator((Story) => <Story />);

结果(完整的.storybook/config.js ):

import { configure, addDecorator } from '@storybook/react';
import React from 'react';

// automatically import all files ending in *.stories.js
configure(require.context('../stories', true, /\.stories\.js$/), module);

addDecorator((Story) => <Story />);

我不确定这是否是在故事书中使用反应挂钩的最佳方法,但是将故事书包装在其自己的组件<Story />组件中与"@storybook/react": "^5.2.6",一起使用在这里。

在执行此操作之前,每当我更新一个旋钮(即:一个布尔值)时,它便会在第一个渲染上起作用,但随后会停止渲染。 上述解决方案已解决该问题。 顺便说一句,我不确定在故事书中使用react钩子是否是一个好习惯,如果可能的话就模拟所有内容,这样可能更好。

描述错误

无法直接在故事中使用挂钩,失败的原因是Hooks can only be called inside the body of a function component

重现

示例代码:

import React from 'react'

import { storiesOf } from '@storybook/react'

const stories = storiesOf('Hooks test', module)

const TestComponent: React.FC = () => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
}

stories.add('this story works', () => <TestComponent />)

stories.add('this story fails', () => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
})

预期行为
第一个故事正常,第二个故事在初始渲染时失败

版本号
@storybook/[email protected]
[email protected]
[email protected]

=====================================

不要使用箭头功能创建功能组件。
做为以下示例之一:

function MyComponent(props) {
  const [states, setStates] = React.useState({ value: '' });

  return (
    <input
      type="text"
      value={states.value}
      onChange={(event) => setStates({ value: event.target.value })}
    />
  );
}

要么

//IMPORTANT: Repeat the function name

const MyComponent = function MyComponent(props) { 
  const [states, setStates] = React.useState({ value: '' });

  return (
    <input
      type="text"
      value={states.value}
      onChange={(event) => setStates({ value: event.target.value })}
    />
  );
};

如果“ ref”有问题(可能在循环中),则解决方案是使用forwardRef():

// IMPORTANT: Repeat the function name
// Add the "ref" argument to the function, in case you need to use it.

const MyComponent = React.forwardRef( function MyComponent(props, ref) {
  const [states, setStates] = React.useState({ value: '' });

  return (
    <input
      type="text"
      value={states.value}
      onChange={(event) => setStates({ value: event.target.value })}
    />
  );
});

在MDX中如何实现?

但仍然出现错误:
在函数“ component”中调用React Hook“ useState”,该函数既不是React函数组件也不是自定义的React Hook函数
https://github.com/storybookjs/storybook/issues/5721#issuecomment -518225880

我有这个完全相同的问题。 解决方法是将命名故事输出大写。

从'react'导入React;
从'./Foo'导入Foo;

导出默认值{
 标题:“ Foo”;
 };

 export const Basic =()=> <Foo />

文档说建议使用大写,但是如果您希望该警告消失,则有必要。

就我而言,问题是我忘记了导入正在使用的钩子。

将以下内容添加到我的.storybook / config.js中对我有用

addDecorator((Story) => <Story />)

@emjaksa Thanx! 经过测试并在Storybook v5.3上正常工作。

我遇到了同样的问题,我的功能性React组件有useState(value)value没有从旋钮重新渲染新值,此问题是由useState引起的:

从React docs:
Screen Shot 2020-08-07 at 19 00 04

工作解决方案故事书v5.3:

更新preview.js

.storybook/preview.js
import React from 'react'; // Important to render the story
import { withKnobs } from '@storybook/addon-knobs';
import { addDecorator } from '@storybook/react';

addDecorator(withKnobs);
addDecorator(Story => <Story />); // This guy will re-render the story

并更新main.js

.storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.jsx'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-knobs/register', // Attention to this guy
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ],
  webpackFinal: async config => {
    return config;
  },
};



有谁知道为什么这个调用装饰器使钩子起作用?

tsx
SomeComponent.decorators = [(故事)=> ]

@tmikeschu是因为<Story />将代码包装在React.createElement ,这对于钩子是必需的。

@shilman谢谢!

此页面是否有帮助?
0 / 5 - 0 等级