Storybook: Não é possível usar ganchos React diretamente dentro de uma história

Criado em 22 fev. 2019  ·  53Comentários  ·  Fonte: storybookjs/storybook

Descreva o bug

Não é possível usar ganchos diretamente em uma história, falha com Hooks can only be called inside the body of a function component .

Reproduzir

Código de exemplo:

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

Comportamento esperado
A primeira história funciona bem, a segunda história falha na renderização inicial

Versões
@storybook/[email protected]
[email protected]
[email protected]

react has workaround question / support

Comentários muito úteis

Já enfrentamos o mesmo problema e este é um hack simples que fazemos para contornar o problema.

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

Eu concordo que seria muito melhor ter suporte nativo sem hacks. Se os mantenedores concordarem também, talvez eu possa encontrar algum tempo para propor um PR 🙂.

Todos 53 comentários

Não tenho certeza se isso é um bug. Acredito, e posso estar errado, que o segundo argumento de stories.add é esperar que uma função retorne um componente e não um componente React real. Tente mover seu componente de função para fora, você deve ter algum sucesso.

Exemplo

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

Acredito, e posso estar errado, que o segundo argumento de stories.add é esperar que uma função retorne um componente e não um componente React real.

Realmente não deveria importar AFAIK, mas sempre vale a pena tentar. Além disso, @sargant , o que gera o erro? Livro de histórias ou o sistema de tipos?

@Keraito Não, tenho quase certeza de que o erro está correto.

É porque estamos chamando a função fora do contexto de react, também conhecido como storybook chamará a função assim:

const element = storyFn();

não

const element = <StoryFn />

Muito possivelmente, se o iniciássemos assim, poderia funcionar.

No momento, o conselho de @gabefromutah é

Esta é a linha de código real:
https://github.com/storybooks/storybook/blob/next/app/react/src/client/preview/render.js#L24

Se alguém quiser experimentar para fazer isso funcionar, acho que esse é o lugar por onde começar.

@sargant a sugestão de @gabefromutah funciona para você?

A sugestão de @ndelangen @gabefromutah é igual ao meu exemplo inicial "esta história funciona", acredito?

No caso de não ser um bug, ainda pode ser um aprimoramento útil para evitar o uso de plug-ins de terceiros para o estado.

Muito possivelmente, se o iniciássemos assim, poderia funcionar.

@ndelangen não tem certeza de como isso irá interagir com outros complementos específicos, especialmente addon-info , que precisam de informações como acessórios do componente renderizado subjacente.

Já enfrentamos o mesmo problema e este é um hack simples que fazemos para contornar o problema.

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

Eu concordo que seria muito melhor ter suporte nativo sem hacks. Se os mantenedores concordarem também, talvez eu possa encontrar algum tempo para propor um PR 🙂.

@ kevin940726 isso seria incrível 👍

Adicionar o seguinte ao meu .storybook / config.js funcionou para mim

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

Eu implementei algo semelhante, mas acho que quebra vários addons, especificamente addon-info para a saída de documentação de prop. Decidi que usar ganchos é mais importante do que essa informação (já que gero isso separadamente de qualquer maneira), mas duvido que isso se aplique a todos os livros de histórias em geral. Não tenho ideia de como você lidaria com ganchos como a API fora do Reac.

Tentei implementá-lo no código-fonte, mas estou tendo dificuldade para fazê-lo funcionar com o storyshot-addon. Acho que isso seria uma mudança significativa, pois geraria um novo nó pai em cada instantâneo. Como não estamos no controle do renderizador que o usuário escolhe, não podemos mergulhar em um nível de profundidade. Acho que devemos pensar em outras alternativas, como documentar a solução e talvez fornecer alguma API auxiliar para os usuários optarem.

Adicionar o seguinte ao meu .storybook / config.js funcionou para mim

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

@emjaksa Você poderia fornecer um trecho disso, por favor?

Esta foi a minha solução alternativa para este problema:

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

Como mencionei, ainda é uma solução alternativa, mas funciona e não quebra o addon info . (Eu não testei outros addons)

Meu complemento de informações não quebra desde que eu tenha adicionado o decorador de história por último.

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)

Ainda outra solução alternativa:

Eu tenho um componente utilitário chamado UseState definido assim:

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

E eu uso isso em histórias como esta:

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

Mas eu gosto mais da solução alternativa de

Não consigo fazer a história ser renderizada novamente na mudança de estado. Forçar a re-renderização também não funciona.

Embora este código funcione bem para React Hooks

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

