Storybook: Нельзя использовать перехватчики React непосредственно внутри истории.

Созданный на 22 февр. 2019  ·  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 Нет, я почти уверен, что ошибка верна.

Это потому, что мы вызываем функцию вне контекста реакции, он же Storybook вызовет функцию следующим образом:

const element = storyFn();

не

const element = <StoryFn />

Вполне возможно, если бы мы инициировали это так, это могло бы сработать.

На данный момент совет

Вот фактическая строка кода:
https://github.com/storybooks/storybook/blob/next/app/react/src/client/preview/render.js#L24

Я думаю, что если кто-то хочет поэкспериментировать, чтобы заставить эту работу работать, то с этого и следует начать.

@sargant подходит ли вам предложение

Предложение @ndelangen @gabefromutah совпадает с моим первоначальным примером "эта история работает", как я полагаю?

В случае, если это не ошибка, это может быть полезным улучшением, позволяющим избежать использования сторонних плагинов для состояния.

Вполне возможно, если бы мы инициировали это так, это могло бы сработать.

@ndelangen не совсем уверен, как это будет взаимодействовать с другими определенными надстройками, особенно с addon-info , которым нужна информация, такая как реквизиты базового визуализированного компонента.

Мы столкнулись с той же проблемой, и это простой способ ее решения.

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

Я реализовал нечто подобное, но я думаю, что это ломает кучу надстроек, в частности, addon-info для вывода документации по опорам. Я решил, что использование хуков более важно, чем эта информация (так как я все равно генерирую ее отдельно), но я сомневаюсь, что это применимо ко всему сборнику рассказов в целом. Не знаю, как бы вы справились с хуками вроде api вне React.

Я пытался реализовать это в исходном коде, но мне трудно заставить его работать с помощью 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 Вы можете переопределить параметр propTables в addon-info ? Я буду решать эту проблему в следующих addon-docs : https://medium.com/storybookjs/storybook-docs-sneak-peak-5be78445094a

@shilman, будет ли он также включать источник <DropdownBasicStory /> ?

@artyomtrityak Я посмотрю, что я могу сделать 😆

Всем привет! Похоже, что в последнее время в этом вопросе особо ничего не происходит. Если все еще есть вопросы, комментарии или ошибки, пожалуйста, продолжайте обсуждение. К сожалению, у нас нет времени разбираться в каждой проблеме. Мы всегда открыты для участия, поэтому, если вы хотите помочь, отправьте нам запрос на включение. Неактивные вопросы будут закрыты через 30 дней. Благодаря!

Если кто-то еще получает это, я обнаружил, что проблема заключается в добавлении типов опор в функциональный компонент.

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 /> , я думаю, что в некоторых случаях они не совсем правильны, потому что вы фактически воссоздаете компоненты реакции (компонент истории) вместо того, чтобы возвращать визуализированные элементы (при использовании story() в сборнике рассказов config.js как показано в документации).

Например, если в вашей истории есть 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>;
});

При желании вы можете имитировать 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 спасибо !! у вас есть пример фрагмента кода, как его использовать?

@shilman спасибо!
но все еще получаю ошибку:
React Hook "useState" вызывается в функции "component", которая не является ни компонентом функции React, ни пользовательской функцией React Hook.

@shiranZe Вы используете последнюю версию 5.2-beta?

@shilman да ...

мой код в Stories / components / Menu / index.js:

const component = () => {
const [buttonEl, setButtonEl] = useState (ноль)

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

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

экспорт по умолчанию [readme, компонент];

и в Stories / components / index.js:
storiesOf('Components', module) .addDecorator(withKnobs) .add('Text', withReadme(...Menu))

@shiranZe Думаю, это проблема с addon-readme - может быть, там о проблеме написать?

Спасибо @shilman за ответ. Не знаю, проблема ли в этом. Я пробовал без addon-readme но все равно получаю ту же ошибку. у вас есть URL книги рассказов 5.2-beta?
я попытался взглянуть на https://storybooks-official.netlify.com/ (другое | демо / кнопка)
но я не могу найти там пример с реагирующими хуками.

У меня все еще есть проблема с 5.2.0-beta.30 и дело в том, что ни один из обходных путей у меня не сработал, так как я тоже использую аддон кнопок.

@shiranZe наше развертывание netlify отключено (cc @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://github.com/Sambego/storybook-state

Для тех, кто ищет здесь сообщение об ошибке:

Я получил эту ошибку, когда при изменении компонента класса на функциональный компонент с помощью хуков и useState неправильно импортировались из @storybook/addons . Мне нужно, чтобы это происходило из react а не из @storybook/addons ... сбой автоматического импорта.

Просто отвечаю на запрос сниппете

Добавление следующего в мой .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", .

До этого, всякий раз, когда я обновлял ручку (например, логическое значение), она работала при первом рендеринге, но затем прекращала рендеринг. Вышеупомянутое решение исправило это. Кстати, я не уверен, что использовать в сборнике рассказов перехватчики реакции - хорошая практика, просто поиздевайтесь над всем, если возможно, так, наверное, лучше.

Опишите ошибку

Невозможно использовать крючки непосредственно в истории, ошибка 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 })}
    />
  );
});

как этого можно достичь в многомерных выражениях?

но все еще получаю ошибку:
React Hook "useState" вызывается в функции "component", которая не является ни компонентом функции React, ни пользовательской функцией React Hook.
https://github.com/storybookjs/storybook/issues/5721#issuecomment -518225880

У меня была такая же проблема. Исправление состоит в том, чтобы использовать экспорт именованных историй.

 импортировать React из React;
 импортировать Foo из './Foo';

 экспорт по умолчанию {
 название: 'Foo';
 };

 экспорт const Basic = () => <Foo />

В документах говорится, что использование заглавных букв рекомендуется, но это необходимо, если вы хотите, чтобы это предупреждение исчезло.

В моем случае проблема заключалась в том, что я забыл импортировать крючок, который использовал.

Добавление следующего в мой .storybook / config.js сработало для меня

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

@emjaksa Спасибо! Протестировано и отлично работает на Storybook v5.3.

У меня была такая же проблема, мой функциональный компонент реакции имеет useState(value) и value не повторно отображал новое значение из Knobs, эта проблема вызвана useState:

Из документов React:
Screen Shot 2020-08-07 at 19 00 04

Рабочее решение Storybook 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 рейтинги