Back to Intro to Storybook
Chapters
  • Begin
  • Eenvoudige component
  • Samengestelde component
  • Data
  • Schermen
  • Deploy
  • Testen
  • Addons
  • Conclusie
  • Bijdragen

Bouw een samengestelde component

Bouw een samengestelde component uit eenvoudigere componenten
Deze gemeenschapsvertaling is nog niet bijgewerkt naar de nieuwste versie van Storybook. Help ons om het bij te werken door de wijzigingen in de Nederlandse gids voor deze vertaling toe te passen. Pull requests ze zijn welkom.

In het vorige hoofdstuk hebben we onze eerste component gebouwd; dit hoofdstuk gaat voort op wat we geleerd hebben bij het bouwen van TaskList, een lijst van taken. Laten we nu een aantal componenten combineren en zien wat er gebeurt wanneer de complexiteit toeneemt.

Tasklist

Taskbox benadrukt gepinde taken door ze boven de standaard taken te plaatsen. Dit resulteert in 2 variaties van TaskList waarvoor we stories moeten creëren: standaard taken en gepinde taken.

standaard en gepinde taken

Omdat Task data asynchroon verzonden kan worden, moeten we ook een laadstatus weergeven als er geen verbinding is. Bovendien is een lege status vereist wanneer er geen taken zijn.

lege en ladende taken

Voorbereiding

Een samengestelde component is niet heel verschillend van het basis component die het omvat. Maak bestanden voor het TaskList component en de bijhorende stories aan: src/components/TaskList.js en src/components/TaskList.stories.js.

Start met een ruwe implementatie van TaskList. Je zal de Task component van eerder moeten importeren en de attributen en acties als input doorgeven.

Copy
src/components/TaskList.js
import React from 'react';

import Task from './Task';

function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
  const events = {
    onPinTask,
    onArchiveTask,
  };

  if (loading) {
    return <div className="list-items">loading</div>;
  }

  if (tasks.length === 0) {
    return <div className="list-items">empty</div>;
  }

  return (
    <div className="list-items">
      {tasks.map((task) => (
        <Task key={task.id} task={task} {...events} />
      ))}
    </div>
  );
}

export default TaskList;

Maak vervolgens de teststatussen van TaskList aan in het stories bestand.

Copy
src/components/TaskList.stories.js
import React from 'react';
import { storiesOf } from '@storybook/react';

import TaskList from './TaskList';
import { task, actions } from './Task.stories';

export const defaultTasks = [
  { ...task, id: '1', title: 'Task 1' },
  { ...task, id: '2', title: 'Task 2' },
  { ...task, id: '3', title: 'Task 3' },
  { ...task, id: '4', title: 'Task 4' },
  { ...task, id: '5', title: 'Task 5' },
  { ...task, id: '6', title: 'Task 6' },
];

export const withPinnedTasks = [
  ...defaultTasks.slice(0, 5),
  { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
];

storiesOf('TaskList', module)
  .addDecorator((story) => <div style={{ padding: '3rem' }}>{story()}</div>)
  .add('default', () => <TaskList tasks={defaultTasks} {...actions} />)
  .add('withPinnedTasks', () => <TaskList tasks={withPinnedTasks} {...actions} />)
  .add('loading', () => <TaskList loading tasks={[]} {...actions} />)
  .add('empty', () => <TaskList tasks={[]} {...actions} />);

addDecorator() stelt ons in staat om "context" toe te voegen aan het renderen van elke taak. In dit geval voegen we padding toe rond de lijst zodat het makkelijker is om deze visueel te verifiëren.

Decorators zijn een manier om willekeurige wrappers toe te voegen aan _stories_. In dit geval gebruiken we een decorator om styling toe te voegen. Ze kunnen ook gebruikt worden om _stories_ te wrappen in "providers" – d.w.z library componenten die een React context aanmaken.

task voorziet de vorm van een Task die we gemaakt en geëxporteerd hebben van het Task.stories.js bestand. Op een gelijkaardige manier definieren actions de acties (mocked callbacks) die een Task component verwacht, en de TaskList ook nodig heeft.

Controleer nu Storybook voor de nieuwe TaskList stories.

Statussen uitbouwen

Onze component is nog steeds onvolledig, maar nu hebben we een idee van de stories waar we naartoe willen werken. Misschien denk je dat de .list-items wrapper te simplistisch is. Dat klopt - in de meeste gevallen zouden we geen nieuwe component aanmaken alleen maar om een wrapper toe te voegen. De echte complexiteit van de TaskList component wordt echter pas zichtbaar in de randgevallen withPinnedTasks, loading en empty.

Copy
src/components/TaskList.js
import React from 'react';

import Task from './Task';

function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
  const events = {
    onPinTask,
    onArchiveTask,
  };

  const LoadingRow = (
    <div className="loading-item">
      <span className="glow-checkbox" />
      <span className="glow-text">
        <span>Loading</span> <span>cool</span> <span>state</span>
      </span>
    </div>
  );

  if (loading) {
    return (
      <div className="list-items">
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
      </div>
    );
  }

  if (tasks.length === 0) {
    return (
      <div className="list-items">
        <div className="wrapper-message">
          <span className="icon-check" />
          <div className="title-message">You have no tasks</div>
          <div className="subtitle-message">Sit back and relax</div>
        </div>
      </div>
    );
  }

  const tasksInOrder = [
    ...tasks.filter((t) => t.state === 'TASK_PINNED'),
    ...tasks.filter((t) => t.state !== 'TASK_PINNED'),
  ];

  return (
    <div className="list-items">
      {tasksInOrder.map((task) => (
        <Task key={task.id} task={task} {...events} />
      ))}
    </div>
  );
}