Não funciona bem com @storybook/addon-info :
Screenshot 2019-05-13 14 35 10

Isso torna esta solução alternativa inutilizável. Alguma ideia? Storybook 5.1.0-beta.0

@artyomtrityak Você pode substituir a opção propTables em addon-info ? Estarei resolvendo isso de maneira adequada no próximo addon-docs : https://medium.com/storybookjs/storybook-docs-sneak-peak-5be78445094a

@shilman também incluirá a fonte de <DropdownBasicStory /> ?

@artyomtrityak vou ver o que posso fazer 😆

Olá a todos! Parece que não tem havido muito nesta edição ultimamente. Se ainda houver perguntas, comentários ou bugs, sinta-se à vontade para continuar a discussão. Infelizmente, não temos tempo para abordar todos os problemas. Estamos sempre abertos a contribuições, portanto, envie-nos uma solicitação de pull se quiser ajudar. Os problemas inativos serão fechados após 30 dias. Obrigado!

Se alguém ainda está entendendo isso, descobri que o problema parece ser adicionar tipos de acessórios a um componente funcional.

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

Dropdown.propTypes = {
  ...
}

Por alguma razão, comentar .propTypes parece funcionar. Não tenho certeza se é um problema com a análise de tipos de props para documentação ou outra coisa.

Para exibir o código-fonte dos componentes com ganchos

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

Olá a todos! Parece que não tem havido muito nesta edição ultimamente. Se ainda houver perguntas, comentários ou bugs, sinta-se à vontade para continuar a discussão. Infelizmente, não temos tempo para abordar todos os problemas. Estamos sempre abertos a contribuições, portanto, envie-nos uma solicitação de pull se quiser ajudar. Os problemas inativos serão fechados após 30 dias. Obrigado!

Tenha cuidado com a abordagem React.createElement e <Story /> , acho que eles não estão muito certos em alguns casos, porque você está na verdade recriando os componentes de reação (o componente de história) em vez de retornar o elementos renderizados (ao usar story() no livro de histórias config.js conforme mostrado na documentação).

Por exemplo, quando sua história tem Knobs.button que dispara atualizações de estado local do componente, depois de clicar no botão, você provavelmente está criando um novo componente de história em vez de atualizar o estado do componente atual.

Você pode verificar minha suposição com este trecho de código simples, onde o valor não será atualizado após clicar no botão de botões giratórios.

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

Para fazer funcionar, você pode fazer:

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

Portanto, como solução alternativa, criei um wrapper:

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

Opcionalmente, você pode imitar a API do livro de histórias para reduzir a sobrecarga de refatoração e voltar mais tarde, quando o problema tiver sido corrigido.

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

Posso estar errado, se estiver, por favor, me avise. Muito apreciado!

Ta-da !! Acabei de lançar https://github.com/storybookjs/storybook/releases/tag/v5.2.0-beta.10 contendo PR # 7571 que faz referência a esse problema. Atualize hoje para experimentar!

Você pode encontrar este pré-lançamento na tag @next NPM.

Fechando esta questão. Abra novamente se achar que ainda há mais a fazer.

@shilman obrigado !! você tem um exemplo de trecho de código de como usá-lo?

@shilman obrigado!
mas ainda obtendo o erro:
React Hook "useState" é chamado na função "componente" que não é um componente de função React nem uma função React Hook customizada

@shiranZe você está usando um 5.2-beta recente?

@shilman sim ...

meu código em histórias / componentes / Menu / index.js:

componente const = () => {
const [buttonEl, setButtonEl] = useState (nulo)

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

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

exportação padrão [leia-me, componente];

e nas histórias / componentes / index.js:
storiesOf('Components', module) .addDecorator(withKnobs) .add('Text', withReadme(...Menu))

@shiranZe acho que é um problema com addon-readme - talvez arquive um problema aí?

Obrigado @shilman pela sua resposta. Não sei se é esse o problema. Tentei sem addon-readme e ainda obtive o mesmo erro. você tem um URL do livro de histórias 5.2-beta?
tentei dar uma olhada em https://storybooks-official.netlify.com/ (outro | demo / Botão)
mas não consigo encontrar lá o exemplo com ganchos de reação.

Ainda tenho o problema com 5.2.0-beta.30 e a questão é que nenhuma das soluções alternativas funcionou para mim, já que estou usando o complemento de botões também.

@shiranZe nosso netlify deploy está desligado (cc @ndelangen), mas você pode verificar o repositório no branch next e experimentá-lo lá.

@sourcesoft Acredito que o problema dos "ganchos de visualização" relacionados aos botões (cc @Hypnosphi) não tem nada a ver com esse problema - a única coisa que eles têm em comum é o conceito de ganchos.

@shilman Obrigado, acho que você está certo. A propósito, descobri como consertar por tentativa e erro, mudei o gancho personalizado:

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

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

para

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

  return {
    onChange,
  };
};

