Storybook: No se pueden usar los ganchos de React directamente dentro de una historia

Creado en 22 feb. 2019  ·  53Comentarios  ·  Fuente: storybookjs/storybook

Describe el error

No se pueden usar ganchos directamente en una historia, falla con Hooks can only be called inside the body of a function component .

Reproducir

Código de ejemplo:

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

Comportamiento esperado
El primer piso funciona bien, el segundo piso falla en el renderizado inicial

Versiones
@storybook/[email protected]
[email protected]
[email protected]

react has workaround question / support

Comentario más útil

Hemos experimentado el mismo problema, y ​​este es un truco simple que hacemos para solucionarlo.

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

Estoy de acuerdo en que sería mucho mejor que fuera compatible de forma nativa sin hacks. Si los mantenedores también están de acuerdo, tal vez pueda encontrar algo de tiempo para hacer un PR 🙂.

Todos 53 comentarios

No estoy seguro de que sea un error. Creo, y podría estar equivocado, el segundo argumento de stories.add espera que una función devuelva un componente y no un componente React real. Intente mover el componente de su función al exterior, debería tener algo de éxito.

Ejemplo

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

Creo, y podría estar equivocado, el segundo argumento de stories.add espera que una función devuelva un componente y no un componente React real.

Realmente no debería importar AFAIK, pero siempre vale la pena intentarlo. Además, @sargant ¿qué arroja el error? ¿Libro de cuentos o el sistema de tipos?

@Keraito No, estoy bastante seguro de que el error es correcto.

Es porque estamos llamando a la función fuera del contexto de react, también conocido como storybook llamará a la función así:

const element = storyFn();

no

const element = <StoryFn />

Posiblemente si lo iniciamos así podría funcionar.

Ahora mismo , el consejo de

Aquí está la línea de código real:
https://github.com/storybooks/storybook/blob/next/app/react/src/client/preview/render.js#L24

Si alguien quiere experimentar para que esto funcione, ese es el lugar para comenzar, creo.

@sargant, ¿la sugerencia de @gabefromutah funciona para ti?

¿ Creo que la sugerencia de @ndelangen @gabefromutah es la misma que mi ejemplo inicial de "esta historia funciona"?

En el caso de que esto no sea un error, podría ser una mejora útil para evitar tener que usar complementos de terceros para el estado.

Posiblemente si lo iniciamos así podría funcionar.

@ndelangen no está completamente seguro de cómo interactuará con otros complementos, especialmente addon-info , que necesitan información como accesorios del componente renderizado subyacente.

Hemos experimentado el mismo problema, y ​​este es un truco simple que hacemos para solucionarlo.

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

Estoy de acuerdo en que sería mucho mejor que fuera compatible de forma nativa sin hacks. Si los mantenedores también están de acuerdo, tal vez pueda encontrar algo de tiempo para hacer un PR 🙂.

@ kevin940726 eso sería increíble 👍

Agregar lo siguiente a mi .storybook / config.js funcionó para mí

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

Implementé algo similar, pero creo que eso rompe un montón de complementos, específicamente addon-info para generar documentación de prop. Decidí que usar ganchos es más importante que esa información (ya que de todos modos los genero por separado) pero dudo que se aplique a todos los libros de cuentos en general. No tengo idea de cómo manejarías un gancho como api fuera de reaccionar.

Traté de implementarlo en el código fuente, pero estoy teniendo dificultades para hacerlo funcionar con storyshot-addon. Creo que este sería un cambio importante, ya que generaría un nuevo nodo principal en cada instantánea. Como no tenemos el control del renderizador que elige el usuario, no podemos sumergirnos en un nivel de profundidad. Creo que podríamos tener que pensar en otras alternativas, como que podríamos documentar la solución y tal vez proporcionar alguna API auxiliar para que los usuarios opten por participar.

Agregar lo siguiente a mi .storybook / config.js funcionó para mí

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

@emjaksa ¿Podría

Esta fue mi solución a 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 mencioné, sigue siendo una solución alternativa, pero funciona y no rompe el complemento info . (No he probado otros complementos)

Mi complemento de información no se rompe siempre que agregue el decorador de historias en último lugar.

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)

Otra solución alternativa:

Tengo un componente de utilidad llamado UseState definido así:

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

Y lo uso en historias como esta:

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

Pero me gusta la solución alternativa de @ kevin940726 la mejor

No puedo hacer que la historia se vuelva a representar en el cambio de estado. Forzar la renderización tampoco funciona.

