Storybook: Impossible d'utiliser les hooks React directement dans une histoire

Créé le 22 févr. 2019  ·  53Commentaires  ·  Source: storybookjs/storybook

Décrivez le bogue

Impossible d'utiliser les hooks directement dans une histoire, échoue avec Hooks can only be called inside the body of a function component .

Reproduire

Exemple de code:

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

Comportement prévisible
La première histoire fonctionne bien, la deuxième histoire échoue lors du rendu initial

Versions
@storybook/[email protected]
[email protected]
[email protected]

react has workaround question / support

Commentaire le plus utile

Nous avons rencontré le même problème, et c'est un simple hack que nous faisons pour le contourner.

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

Je suis d'accord qu'il serait beaucoup mieux qu'il soit supporté nativement sans hacks. Si les responsables sont également d'accord, je pourrai peut-être trouver un peu de temps pour élaborer un PR 🙂.

Tous les 53 commentaires

Pas sûr que ce soit un bug. Je pense, et je peux me tromper, que le deuxième argument de stories.add s'attend à ce qu'une fonction renvoie un composant et non un composant React réel. Essayez de déplacer votre composant fonctionnel vers l'extérieur, vous devriez avoir du succès.

Exemple

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

Je pense, et je peux me tromper, que le deuxième argument de stories.add s'attend à ce qu'une fonction renvoie un composant et non un composant React réel.

Cela ne devrait pas vraiment avoir d'importance AFAIK, mais cela vaut toujours la peine d'essayer. De plus, @sargant qu'est-ce qui génère l'erreur? Storybook ou le système de type?

@Keraito Non, je suis sûr que l'erreur est correcte.

C'est parce que nous appelons la fonction hors du contexte de react, aka storybook appellera la fonction comme ceci:

const element = storyFn();

ne pas

const element = <StoryFn />

Très probablement, si nous le lancions comme ça, cela pourrait fonctionner.

Pour le moment, les conseils de @gabefromutah sont

Voici la ligne de code réelle:
https://github.com/storybooks/storybook/blob/next/app/react/src/client/preview/render.js#L24

Si quelqu'un veut expérimenter pour que cela fonctionne, c'est le point de départ, je pense.

@sargant est- ce que la suggestion de

La suggestion de @ndelangen @gabefromutah est la même que mon exemple initial "cette histoire fonctionne" je crois?

Dans le cas où ce n'est pas un bogue, cela peut être une amélioration utile pour éviter d'avoir à utiliser des plugins tiers pour l'état.

Très probablement, si nous le lancions comme ça, cela pourrait fonctionner.

@ndelangen ne addon-info , qui ont besoin d'informations telles que les accessoires du composant rendu sous-jacent.

Nous avons rencontré le même problème, et c'est un simple hack que nous faisons pour le contourner.

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

Je suis d'accord qu'il serait beaucoup mieux qu'il soit supporté nativement sans hacks. Si les responsables sont également d'accord, je pourrai peut-être trouver un peu de temps pour élaborer un PR 🙂.

@ kevin940726 ce serait génial 👍

L'ajout de ce qui suit à mon .storybook / config.js a fonctionné pour moi

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

J'ai implémenté quelque chose de similaire, mais je pense que cela casse un tas d'addons, en particulier addon-info pour la sortie de la documentation prop. J'ai décidé d'utiliser des crochets est plus important que ces informations (car je les génère de toute façon séparément) mais je doute que cela s'applique à tous les livres d'histoires en général. Aucune idée de la façon dont vous géreriez un hook comme une API en dehors de react.

J'ai essayé de l'implémenter dans le code source, mais j'ai du mal à le faire fonctionner avec storyshot-addon. Je pense que ce serait un changement radical car cela générerait un nouveau nœud parent dans chaque instantané. Comme nous ne contrôlons pas le moteur de rendu choisi par l'utilisateur, nous ne pouvons pas plonger dans un niveau de profondeur. Je pense que nous devrons peut-être penser à d'autres alternatives, comme nous pourrions documenter la solution et peut-être fournir une API d'aide aux utilisateurs.

L'ajout de ce qui suit à mon .storybook / config.js a fonctionné pour moi

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

@emjaksa Pourriez-vous fournir un extrait de ceci s'il vous plaît?

C'était ma solution de contournement à ce problème:

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

Comme je l'ai mentionné, c'est toujours une solution de contournement, mais cela fonctionne et ne casse pas l'addon info . (Je n'ai pas testé d'autres addons)

Mon module complémentaire d'informations ne se rompt pas à condition d'avoir ajouté le décorateur d'histoire en dernier.

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)

Encore une autre solution de contournement:

J'ai un composant utilitaire appelé UseState défini comme ceci:

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

Et je l'utilise dans les histoires comme celle-ci:

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

Mais j'aime bien la solution de contournement de @ kevin940726

Je ne parviens pas à obtenir le rendu de l'histoire lors du changement d'état. Le re-rendu forcé ne fonctionne pas non plus.

Bien que ce code fonctionne bien pour React Hooks

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