Basicamente, apenas removemos o uso de useCallback aqui. Não tenho certeza se a primeira versão era um gancho válido, mas costumava funcionar. Também é um pouco confuso quando o erro diz Hooks can only be called inside the body of a function component ou ter várias versões do React.

Em seu exemplo acima, após remover useCallback , você está realmente usando o registro de algum gancho em useField ? 🤔

parece que há algum problema com o uso de ganchos com botões. Se alguém puder fornecer uma reprodução simples, ficarei feliz em dar uma olhada

@shilman Você já tentou o exemplo que postei antes? Aqui está o snippet:

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

Está usando a API antiga, mas deve ser fácil de transformar para a API mais recente para teste. Você pode encontrar o comportamento esperado da postagem original .

@zhenwenc FYI o código funciona, mas quebra o uso de documentos renderizados por react-docgen-typescript-webpack-plugin .

A solução alternativa parece um pouco frágil e conflitante com outras bibliotecas. Como alternativa, alguém tem experiência no uso de ?: https://github.com/Sambego/storybook-state

Para aqueles que estão pesquisando a mensagem de erro no Google:

Recebi este erro ao alterar um componente de classe para um componente funcional com ganchos e useState estava importando incorretamente de @storybook/addons . Eu precisava que viesse de react vez de @storybook/addons ... a importação automática falhou.

Apenas respondendo à solicitação de

Adicionar o seguinte ao meu .storybook / config.js funcionou para mim
addDecorator((Story) => <Story />)

@emjaksa Você poderia fornecer um trecho disso, por favor?

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

Resultado (complete .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 />);

Não tenho certeza se esta é a melhor maneira de fazer ganchos de reação funcionarem dentro do livro de histórias, mas envolver o livro de histórias em seu próprio componente <Story /> funcionou aqui com "@storybook/react": "^5.2.6", .

Antes de fazer isso, sempre que eu atualizava um botão (ou seja, um booleano), ele funcionava na primeira renderização, mas parava depois. A solução acima corrigiu isso. A propósito, não tenho certeza se é uma boa prática usar ganchos de reação no livro de histórias, apenas zombe de tudo se possível, provavelmente é melhor assim.

Descreva o bug

Não é possível usar ganchos diretamente em uma história, falha com Hooks can only be called inside the body of a function component .

Reproduzir

Código de exemplo:

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

Comportamento esperado
A primeira história funciona bem, a segunda história falha na renderização inicial

Versões
@storybook/[email protected]
[email protected]
[email protected]

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

Não use a função de seta para criar componentes funcionais.
Faça como um dos exemplos abaixo:

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

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

Ou

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

Se você tiver problemas com "ref" (provavelmente em loops), a solução é usar 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 })}
    />
  );
});

como isso pode ser alcançado em MDX?

mas ainda obtendo o erro:
React Hook "useState" é chamado na função "componente" que não é um componente de função React nem uma função React Hook customizada
https://github.com/storybookjs/storybook/issues/5721#issuecomment -518225880

Eu estava tendo exatamente o mesmo problema. A correção é capitalizar a exportação de história nomeada.

 import React from 'react';
 importar Foo de './Foo';

 export default {
 título: 'Foo';
 };

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

Os documentos dizem que o uso de letras maiúsculas é recomendado, mas é necessário se você quiser que o aviso desapareça.

No meu caso, o problema é que esqueci de importar o gancho que estava usando.

Adicionar o seguinte ao meu .storybook / config.js funcionou para mim

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

@emjaksa Thanx! Testado e funcionando bem no Storybook v5.3.

Eu tive o mesmo problema, meu componente de reação funcional tem useState(value) e value não renderizou novamente o novo valor de Knobs, esse problema é causado por useState:

Dos documentos do React:
Screen Shot 2020-08-07 at 19 00 04

Solução de trabalho Storybook v5.3:

Atualizar 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

E também atualize 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;
  },
};



Alguém sabe por que esse decorador de invocação faz os ganchos funcionarem?

`` `tsx
SomeComponent.decorators = [(Story) => ]

@tmikeschu é porque <Story /> envolve o código em React.createElement que é necessário para ganchos.

@shilman obrigado!

Esta página foi útil?
0 / 5 - 0 avaliações