Si bien este código funciona bien para React Hooks

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

No funciona bien con @storybook/addon-info :
Screenshot 2019-05-13 14 35 10

Esto hace que esta solución sea inutilizable. ¿Algunas ideas? Libro de cuentos 5.1.0-beta.0

@artyomtrityak Puede anular la opción propTables en addon-info ? Resolveré esto correctamente en el próximo addon-docs : https://medium.com/storybookjs/storybook-docs-sneak-peak-5be78445094a

@shilman, ¿ incluirá también la fuente de <DropdownBasicStory /> ?

@artyomtrityak Veré qué puedo hacer 😆

¡Hola a todos! Parece que últimamente no ha habido mucho en este tema. Si todavía hay preguntas, comentarios o errores, no dude en continuar con la discusión. Desafortunadamente, no tenemos tiempo para abordar todos los problemas. Siempre estamos abiertos a contribuciones, así que envíenos una solicitud de extracción si desea ayudar. Los problemas inactivos se cerrarán después de 30 días. ¡Gracias!

Si alguien más sigue recibiendo esto, descubrí que el problema parece ser agregar tipos de accesorios a un componente funcional.

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

Dropdown.propTypes = {
  ...
}

Por alguna razón, comentar .propTypes parece hacer que funcione. No estoy seguro de si se trata de un problema con los tipos de accesorios que analizan la documentación o algo más.

Para mostrar el código fuente de los componentes con 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>
));

¡Hola a todos! Parece que últimamente no ha habido mucho en este tema. Si todavía hay preguntas, comentarios o errores, no dude en continuar con la discusión. Desafortunadamente, no tenemos tiempo para abordar todos los problemas. Siempre estamos abiertos a contribuciones, así que envíenos una solicitud de extracción si desea ayudar. Los problemas inactivos se cerrarán después de 30 días. ¡Gracias!

