Back to Intro to Storybook
Chapters
  • Get started
  • Simple component
  • Composite component
  • Data
  • Screens
  • Deploy
  • Testing
  • Addons
  • Conclusion
  • Contribute

Wire in data

Learn how to wire in data to your UI component
This community translation has not been updated to the latest version of Storybook yet. Help us update it by applying the changes in the English guide to this translation. Pull requests are welcome.

So far we created isolated stateless components –great for Storybook, but ultimately not useful 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.

Loading Data

With Ember you can use various ways to load data, but two things that you need to keep in mind:

  • Where it's being loaded
  • How it's being loaded

By default you should use a route and a data persistance layer, such as ember-data or Apollo. For our small example we're going to use tracked-redux to demonstrate how it can be used with a route.

Add the necessary dependency to your project with:

Copy
ember install tracked-redux

First we’ll construct a simple Redux store that responds to actions that change the state of tasks, in a file called redux.js in the app folder (intentionally kept simple):

Copy
app/store.js
import { createStore } from 'tracked-redux';

export const actions = {
  ARCHIVE_TASK: 'ARCHIVE_TASK',
  PIN_TASK: 'PIN_TASK',
};

// The action creators bundle actions with the data required to execute them
export const archiveTask = id => ({ type: actions.ARCHIVE_TASK, id });
export const pinTask = id => ({ type: actions.PIN_TASK, id });

// A sample set of tasks
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' },
];

// Store's initial state
const initialState = {
  isError: false,
  isLoading: false,
  tasks: defaultTasks,
};

// All our reducers simply change the state of a single task.
function taskStateReducer(taskState) {
  return (state, action) => {
    return {
      ...state,
      tasks: state.tasks.map(task =>
        task.id === action.id ? { ...task, state: taskState } : task
      ),
    };
  };
}

// The reducer describes how the contents of the store change for each action
const reducers = (state, action) => {
  switch (action.type) {
    case actions.ARCHIVE_TASK:
      return taskStateReducer('TASK_ARCHIVED')(state, action);
    case actions.PIN_TASK:
      return taskStateReducer('TASK_PINNED')(state, action);
    default:
      return state || initialState;
  }
};

export const store = createStore(reducers);

Using a Route

We have our store set up. We can now declare fields on the objects where required.

For that we're going to use both a route and a controller. The latter will contain the actions we've created earlier so that we can modify our store with ease.

Inside the app directory, create a new one called tasks and inside add a new file called route.js with the following:

Copy
app/tasks/route.js
import Route from '@ember/routing/route';
import { store } from '../store';

export default class TasksRoute extends Route {
  model() {
    // returns the store tracked state
    // whenever the state changes, these will be reflected in the template
    return store
      .getState()
      .tasks.filter(t => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED');
  }
}

Next, we'll need the controller. Inside the tasks folder create another file called controller.js with the following:

Copy
app/tasks/controller.js
import Controller from '@ember/controller';
import { action } from '@ember/object';

import { store, pinTask, archiveTask } from '../store';

export default class TaskController extends Controller {
  @action
  pinTask(task) {
    store.dispatch(pinTask(task));
  }

  @action
  archiveTask(task) {
    store.dispatch(archiveTask(task));
  }
}

And one final file called template.hbs, in which we'll add the presentational <TaskList> component we've created in the previous chapter:

Copy
app/tasks/template.hbs
<TaskList
  @tasks={{@model}}
  @pinTask={{this.pinTask}}
  @archiveTask={{this.archiveTask}}
 />

With this we've accomplished what we've set out to do, we've managed to set up a data persistance layer and also we've managed to keep the components decoupled by adopting some best practices.

Our implementation is rather rudimentary and requires additional work if we decide to update our application. In the next chapter we'll introduce screen components, which will improve how the data is handled in our small application.

💡 Don't forget to commit your changes with git!
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!
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