Intro to Storybook
  • Get started
  • Simple component
  • Composite component
  • Data
  • Screens
  • Deploy
  • Testing
  • Addons
  • Conclusion
  • Contribute
Framework:
ReactReact NativeVueAngularSvelteEmber

Wire in data

Learn how to wire in data to your UI component

So far, we have created isolated stateless components-–great for Storybook, but ultimately not helpful until we give them some data in our app.

This tutorial doesn’t focus on the particulars of building an app, so we won’t dig into those details here. But we will take a moment to look at a common pattern for wiring in data with container components.

Container components

Our TaskList component as currently written is “presentational” in that it doesn’t talk to anything external to its own implementation. To get data into it, we need a “container”.

This example uses Redux Toolkit, the most effective toolset for developing applications for storing data with Redux, to build a simple data model for our app. However, the pattern used here applies just as well to other data management libraries like Apollo and MobX.

Add the necessary dependencies to your project with:

yarn add @reduxjs/toolkit react-redux

First, we’ll construct a simple Redux store that responds to actions that change the task's state in a file called store.js in the src/lib directory (intentionally kept simple):

src/lib/store.js
/* A simple redux store/actions/reducer implementation.
 * A true app would be more complex and separated into different files.
 */
import { configureStore, createSlice } from '@reduxjs/toolkit';

/*
 * The initial state of our store when the app loads.
 * Usually, you would fetch this from a server.
 */
const defaultTasks = [
  { id: '1', title: 'Something', state: 'TASK_INBOX' },
  { id: '2', title: 'Something more', state: 'TASK_INBOX' },
  { id: '3', title: 'Something else', state: 'TASK_INBOX' },
  { id: '4', title: 'Something again', state: 'TASK_INBOX' },
];

/*
 * The store is created here.
 * You can read more about Redux Toolkit's slices in the docs:
 * https://redux-toolkit.js.org/api/createSlice
 */
const TasksSlice = createSlice({
  name: 'tasks',
  initialState: defaultTasks,
  reducers: {
    updateTaskState: (state, action) => {
      const { id, newTaskState } = action.payload;
      const task = state.findIndex(task => task.id === id);
      if (task >= 0) {
        state[task].state = newTaskState;
      }
    },
  },
});

// The actions contained in the slice are exported for usage in our components
export const { updateTaskState } = TasksSlice.actions;

/*
 * Our app's store configuration goes here.
 * Read more about Redux's configureStore in the docs:
 * https://redux-toolkit.js.org/api/configureStore
 */
const store = configureStore({
  reducer: {
    tasks: TasksSlice.reducer,
  },
});

export default store;

Then we’ll update our TaskList component to connect to the Redux store and render the tasks we are interested in:

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

import Task from './Task';

import { useDispatch, useSelector } from 'react-redux';
import { updateTaskState } from '../lib/store';

export function PureTaskList({ loading, tasks, onPinTask, onArchiveTask }) {
  /* previous implementation of TaskList */
}

PureTaskList.propTypes = {
  /** Checks if it's in loading state */
  loading: PropTypes.bool,
  /** The list of tasks */
  tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
  /** Event to change the task to pinned */
  onPinTask: PropTypes.func.isRequired,
  /** Event to change the task to archived */
  onArchiveTask: PropTypes.func.isRequired,
};

PureTaskList.defaultProps = {
  loading: false,
};

export function TaskList() {
  // We're retrieving our state from the store
  const tasks = useSelector(state => state.tasks);
  // We're defining an variable to handle dispatching the actions back to the store
  const dispatch = useDispatch();

  const pinTask = value => {
    // We're dispatching the Pinned event back to our store
    dispatch(updateTaskState({ id: value, newTaskState: 'TASK_PINNED' }));
  };
  const archiveTask = value => {
    // We're dispatching the Archive event back to our store
    dispatch(updateTaskState({ id: value, newTaskState: 'TASK_ARCHIVED' }));
  };

  const filteredTasks = tasks.filter(t => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED');
  return (
    <PureTaskList
      tasks={filteredTasks}
      onPinTask={task => pinTask(task)}
      onArchiveTask={task => archiveTask(task)}
    />
  );
}

Now that we have some actual data populating our component, obtained from the Redux store, we could have wired it to src/App.js and render the component there. But for now, let's hold off doing that and continue on our component-driven journey.

Don't worry about it. We'll take care of it in the next chapter.

Our Storybook tests will have stopped working at this stage because TaskList is now a container and no longer expects any props. Instead, TaskList connects to the store and sets the props on the PureTaskList component it wraps.

However, we can quickly solve this problem by simply rendering the PureTaskList--the presentational component, to which we've just added the export statement in the previous step-- in our Storybook stories:

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

+ import { PureTaskList } from './TaskList';
import * as TaskStories from './Task.stories';

export default {
+ component: PureTaskList,
  title: 'PureTaskList',
  decorators: [story => <div style={{ padding: '3rem' }}>{story()}</div>],
};

+ const Template = args => <PureTaskList {...args} />;

export const Default = Template.bind({});
Default.args = {
  // Shaping the stories through args composition.
  // The data was inherited the Default story in task.stories.js.
  tasks: [
    { ...TaskStories.Default.args.task, id: '1', title: 'Task 1' },
    { ...TaskStories.Default.args.task, id: '2', title: 'Task 2' },
    { ...TaskStories.Default.args.task, id: '3', title: 'Task 3' },
    { ...TaskStories.Default.args.task, id: '4', title: 'Task 4' },
    { ...TaskStories.Default.args.task, id: '5', title: 'Task 5' },
    { ...TaskStories.Default.args.task, id: '6', title: 'Task 6' },
  ],
};

export const WithPinnedTasks = Template.bind({});
WithPinnedTasks.args = {
  // Shaping the stories through args composition.
  // Inherited data coming from the Default story.
  tasks: [
    ...Default.args.tasks.slice(0, 5),
    { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
  ],
};

export const Loading = Template.bind({});
Loading.args = {
  tasks: [],
  loading: true,
};

export const Empty = Template.bind({});
Empty.args = {
  // Shaping the stories through args composition.
  // Inherited data coming from the Loading story.
  ...Loading.args,
  loading: false,
};
💡 With this change all of our tests will require an update. Update the imports and re-run the test command with the -u flag to update them. Also don't forget to commit your changes with git!
Keep your code in sync with this chapter. View b29407b on GitHub.
Is this free guide helping you? Tweet to give kudos and help other devs find it.
Next Chapter
Screens
Construct a screen out of components
✍️ Edit on GitHub – PRs welcome!
Docs
Documentation
Add Storybook to your project in less than a minute to build components faster and easier.
reactvueangularweb-components
Tutorial
Tutorials
Learn Storybook with in-depth tutorials that teaches Storybook best practices. Follow along with code samples.
Learn Storybook now
Storybook
The MIT License (MIT). Website design by @domyen and the awesome Storybook community.
StorybookDocsTutorialsReleasesAddonsBlogGet involvedUse casesSupportTeam
Subscribe
Get news, free tutorials, and Storybook tips emailed to you.

Maintained by
Chromatic
Continuous integration by
CircleCI
Hosting by
Netlify