Tenga cuidado con el enfoque React.createElement y <Story /> , creo que no son del todo correctos en algunos casos, porque en realidad está recreando los componentes de reacción (el componente de historia) en lugar de devolver el elementos renderizados (mientras se usa story() en el libro config.js cuentos

Por ejemplo, cuando su historia tiene Knobs.button que activa las actualizaciones del estado local del componente, después de hacer clic en el botón, es probable que esté creando un nuevo componente de la historia en lugar de actualizar el estado del componente actual.

Puede verificar mi suposición con este simple fragmento de código, donde el valor no se actualizará después de hacer clic en el botón de perillas.

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 que funcione, puede hacer:

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

Por lo tanto, como solución temporal, he creado un contenedor:

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, puede imitar la API del libro de cuentos para reducir la sobrecarga de refactorización y volver a cambiar más tarde cuando se haya solucionado el problema.

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

Podría estar equivocado, si es así, hágamelo saber. ¡Muy apreciado!

Ta-da !! Acabo de publicar https://github.com/storybookjs/storybook/releases/tag/v5.2.0-beta.10 que contiene PR # 7571 que hace referencia a este problema. ¡Actualice hoy para probarlo!

Puede encontrar esta versión preliminar en la etiqueta @next NPM.

Cerrando este tema. Vuelva a abrir si cree que aún hay más por hacer.

@shilman gracias !! ¿Tiene un ejemplo de fragmento de código de cómo usarlo?

@shilman gracias!
pero sigue recibiendo el error:
React Hook "useState" se llama en la función "componente" que no es ni un componente de la función React ni una función personalizada React Hook

@shiranZe, ¿estás usando una versión reciente de 5.2-beta?

@shilman sí ...

mi código en stories / components / Menu / index.js:

componente constante = () => {
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>

exportar predeterminado [léame, componente];

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

@shiranZe supongo que es un problema con addon-readme , ¿tal vez presentar un problema allí?

Gracias @shilman por tu respuesta. No sé si ese es el problema. Intenté sin addon-readme y sigo recibiendo el mismo error. ¿Tiene una URL del libro de cuentos 5.2-beta?
Intenté echar un vistazo a https://storybooks-official.netlify.com/ (otro | demo / Botón)
pero no puedo encontrar el ejemplo con react hooks.

Todavía tengo el problema con 5.2.0-beta.30 y la cosa es que ninguna de las soluciones me funcionó, ya que también estoy usando el complemento de perillas.

@shiranZe, nuestro despliegue de netlify está desactivado (cc @ndelangen) pero puede consultar el repositorio en la rama next y probarlo allí.

@sourcesoft Creo que el problema de los "ganchos de vista previa" relacionados con las perillas (cc @Hypnosphi) no tiene nada que ver con este problema; lo único que tienen en común es el concepto de ganchos.

@shilman Gracias, creo que tienes razón. Por cierto, descubrí cómo solucionarlo por prueba y error, cambié el gancho personalizado:

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

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

a

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

  return {
    onChange,
  };
};

Básicamente, eliminó el uso de useCallback aquí. No estoy seguro de si la primera versión era un gancho válido, pero solía funcionar. También es un poco confuso cuando el error dice Hooks can only be called inside the body of a function component o tener varias versiones de React.

En su ejemplo anterior, después de eliminar el useCallback , ¿está realmente utilizando el registro de algún gancho en useField ? 🤔

Parece que hay algún problema con el uso de ganchos con perillas. Si alguien puede proporcionar una reproducción sencilla, me complacerá echarle un vistazo.

@shilman ¿Has probado el ejemplo que

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 la API anterior, pero debería ser fácil de transformar a la API más reciente para realizar pruebas. Puede encontrar el comportamiento esperado en la publicación original .

@zhenwenc FYI, el código funciona, pero interrumpe el uso de documentos representados por react-docgen-typescript-webpack-plugin .

La solución parece un poco frágil y conflictiva con otras bibliotecas. Como alternativa, ¿alguien tiene experiencia en el uso ?: https://github.com/Sambego/storybook-state

Para aquellos que buscan en Google el mensaje de error:

Recibí este error cuando cambiaba un componente de clase a un componente funcional con ganchos y useState importaba incorrectamente desde @storybook/addons . Necesitaba que viniera de react lugar de @storybook/addons ... falla de importación automática.

Solo respondiendo a la solicitud de @orpheus de un fragmento

Agregar lo siguiente a mi .storybook / config.js funcionó para mí
addDecorator((Story) => <Story />)

@emjaksa ¿Podría

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

No estoy seguro de si esta es la mejor manera de hacer que los ganchos de reacción funcionen dentro del libro de cuentos, pero envolver el libro de cuentos en su propio componente <Story /> funcionó aquí con "@storybook/react": "^5.2.6", .

Antes de hacer esto, cada vez que actualizaba una perilla (es decir, un booleano), funcionaba en el primer renderizado, pero luego dejaba de renderizar. La solución anterior solucionó eso. Por cierto, no estoy seguro de que sea una buena práctica usar los ganchos de reacción en el libro de cuentos, solo burlarse de todo si es posible, probablemente sea mejor así.

Describe el error

No se pueden usar ganchos directamente en una historia, falla con Hooks can only be called inside the body of a function component .

Reproducir

Código de ejemplo:

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

Comportamiento esperado
El primer piso funciona bien, el segundo piso falla en el renderizado inicial

Versiones
@storybook/[email protected]
[email protected]
[email protected]

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

No utilice la función de flecha para crear componentes funcionales.
Haz como uno de los ejemplos siguientes:

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

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

O

//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 tiene problemas con "ref" (probablemente en bucles), la solución es 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 })}
    />
  );
});

¿Cómo se puede lograr esto en MDX?

pero sigue recibiendo el error:
React Hook "useState" se llama en la función "componente" que no es ni un componente de la función React ni una función personalizada React Hook
https://github.com/storybookjs/storybook/issues/5721#issuecomment -518225880

Estaba teniendo exactamente el mismo problema. La solución es capitalizar la exportación de la historia nombrada.

 importar React de 'reaccionar';
 importar Foo desde './Foo';

 exportar predeterminado {
 título: 'Foo';
 };

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

Los documentos dicen que se recomienda usar

En mi caso, el problema fue que olvidé importar el gancho que estaba usando.

Agregar lo siguiente a mi .storybook / config.js funcionó para mí

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

@emjaksa ¡Gracias! Probado y funcionando bien en Storybook v5.3.

Tuve el mismo problema, mi componente funcional de reacción tiene useState(value) y value no volvió a renderizar el nuevo valor de Knobs, este problema es causado por useState:

De los documentos de React:
Screen Shot 2020-08-07 at 19 00 04

Solución de trabajo Storybook v5.3:

Actualizar 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

Y también actualice 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;
  },
};



¿Alguien sabe por qué este decorador invocador hace que funcionen los ganchos?

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

@tmikeschu es porque <Story /> envuelve el código en React.createElement que es necesario para los ganchos.

@shilman gracias!

¿Fue útil esta página
0 / 5 - 0 calificaciones