Cela ne fonctionne pas bien avec @storybook/addon-info :
Screenshot 2019-05-13 14 35 10

Cela rend cette solution de contournement inutilisable. Des idées? Livre d'histoires 5.1.0-beta.0

@artyomtrityak Vous pouvez remplacer l'option propTables dans addon-info ? Je vais résoudre ce problème correctement dans le prochain addon-docs : https://medium.com/storybookjs/storybook-docs-sneak-peak-5be78445094a

@shilman inclura- t-il également la source de <DropdownBasicStory /> ?

@artyomtrityak je vais voir ce que je peux faire 😆

Salut à tous! On dirait qu'il n'y a pas eu grand-chose dans ce numéro ces derniers temps. S'il y a encore des questions, des commentaires ou des bugs, n'hésitez pas à poursuivre la discussion. Malheureusement, nous n'avons pas le temps d'aborder tous les problèmes. Nous sommes toujours ouverts aux contributions, veuillez donc nous envoyer une demande de tirage si vous souhaitez aider. Les problèmes inactifs seront fermés après 30 jours. Merci!

Si quelqu'un d'autre obtient toujours cela, j'ai trouvé que le problème semble être l'ajout de types d'accessoires à un composant fonctionnel.

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

Dropdown.propTypes = {
  ...
}

Pour une raison quelconque, commenter .propTypes semble le faire fonctionner. Je ne sais pas si c'est un problème avec l'analyse des types d'accessoires pour la documentation ou autre chose.

Pour afficher le code source des composants avec des crochets

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

Salut à tous! On dirait qu'il n'y a pas eu grand-chose dans ce numéro ces derniers temps. S'il y a encore des questions, des commentaires ou des bugs, n'hésitez pas à poursuivre la discussion. Malheureusement, nous n'avons pas le temps d'aborder tous les problèmes. Nous sommes toujours ouverts aux contributions, veuillez donc nous envoyer une demande de tirage si vous souhaitez aider. Les problèmes inactifs seront fermés après 30 jours. Merci!

Soyez prudent avec les approches React.createElement et <Story /> ci-dessus, je pense qu'elles ne sont pas tout à fait correctes dans certains cas, car vous recréez en fait les composants de réaction (le composant de l'histoire) au lieu de renvoyer le éléments rendus (en utilisant story() dans le livre d'histoires config.js comme indiqué dans la documentation).

Par exemple, lorsque votre histoire a Knobs.button qui déclenche les mises à jour de l'état local du composant, après avoir cliqué sur le bouton, vous créez probablement un nouveau composant d'histoire au lieu de mettre à jour l'état du composant actuel.

Vous pouvez vérifier mon hypothèse avec cet extrait de code simple, où la valeur ne sera pas mise à jour après avoir cliqué sur le bouton des boutons.

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

Pour que cela fonctionne, vous pouvez faire:

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

Par conséquent, comme solution de contournement, j'ai créé un 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>;
});

Si vous le souhaitez, vous pouvez imiter l'API du livre d'histoires pour réduire la charge de refactorisation et revenir plus tard une fois le problème résolu.

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

Je peux me tromper, si oui, faites-le moi savoir. Très appréciée!

Ta-da !! Je viens de publier https://github.com/storybookjs/storybook/releases/tag/v5.2.0-beta.10 contenant PR # 7571 qui fait référence à ce problème. Mettez à niveau dès aujourd'hui pour l'essayer!

Vous pouvez trouver cette avant-première sur le tag @next NPM.

Clôture de ce numéro. Veuillez rouvrir si vous pensez qu'il reste encore beaucoup à faire.

@shilman merci !! avez-vous un exemple d'extrait de code expliquant comment l'utiliser?

@shilman merci!
mais toujours l'erreur:
React Hook "useState" est appelé dans la fonction "component" qui n'est ni un composant de fonction React ni une fonction React Hook personnalisée

@shiranZe utilisez-vous une version 5.2-beta récente?

@shilman oui ...

mon code sur les stories / composants / Menu / index.js:

composant const = () => {
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>

export par défaut [readme, composant];

et dans les stories / components / index.js:
storiesOf('Components', module) .addDecorator(withKnobs) .add('Text', withReadme(...Menu))

@shiranZe Je suppose que c'est un problème avec addon-readme - peut-être y déposer un problème?

Merci @shilman pour votre réponse. Je ne sais pas si c'est le problème. J'ai essayé sans addon-readme et j'ai toujours la même erreur. avez-vous l'URL du livre d'histoire 5.2-beta?
j'ai essayé de jeter un oeil à https://storybooks-official.netlify.com/ (autre | démo / Button)
mais je ne trouve pas là l'exemple avec des hooks de réaction.

J'ai toujours le problème avec 5.2.0-beta.30 et le problème est qu'aucune des solutions de contournement n'a fonctionné pour moi, car j'utilise également l'addon de boutons.

@shiranZe, notre déploiement netlify est désactivé (cc @ndelangen) mais vous pouvez consulter le dépôt sur la branche next et l'essayer ici.

@sourcesoft Je crois que le problème des "crochets de prévisualisation" lié aux boutons (cc @Hypnosphi) n'a rien à voir avec ce problème - la seule chose qu'ils ont en commun est le concept de hooks.

@shilman Merci, je pense que vous avez raison. Btw j'ai compris comment le réparer par essais et erreurs, changé le hook personnalisé:

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

En gros, vient de supprimer l'utilisation de useCallback ici. Je ne sais pas si la première version était un hook valide, mais cela fonctionnait auparavant. De plus, c'est un peu déroutant lorsque l'erreur indique Hooks can only be called inside the body of a function component ou d'avoir plusieurs versions de React.

Dans votre exemple ci-dessus, après avoir supprimé useCallback , utilisez- vous réellement l'enregistrement d'un hook dans useField ? 🤔

il semble qu'il y ait un problème avec l'utilisation de crochets avec des boutons. Si quelqu'un peut fournir un simple repro, je serais heureux de l'examiner

@shilman Avez-vous essayé l'exemple que j'ai posté plus tôt? Voici l'extrait:

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

Il utilise l'ancienne API, mais devrait être facile à transformer vers la dernière API pour les tests. Vous pouvez trouver le comportement attendu de la publication d'origine .

@zhenwenc Pour info, le code fonctionne, mais interrompt l'utilisation des documents rendus par react-docgen-typescript-webpack-plugin .

La solution de contournement semble un peu fragile et en conflit avec d'autres bibliothèques. Au lieu de cela, quelqu'un a-t-il une expérience d'utilisation?: Https://github.com/Sambego/storybook-state

Pour ceux qui recherchent ici le message d'erreur:

J'ai eu cette erreur quand un changement d'un composant de classe en un composant fonctionnel avec des hooks et useState importait incorrectement de @storybook/addons . J'avais besoin qu'il provienne de react plutôt que de @storybook/addons ... échec de l'importation automatique.

Je réponds simplement à la demande d'extrait de

L'ajout de ce qui suit à mon .storybook / config.js a fonctionné pour moi
addDecorator((Story) => <Story />)

@emjaksa Pourriez-vous fournir un extrait de ceci s'il vous plaît?

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

Résultat (compléter .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 />);

Je ne suis pas sûr que ce soit le meilleur moyen de faire fonctionner les hooks de réaction dans un livre de contes, mais envelopper le livre de contes dans son propre composant <Story /> fonctionné ici avec "@storybook/react": "^5.2.6", .

Avant de faire cela, chaque fois que je mettais à jour un bouton (c'est-à-dire: un booléen), cela fonctionnait sur le premier rendu, mais arrêtait ensuite le rendu. La solution ci-dessus a corrigé cela. Btw, je ne suis pas sûr que ce soit une bonne pratique d'utiliser des hooks de réaction dans un livre d'histoires, moquez-vous de tout si possible, c'est probablement mieux ainsi.

Décrivez le bogue

Impossible d'utiliser les hooks directement dans une histoire, échoue avec Hooks can only be called inside the body of a function component .

Reproduire

Exemple de code:

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

Comportement prévisible
La première histoire fonctionne bien, la deuxième histoire échoue lors du rendu initial

Versions
@storybook/[email protected]
[email protected]
[email protected]

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

N'utilisez pas la fonction de flèche pour créer des composants fonctionnels.
Faites comme l'un des exemples ci-dessous:

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

Si vous rencontrez des problèmes avec "ref" (probablement dans des boucles), la solution est d'utiliser 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 })}
    />
  );
});

comment cela peut-il être réalisé dans MDX?

mais toujours l'erreur:
React Hook "useState" est appelé dans la fonction "component" qui n'est ni un composant de fonction React ni une fonction React Hook personnalisée
https://github.com/storybookjs/storybook/issues/5721#issuecomment -518225880

J'avais exactement le même problème. Le correctif consiste à mettre en majuscule l'exportation de l'histoire nommée.

 importer React depuis 'react';
 import Foo de './Foo';

 export par défaut {
 titre: 'Foo';
 };

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

La documentation indique que la capitalisation est recommandée, mais elle est nécessaire si vous souhaitez que cet avertissement disparaisse.

Dans mon cas, le problème était que j'avais oublié d'importer le crochet que j'utilisais.

L'ajout de ce qui suit à mon .storybook / config.js a fonctionné pour moi

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

@emjaksa Thanx! Testé et fonctionne bien sur Storybook v5.3.

J'ai eu le même problème, mon composant de réaction fonctionnel a useState(value) et value n'a pas rendu la nouvelle valeur de Knobs, ce problème est causé par useState:

À partir de la documentation React:
Screen Shot 2020-08-07 at 19 00 04

Solution de travail Storybook v5.3:

Mettre à jour 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

Et aussi mettre à jour 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;
  },
};



Est-ce que quelqu'un sait pourquoi ce décorateur invoquant fait fonctionner les crochets?

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

@tmikeschu c'est parce que <Story /> enveloppe le code dans React.createElement qui est nécessaire pour les hooks.

@shilman merci!

Cette page vous a été utile?
0 / 5 - 0 notes