export default TaskList;

De toegevoegde markup resulteert in de volgende UI:

Merk de positie van de gepinde item in de lijst op. We willen het gepinde item bovenaan de lijst tonen om het een prioriteit te geven voor de gebruiker.

Data vereisten en props

Naarmate de component groeit, doen de input vereisten dat ook. Definieer nu de vereiste props van TaskList. Omdat Task een child component is, moeten we zeker zijn dat de data in de juiste vorm wordt aangeleverd om deze te renderen. Om tijd en zorgen te besparen, kunnen we de propTypes herbruiken die je eerder in Task gedefinieerd hebt.

Copy
src/components/TaskList.js
import React from 'react';
import PropTypes from 'prop-types';

import Task from './Task';

function TaskList() {
  ...
}


TaskList.propTypes = {
  loading: PropTypes.bool,
  tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
  onPinTask: PropTypes.func.isRequired,
  onArchiveTask: PropTypes.func.isRequired,
};

TaskList.defaultProps = {
  loading: false,
};

export default TaskList;

Geautomatiseerde testen

In het vorige hoofdstuk leerden we test stories te snapshotten aan de hand van Storyshots. Voor Task was er niet veel complexiteit om te testen behalve om te kijken of dit normaal rendert. Omdat TaskList een extra laag complexiteit toevoegt, willen we verifiëren dat bepaalde inputs zekere outputs teruggeven op een manier die gebruikt kan worden voor geautomatiseerde testen. Om dit mogelijk te maken zullen we unit tests toevoegen aan de hand van Jest gekoppeld aan een test renderer zoals Enzyme. Jest logo

Unit tests met Jest

Storybook stories gekoppeld met handmatige visuele testen en snapshot tests (zie hierboven) zijn een goede manier om UI bugs te voorkomen. Als stories een grote diversiteit aan component use cases voor hun rekening nemen, en we gebruiken tools om er zeker van te zijn dat een mens een verandering aan een story controleert, zijn fouten minder waarschijnlijk.

Het gevaar zit echter in de details. We hebben daarom tests nodig die alle details controleren. Dit brengt ons bij unit tests.

In ons geval willen we de TaskList elke gepinde taak laten renderen voor niet gepinde taken die deze doorgegeven heeft in de tasks prop. Ook al hebben we een story (withPinnedTasks) om exact dit scenario te testen, kan het onduidelijk zijn voor een menselijke reviewer dat indien de component stopt met taken op deze manier te sorteren, het een bug is. Voor het menselijk oog zal dit niet opvallen als foutief gedrag.

Om dit probleem te voorkomen, kunnen we Jest gebruiken om de story te renderen naar de DOM en wat code uit te voeren om opvallende kenmerken van de output te verifiëren.

Maak een test bestand src/components/TaskList.test.js aan. Hier zullen we onze testen schrijven die de output controleren.

Copy
src/components/TaskList.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import TaskList from './TaskList';
import { withPinnedTasks } from './TaskList.stories';

it('renders pinned tasks at the start of the list', () => {
  const div = document.createElement('div');
  const events = { onPinTask: jest.fn(), onArchiveTask: jest.fn() };
  ReactDOM.render(<TaskList tasks={withPinnedTasks} {...events} />, div);

  // We expect the task titled "Task 6 (pinned)" to be rendered first, not at the end
  const lastTaskInput = div.querySelector('.list-item:nth-child(1) input[value="Task 6 (pinned)"]');
  expect(lastTaskInput).not.toBe(null);

  ReactDOM.unmountComponentAtNode(div);
});

TaskList test runner

Merk op dat we de withPinnedTasks lijst van taken hebben kunnen herbruiken in zowel de story als de unit test; zo kunnen we doorgaan met het benutten van een bestaande bron, namelijk de voorbeelden die relevante variaties van een component vertegenwoordigen.

Merk ook op dat deze test veel onderhoud vereist. Het is mogelijk dat naarmate het project volwassen wordt en de exacte implementatie van de Task verandert - misschien door gebruik van een andere klassenaam of eentextarea in plaats van een input - de test mislukt en moet worden bijgewerkt. Dit is niet noodzakelijk een probleem, maar eerder een indicatie om voorzichtig te zijn met het royaal gebruiken van unit-tests voor UI componenten. Ze zijn niet makkelijk te onderhouden. Vertrouw in plaats daarvan waar mogelijk op snapshot tests en visuele regressie tests (zie testhoofdstuk).

Keep your code in sync with this chapter. View 429780a on GitHub.
Is this free guide helping you? Tweet to give kudos and help other devs find it.
Next Chapter
Data
Leer hoe je data kunt doorgeven aan je UI component
✍️ Edit on GitHub – PRs welcome!
Join the community
6,616 developers and counting
WhyWhy StorybookComponent-driven UI
Open source software
Storybook

Maintained by
Chromatic
Special thanks to Netlify and CircleCI