Chapter 2. Your first Redux application

This chapter covers

  • Configuring a Redux store
  • Connecting Redux to React with the react-redux package
  • Using actions and action creators
  • Using reducers to update state
  • Understanding container and presentational React components

By now, you’re almost certainly itching to get started on a Redux application. You have more than enough context to begin, so let’s scratch that itch. This chapter guides you through the set up and development of a simple task-management application using Redux to manage its state.

By the end of the chapter, you’ll have walked through a complete application, but more importantly, you’ll have learned enough of the fundamentals to leave the nest and create simple Redux applications of your own. Through the introduction of components that were strategically omitted in chapter 1, you’ll develop a better understanding of the unidirectional data flow and how each piece of the puzzle contributes to that flow.

You may wonder if introducing Redux is overkill for the small application you’ll build in this chapter. To iterate a point made in chapter 1, we encourage the use of vanilla React until you experience enough pain points to justify bringing in Redux.

If this chapter were the whole of it, Redux would indeed be overkill. It’s not until you reach features introduced in later chapters that it really begins to make sense. As a matter of practicality, you’ll head straight for Redux; that’s why you’re here, after all! As a thought experiment, you may enjoy rebuilding the application in React to determine when including Redux makes sense for yourself, once you become comfortable with the fundamentals.

2.1. Building a task-management application

The path you’ll walk is a well-trodden one: building a project task-management application. In this chapter, you’ll implement simple functionality, but you’ll add increasingly complex features throughout the rest of the book as we cover each concept in more detail.

This app is lovingly named Parsnip. Why Parsnip? No good reason. It spoke to us in the moment, and we went with it. Specifically, Parsnip will be a Kanban board, a tool that allows users to organize and prioritize work (similar to Trello, Waffle, Asana, and a number of other tools). An app like this is highly interactive and requires complex state management—a perfect vehicle for us to apply Redux skills.

To see Redux in action without the bells and whistles, you’ll start with one resource, a task. Your users should

  • Create a new task with three properties: title, description, and status.
  • See a list of all tasks.
  • Change the status of a task. For example, a task may move from Unstarted, to In Progress, and finally to Completed.

By the end of the chapter, you’ll have something similar to figure 2.1.

Figure 2.1. A mockup of what you’ll build in this chapter.

2.1.1. Designing the state shape

There’s no single right way to approach problems with Redux, but we recommend taking time to think about how the application state should look before implementing a new feature. If React applications are a reflection of the current state, what should your state object look like to satisfy the requirements? What properties should it have? Are arrays or objects more appropriate? These are the kinds of questions you should ask when you approach new features. To recap, you know you need to do the following:

  • Render a list of tasks.
  • Allow users to add items to the list.
  • Allow users to mark tasks as Unstarted, In Progress, or Completed.

What state do you need to track to make all this possible? It turns out that our requirements are straightforward: you need a list of task objects with a title, description, and status. Application state that lives in Redux is a simple JavaScript object. The following listing is an example of what that object might look like.

Listing 2.1. An outline of the Redux store
{
  tasks: [                           1
    {
      id: 1,                         2
      title: 'Learn Redux',
      description: 'The store, actions, and reducers, oh my!',
      status: 'In Progress',
    },
    {
      id: 2,
      title: 'Peace on Earth',
      description: 'No big deal.',
      status: 'Unstarted',
    }
  ]
}

  • 1 The tasks key represents one “slice” of the data that could make up a store.
  • 2 Each task is an object with several properties.

The store is simple, a tasks field with an array of task objects. How you organize the data in your Redux store is completely up to you, and we’ll explore popular patterns and best practices later in the book.

Deciding upfront how the data will look will be a big help down the road in determining what kinds of actions and reducers you might need. Remember, it may be helpful to think of client-side state like a database. Similarly to if you were dealing with a persistent data store such as a SQL database, declaring a data model will help you organize your thoughts and drive out the code you need. Throughout the book, you’ll start each new feature by revisiting this process of defining a desired state shape.

2.2. Using Create React App

React has always enjoyed a reputation for being beginner-friendly. Compared with larger frameworks such as Angular and Ember, its API and feature set are small. The same can’t be said for many of the surrounding tools you’ll find in many production-ready applications. This includes Webpack, Babel, ESLint, and a dozen others with varying learning curves. We developers couldn’t be bothered to do all this configuration for each new project or prototype from scratch, so an abundance of starter kits and boilerplate applications were created. Although popular, many of these starter kits became wildly complex and equally intimidating for beginners to use.

Fortunately, in mid-2016, Facebook released an officially supported tool that does this complex configuration work for you and abstracts most of it away. Create React App is a command line interface (CLI) tool that will generate a relatively simple, production-ready React application. Provided you agree with enough of the choices made within the project, Create React App can easily save days of setup and configuration time. We’re sold on this tool as the preferred way to get new React projects off the ground, so we’ll use it to kick-start this application.

2.2.1. Installing Create React App

Create React App is a module that can be installed using your favorite package manager. In this book, you’ll use npm. In a terminal window, run the following command at the prompt:

npm install --global create-react-app

Once installed, you can create a new project with

create-react-app parsnip

Creating a new application can take a few minutes, depending on the time it takes to install the dependencies on your machine. When it completes, there will be a newly created parsnip directory waiting for you. Navigate to that directory now, and we’ll get up and running.

To view the application, you’ll start the development server, which takes care of serving your JavaScript code to the browser (among other things). Run the following command from within the parsnip directory:

npm start

If create-react-app didn’t open a browser window automatically after starting the development server, open a browser and head to localhost:3000. You should see something similar to figure 2.2.

Figure 2.2. The home page in a browser after bootstrapping a new React application with the create-react-app command

Go ahead and follow the instructions. Try changing the “To get started...” text by editing the src/App.js file. You should see the browser refresh automatically, without having to reload the page. We’ll cover this feature and more development workflow enhancements in-depth in chapter 3.

2.3. Basic React components

Before you jump into configuring Redux, let’s lay groundwork by building a few simple React components. We generally like to approach features “from the outside in,” meaning you’ll start by building the UI first, then hook up any necessary behavior. It helps you stay grounded in what the user will eventually experience, and the earlier you can interact with a working prototype, the better. It’s much better to iron out issues with a design or feature spec early, before too much work gets underway.

You also want to make sure you’re building flexible, reusable UI components. If you define your components with clear interfaces, reusing and rearranging them becomes easy. Start by creating a new directory under src/ called components/, then create files for the new components, Task.js, TaskList.js, and TasksPage.js.

Task and TaskList will be stateless functional components, introduced in React v0.14. They don’t have access to lifecycle methods such as componentDidMount, only accept props, don’t use this.state or this.setState, and they’re defined as plain functions instead of with createReactClass or ES2015 classes.

These kinds of components are wonderfully simple; you don’t have to worry about this, they’re easier to work with and test, and they cut down on the number of lines of code you might need with classes. They accept props as input and return some UI. What more could you ask for? Copy the code in the following listing to Task.js.

Listing 2.2. src/components/Task.js
import React from 'react';

const Task = props => {                       1
  return (
    <div className="task">
      <div className="task-header">
        <div>{props.task.title}</div>         2
      </div>
      <hr />
      <div className="task-body">{props.task.description}</div>
    </div>
  );
   }

export default Task;

  • 1 Stateless functional components are exported anonymous functions.
  • 2 These components receive and display props from parent components.
Note

We aren’t including the contents of CSS files in this book. They’re verbose and don’t aid in the understanding of Redux topics. Please see the supplementary code if you want to replicate the styles found in screenshots.

The implementation for the TaskList component is equally straightforward. The column name and a list of tasks will be passed in from a parent component. Copy the code in the following listing to TaskList.js.

Listing 2.3. src/components/TaskList.js
import React from 'react';
import Task from './Task';

const TaskList = props => {
  return (
    <div className="task-list">
      <div className="task-list-title">
        <strong>{props.status}</strong>
      </div>
      {props.tasks.map(task => (
        <Task key={task.id} task={task} />
      ))}
    </div>
  );
}

export default TaskList;

Redux allows you to implement a significant chunk of our React components as these stateless functional components. Because you get to offload most of the app’s state and logic to Redux, you can avoid the component bloat that’s typical of nearly all large React applications. The Redux community commonly refers to these types of components as presentational components, and we cover them in more detail later in the chapter.

Within TasksPage.js, import the newly created TaskList component and display one for each status (see the following listing). Although it doesn’t yet, this component needs to manage local state when you introduce the new task form. For that reason, it’s implemented as an ES6 class.

Listing 2.4. src/components/TasksPage.js
import React, { Component } from 'react';
import TaskList from './TaskList';

const TASK_STATUSES = ['Unstarted', 'In Progress', 'Completed'];          1

class TasksPage extends Component {                                       2
  renderTaskLists() {
    const { tasks } = this.props;
    return TASK_STATUSES.map(status => {                                  3
      const statusTasks = tasks.filter(task => task.status === status);
      return <TaskList key={status} status={status} tasks={statusTasks} />;
    });
  }

  render() {
    return (
      <div className="tasks">
        <div className="task-lists">
          {this.renderTaskLists()}
        </div>
      </div>
    );
  }
}

export default TasksPage;

  • 1 Tasks can have one of three states.
  • 2 ES6 classes are used when local state must be managed.
  • 3 Display one column per status, with corresponding tasks.

To start, TasksPage will receive mock tasks from the top-level component, App. App will also be created using an ES6 class, because it will eventually connect to the Redux store, as shown in the following listing.

Listing 2.5. src/App.js
import React, { Component } from 'react';
import TasksPage from './components/TasksPage';

const mockTasks = [                               1
  {
    id: 1,
    title: 'Learn Redux',
    description: 'The store, actions, and reducers, oh my!',
    status: 'In Progress',
  },
  {
    id: 2,
    title: 'Peace on Earth',
    description: 'No big deal.',
    status: 'In Progress',
  },
];

class App extends Component {
  render() {
    return (
      <div className="main-content">
        <TasksPage tasks={mockTasks} />
      </div>
    );
  }
}

export default App;

  • 1 Until Redux is introduced, mock tasks will populate the UI.

At this point, you can run your small React application with npm start and view it in the browser. Bear in mind that it’ll look dreadfully boring until you circle back to apply styles. Again, you can borrow ours from the supplemental code if you like.

2.4. Revisiting the Redux architecture

Your small React application is now ready to be introduced to Redux. Before you dive straight in, let’s consider the full arc of what will be required by revisiting the Redux architecture, introduced in chapter 1. See figure 2.3.

Figure 2.3. The Redux architecture

The store is a logical starting point for introducing Redux into an application. The Redux package exposes a few methods that facilitate the creation of a store. Once a store is created, you’ll connect it to the React application using the react-redux package, enabling a view (component) to dispatch actions. Actions eventually return to the store, to be read by reducers, which determine the next state of the store.

2.5. Configuring the Redux store

The main hub of functionality in Redux is the store—the object responsible for managing application state. Let’s look at the store and its API in an isolated context. As an example, we’ll look at a tiny program to increment a number.

2.5.1. The big picture and the store API

In reading about Redux and talking with other community members, you’ll see or hear references to the store, the Redux store, or the state tree often used interchangeably. Generally, what these terms refer to is a JavaScript object like any other. Let’s look at the API that Redux provides to interact with the store.

The Redux package exports a createStore function that, you guessed it, is used to create a Redux store. Specifically, the Redux store is an object with a few core methods that can read and update state and respond to any changes: getState, dispatch, and subscribe. You’ll capture all three in the quick example in the following listing.

Listing 2.6. The store API in action
import { createStore } from 'redux';

function counterReducer(state = 0, action) {             1
  if (action.type === 'INCREMENT') {
    return state + 1;
  }
  return state;
}

const store = createStore(counterReducer);               2

console.log(store.getState());                           3

store.subscribe(() => {                                  4
  console.log('current state: ', store.getState());
});

store.dispatch({ type: 'INCREMENT' });                   5

  • 1 The store requires at least one reducer function (counterReducer).
  • 2 Creates a store with the reducer
  • 3 Reads the current state of the store
  • 4 Does something after the store has updated
  • 5 Sends a new action to the reducers to update the store

The first argument passed to the createStore function is a reducer. Recall from chapter 1 that reducers are functions that inform the store how it should update state in response to actions. The store requires at least one reducer.

As promised, there are three methods on the store to show off. The first, getState, can read the contents of the store. You’ll need to call this method yourself infrequently.

subscribe allows us to respond to changes in the store. For the sake of this example, you’re logging out the newly updated state to the console. When you start connecting Redux to React, this method is used under the hood to allow React components to re-render when any state changes in the store.

Because you can’t mutate the store yourself and only actions can result in a new state, you need a way to send new actions on to the reducers. That method is dispatch.

2.5.2. Creating a Redux store

Back to business! In this section, you’ll begin to create your store and its dependencies. A store contains one or more reducers and, optionally, middleware. We’ll save middleware for a subsequent chapter, but at least one reducer is required to create a store.

Figure 2.4. A store requires one or more reducers and may include middleware. The arrow between middleware and reducers indicates the order in which actions will eventually be handled.

Let’s begin by adding Redux as a dependency of the project, then move your initial tasks data into Redux. Make sure you’re in the parsnip directory and install the package by running the following command in a terminal window:

npm install -P redux

The –P flag is an alias for --save-prod, resulting in the package being added to your dependencies in the package.json file. Starting in npm5, this is the default install behavior. Now that Redux has been added, the next step is to integrate it into your existing React components. First create the store by adding the code shown in the following listing to index.js.

Listing 2.7. src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App';
import { createStore } from 'redux'           1
import tasks from './reducers'                2
import './index.css';

const store = createStore(tasks)              3
...

  • 1 Imports the createStore function from the redux package
  • 2 Because you need at least one reducer to create a Redux store, import the tasks reducer, which you’ll define in listing 2.8.
  • 3 Creates the store by passing the reducer to createStore

The next step is to make the store available to the React components in the app, but the code you added in listing 2.7 isn’t functional yet. Before going any further in index.js, you need to provide a barebones implementation of the tasks reducer.

2.5.3. The tasks reducer

As you’ve learned, creating a new Redux store requires a reducer. The goal of this section is to get enough done to create a new store, and you’ll fill out the rest of the functionality as you move through the chapter.

If you recall from chapter 1, a reducer is a function that takes the current state of the store and an action and returns the new state after applying any updates. The store is responsible for storing state, but it relies on reducers that you’ll create to determine how to update that state in response to an action.

You won’t handle any actions yet; you’ll return the state without modifications. Within the src directory, create a new directory, reducers, with an index.js file. In this file, you’ll create and export a single function, tasks, that returns the given state, as shown in the following listing.

Listing 2.8. src/reducers/index.js
export default function tasks(state = [], action) {     1
  return state
}

  • 1 Currently the action argument isn’t being used, but you’ll add more functionality to this reducer function once you start dispatching actions.

That’s it! Do a little dance, because you’ve written your first reducer. You’ll be back later to make this function more interesting.

2.5.4. Default reducer state

It’s common to provide reducers with an initial state, which involves nothing more than providing a default value for the state argument in the tasks reducer. Before you get back to connecting the Redux store to your application, let’s move the list of mock tasks out of App.js and into src/reducers/index.js, a more appropriate place for initial state to live. This is shown in the following listing.

Listing 2.9. src/reducers/index.js
const mockTasks = [
  {
    id: 1,
    title: 'Learn Redux',
    description: 'The store, actions, and reducers, oh my!',
    status: 'In Progress',
  },
  {
    id: 2,
    title: 'Peace on Earth',
    description: 'No big deal.',
    status: 'In Progress',
  },
];

export default function tasks(state = { tasks: mockTasks }, action) {    1
  return state;
}

  • 1 Sets the mock tasks as the default state

Don’t worry if your App component is breaking as a result of removing the mock data. You’ll fix that shortly. At this point the store has the correct initial data, but you still need to somehow make this data available to the UI. Enter react-redux!

A note on immutability

Though it’s not a strict requirement, it’s highly encouraged to keep your data immutable; that is, not mutating values directly. Immutability has inherent benefits like being easy to work with and test, but in the case of Redux, the real benefit is that it enables extremely fast and simple equality checks.

For example, if you mutate an object in a reducer, React-Redux’s connect may fail to correctly update its corresponding component. When connect compares old and new states to decide whether it needs to go ahead with a re-render, it checks only if two objects are equal, not that every individual property is equal. Immutability is also great for dealing with historical data, and it’s required for advanced Redux debugging features such as time travel.

The long and short of it is to never mutate data in place with Redux. Your reducers should always accept the current state as input and calculate an entirely new state. JavaScript doesn’t offer immutable data structures out of the box, but there are several great libraries. ImmutableJS (https://facebook.github.io/immutable-js/) and Updeep (https://github.com/substantial/updeep) are two popular examples, and in addition to enforcing immutability, they also provide more advanced APIs for updating deeply nested objects. If you want something more lightweight, Seamless-Immutable (https://github.com/rtfeldman/seamless-immutable) gives you immutable data structures, but allows you to continue using standard JavaScript APIs.

2.6. Connecting Redux and React with react-redux

As we discussed in chapter 1, Redux was built with React in mind, but they’re two totally discrete packages. To connect Redux with React, you’ll use the React bindings from the react-redux package. Redux provides only the means to configure a store. react-redux bridges the gap between React and Redux by providing the ability to enhance a component, allowing it to read state from the store or dispatch actions. react-redux gives you two primary tools for connecting your Redux store to React:

  • Provider—A React component that you’ll render at the top of the React app. Any components rendered as children of Provider can be granted access to the Redux store.
  • connect—A function used as a bridge between React components and data from the Redux store.

Pause here to install the package: npm install –P react-redux.

2.6.1. Adding the Provider component

Provider is a component that takes the store as a prop and wraps the top-level component in your app—in this case, App. Any child component rendered within Provider can access the Redux store, no matter how deeply it’s nested.

In index.js, import the Provider component and wrap the App component, using the code in the following listing.

Listing 2.10. src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';    1
import tasks from './reducers';
import App from './App';
import './index.css';

const store = createStore(tasks);

ReactDOM.render(
  <Provider store={store}>                 2
    <App />
  </Provider>,
  document.getElementById('root')
);

  • 1 Imports the Provider component
  • 2 Provider is now our most top-level React component. It works in conjunction with connect to make the store available to any child component.

Think of the Provider component as an enabler. You won’t interact with it directly often, typically only in a file such as index.js, which takes care of initially mounting the app to the DOM. Behind the scenes, Provider ensures you can use connect to pass data from the store to one or more React components.

2.6.2. Passing data from Redux to React components

You’ve laid the groundwork to pass data from the store into a React component. You have a Redux store with a tasks reducer, and you’ve used the Provider component from react-redux to make the store available to our React components. Now it’s nearly time to enhance a React component with connect. See figure 2.5.

Figure 2.5. The connect method bridges the gap between the store and views (components).

Generally, you can break visual interfaces into two major concerns: data and UI. In your case, the data is the JavaScript objects that represent tasks, and the UI is the few React components that take these objects and render them on the page. Without Redux, you’d deal with both concerns directly within React components.

As you can see in figure 2.6, the data used to render your UI is moved entirely out of React and into Redux. The App component will be considered an entry point for data from Redux. As the application grows, you’ll introduce more data, more UI, and as a result, more entry points. This kind of flexibility is one of Redux’s greatest strengths. Your application state lives in one place, and you can pick and choose how you want that data to flow into the application.

Figure 2.6. A visualization of how React and Redux work together

Listing 2.11 introduces a couple of new concepts: connect and mapStateToProps. By adding connect to the App component, you declare it as an entry point for data from the Redux store. You’ve only connected one component here, but as your application grows, you’ll start to discover best practices for when to use connect with additional components.

Listing 2.11 passes connect a single argument, the mapStateToProps function. Note that the name mapStateToProps is a convention, not a requirement. The name stuck for a reason: because it’s an effective descriptor of the role of this function. State refers to the data in the store, and props are what get passed to the connected component. Whatever you return from mapStateToProps will be passed to your component as props.

Listing 2.11. src/App.js: connecting components
import React, { Component } from 'react';
import { connect } from 'react-redux';               1
import TasksPage from './components/TasksPage';

class App extends Component {
  render() {
    return (
      <div className="main-content">
        <TasksPage tasks={this.props.tasks} />       2
      </div>
    );
  }
}

function mapStateToProps(state) {                    3
  return {
    tasks: state.tasks                               4
  }
}

export default connect(mapStateToProps)(App);

  • 1 Adds connect to the list of imports
  • 2 Tasks will be available via props after connected to the store.
  • 3 The state argument is the entire contents of the Redux store, specifically the result of calling getState on the store instance.
  • 4 The return value of mapStateToProps is passed into the App component as props, which is why render can reference this.props.tasks.

Now the application successfully renders data from the Redux store! Notice how you didn’t have to update the TasksPage component? That’s by design. Because TasksPage accepts its data via props, it doesn’t care what the source of those props is. They could come from Redux, from React’s local state, or from another data library altogether.

2.6.3. Container and presentational components

Recall that TaskList is a presentational or UI component. It accepts data as props and returns output according to the markup you defined. By using connect in the App component, you secretly introduced their counterparts, known as container components.

Presentational components don’t have dependencies on Redux. They don’t know or care that you’re using Redux to manage your application state. By using presentational components, you introduced determinism into your view renders. Given the same data, you’ll always have the same rendered output. Presentational components are easily tested and provide your application with sweet, sweet predictability.

Presentational components are great, but something needs to know how to get data out of the Redux store and pass it to your presentational components. This is where container components, such as App, come in. In this simple example, they have a few responsibilities:

  • Get data from the Redux store via connect.
  • Use mapStateToProps to pass only relevant data to the component being connected.
  • Render presentational components.

Again, separating things into container and presentational components is a convention, not a hard-and-fast rule that React or Redux enforces. But it’s one of the most popular and pervasive patterns for a reason. It allows you to decouple how your app looks from what it does. Defining your UI as presentational components means you have simple, flexible building blocks that are easy to reconfigure and reuse. When you’re working with data from Redux, you can deal with container components without having to worry about markup. The inverse applies for when you’re working with a UI.

At this point, you can view the data being rendered in the browser; your app renders a simple list of tasks retrieved from the Redux store. Now it’s time to wire up behavior! Let’s see what it takes to add a new task to the list.

2.7. Dispatching actions

You’ll follow the same workflow that you used to render the static list of tasks. You’ll start with the UI, then implement functionality. Let’s start with a “New task” button and a form. When a user clicks the button, the form renders with two fields, a title, and a description. Eventually, it’ll look roughly like figure 2.7.

Figure 2.7. The New Task form

Modify the code in TasksPage.js to match the following listing. This code is still plain React, so much of it may be familiar to you.

Listing 2.12. Adding the New Task form
import React, { Component } from 'react';
import TaskList from './TaskList';

class TasksPage extends Component {
  constructor(props) {
    super(props);
    this.state = {                                     1
      showNewCardForm: false,
      title: '',
      description: '',
    };
  }

  onTitleChange = (e) => {                             2
    this.setState({ title: e.target.value });
  }

  onDescriptionChange = (e) => {
    this.setState({ description: e.target.value });
  }

  resetForm() {
    this.setState({
      showNewCardForm: false,
      title: '',
      description: '',
    });
  }

  onCreateTask = (e) => {
    e.preventDefault();
    this.props.onCreateTask({                          3
      title: this.state.title,
      description: this.state.description,
    });
    this.resetForm();                                  4
  }

  toggleForm = () => {
    this.setState({ showNewCardForm: !this.state.showNewCardForm });
  }

  renderTaskLists() {
    const { tasks } = this.props;
    return TASK_STATUSES.map(status => {
      const statusTasks = tasks.filter(task => task.status === status);
      return (
        <TaskList
          key={status}
          status={status}
          tasks={statusTasks}
        />
      );
    });
  }

  render() {
    return (
      <div className="task-list">
        <div className="task-list-header">
          <button
            className="button button-default"
            onClick={this.toggleForm}
          >
            + New task
          </button>
        </div>
        {this.state.showNewCardForm && (
          <form className="task-list-form" onSubmit={this.onCreateTask}>
            <input
              className="full-width-input"
              onChange={this.onTitleChange}
              value={this.state.title}
              type="text"
              placeholder="title"
            />
            <input
              className="full-width-input"
              onChange={this.onDescriptionChange}
              value={this.state.description}
              type="text"
              placeholder="description"
            />
            <button
              className="button"
              type="submit"
            >
              Save
            </button>
          </form>
        )}

        <div className="task-lists">
          {this.renderTaskLists()}
        </div>
      </div>
    );
  }
}

export default TasksPage;

  • 1 It’s often simpler to use React and setState for UI-related state, such as whether the form is open and for the current values of the form inputs.
  • 2 A special syntax ensures the value of this will be correct.
  • 3 Submitting the form is as simple as firing the onCreateTask prop.
  • 4 Resets the form’s state after submission

Your TaskList component now tracks local state—whether the form is visible and the text values in the form. The form inputs are what’s known in React as controlled components. All that means is the values of the input fields are set to the corresponding local state values, and for each character typed into the input field, local state is updated. When a user submits the form to create a new task, you call the onCreateTask prop to indicate an event has taken place. Because you call onCreateTask from this.props, you know that this function needs to be passed down from the parent component, App.

Pop quiz

What’s the only way to initiate a change in the Redux store? (No peeking at this section title.) Dispatching an action is exactly right. You have a good idea, then, of how to implement the onCreateTask function; we need to dispatch an action to add a new task.

In App.js, you know that App is a connected component and is enhanced with the ability to interact with the Redux store. Do you remember which of the store APIs can be used to send off a new action? Take a moment and log the value of this.props in the render method of App, as shown in the following listing. The resulting console output should match that of figure 2.8.

Listing 2.13. Logging this.props in src/App.js
...
render() {
   console.log('props from App: ', this.props)          1
   return (
     ...
   )
 }
}
...

  • 1 Logs the value of this.props at the top of the render method
Figure 2.8. Shows the console output from logging the props available to the App component

There it is: a dispatch prop in addition to your expected tasks array. What’s dispatch? You know that the store is extremely protective of its data. It only provides one way to update state—dispatching an action. dispatch is part of the store’s API, and connect conveniently provides this function to your component as a prop. Let’s create a handler where you dispatch a CREATE_TASK action (see listing 2.14). The action will have two properties:

  • type—A string that represents the category of action being performed. By convention, they’re capitalized and use underscores as delimiters. This is the only required property for an action to be considered valid.
  • payload—An object that provides the data necessary to perform the action. Having a payload field is optional and can be omitted if no additional data is required to perform the action. For example, an action to log a user out may contain a type of LOGOUT with no additional data requirements. If additional data is required, however, any keys may be passed in the action. The name payload isn’t required by Redux, but it’s a popular organizational convention that we’ll stick with throughout the book. The pattern is commonly referred to as Flux Standard Actions (FSA); more details can be found in this GitHub repository at https://github.com/acdlite/flux-standard-action.
Listing 2.14. src/App.js: adding an action handler
import React, { Component } from 'react';
import { connect } from 'react-redux';
import TasksPage from './components/TasksPage';

class App extends Component {
  onCreateTask = ({ title, description }) => {
    this.props.dispatch({                           1
      type: 'CREATE_TASK',
      payload: {
        title,
        description
      }
    });
  }

  render() {
    return (
      <div className="main-content">
        <TasksPage
          tasks={this.props.tasks}
          onCreateTask={this.onCreateTask}          2
        />
      </div>
    );
  }
}

  • 1 this.props.dispatch, injected by connect, dispatches an action to the store.
  • 2 The onCreateTask handler is passed to TasksPage as a simple callback prop.

This listing also illustrates one of the other main roles of container components: action handling. You don’t want TasksPage to worry about the details of creating a new task; it only needs to indicate that the user wishes to do so by firing the onCreateTask prop.

2.8. Action creators

You dispatched the CREATE_TASK action object directly in the previous example, but it’s not something you usually do outside of simple examples. Instead, you’ll invoke action creators—functions that return actions. Figure 2.9 illustrates this relationship.

Figure 2.9. Although views can dispatch actions, they often invoke action creators instead—functions that return actions.

Actions and action creators are closely related and work together to dispatch actions to the store, but they fulfill different roles:

  • ActionsObjects that describe an event
  • Action creatorsFunctions that return actions

Why use action creators? Action creators have a friendlier interface; all you need to know is which arguments the action creator function expects. You won’t have to worry about specifics, such as the shape of the action’s payload or any logic that might need to be applied before the action can be dispatched. By the same token, an action creator’s arguments are helpful because they clearly document an action’s data requirements.

Later in the book, you’ll implement a good chunk of your application’s core logic directly within action creators. They’ll do tasks such as make AJAX requests, perform redirects, and create in-app notifications.

2.8.1. Using action creators

From the last section, you know dispatch accepts an action object as an argument. Instead of dispatching the action directly, you’ll use an action creator. Within the src directory, create a new directory called actions with an index.js file within it. This file is where your action creators and actions will live. Add the code in the following listing to that newly created file.

Listing 2.15. src/actions/index.js: the createTask action creator
let _id = 1;
export function uniqueId() {                              1
  return _id++;
}

export function createTask({ title, description }) {      2
  return {
    type: 'CREATE_TASK',
    payload: {                                            3
      id: uniqueId(),
      title,
      description,
      status: 'Unstarted',
    },
  };
}

  • 1 uniqueId is a utility function to generate numeric ids for tasks. When you hook up the app to a real server in chapter 4, this will no longer be necessary.
  • 2 The function signature makes it clear that a title and a description are required to dispatch the CREATE_TASK action.
  • 3 The payload property contains all the data necessary to perform the action.

There’s one piece of cleanup you need to do after adding the uniqueId function. Update src/reducers/index.js to use uniqueId instead of hard-coded IDs, as shown in the following listing. This ensures your task IDs will increment correctly as you create them, and you’ll use these IDs when you allow users to edit tasks later in the chapter.

Listing 2.16. src/reducers/index.js
import { uniqueId } from '../actions';                          1

const mockTasks = [
  {
    id: uniqueId(),                                             2
    title: 'Learn Redux',
    description: 'The store, actions, and reducers, oh my!',
    status: 'In Progress',
  },
  {
    id: uniqueId(),
    title: 'Peace on Earth',
    description: 'No big deal.',
    status: 'In Progress',
  },
];

  • 1 Imports the uniqueId function you created in src/actions/index.js
  • 2 Uses uniqueId instead of hard-coded IDs

To finish the implementation, update the code in App.js to import and use your new action creator, as shown in the following listing.

Listing 2.17. src/App.js
...
import { createTask } from './actions';                        1

class App extends Component {
 ...
 onCreateTask = ({ title, description }) => {
   this.props.dispatch(createTask({ title, description }));    2
 }
 ...
}
...

  • 1 Imports the action creator
  • 2 Instead of passing an action object to this.props.dispatch, you’ll pass the action creator.

To recap, the App container component has access to the dispatch method, thanks to connect. App imports an action creator, createTask, and passes it a title and a description. The action creator formats and returns an action. In the next section, you’ll follow that action through to the reducer and beyond.

Remember that uniqueId function? How you generate the id field is particularly noteworthy, because it introduces a side effect.

Definition

A side effect is any code that has a noticeable effect on the outside world, such as writing to disk or mutating data. Put another way, it’s code that does anything but take inputs and return a result.

Functions with side effects do something other than return a value. createTask mutates some external state—the ID that you increment whenever you create a new task.

2.8.2. Action creators and side effects

Most of the code you’ve written so far has been deterministic, meaning it produces no side effects. This is all well and good, but you need to deal with side effects somewhere. Code that operates on data is easy to work with and think about, but side effects are necessary to do anything useful. Eventually you’ll need to do things like write to the browser’s local storage and communicate with a web server. Both are considered side effects and are ubiquitous in the world of web applications.

You know you can’t do much without side effects. What you can do is isolate them by enforcing good practices around where they can be performed. Reducers must be pure functions, so they’re out. You guessed it, that leaves action creators! The command createTask is non-deterministic, and that’s perfectly okay. Chapters 4, 5, and 6 will explore various strategies for managing side effects.

2.9. Handling actions with reducers

You defined a simple tasks reducer when you used createStore to initialize your Redux store, but at this point it returns the current state, as shown in the following listing.

Listing 2.18. src/reducers/index.js
...
export default function tasks(state = { tasks: mockTasks }, action) {
  return state;
}

This reducer is completely valid and functional, but it doesn’t do anything particularly useful. The real point of reducers is to handle actions. Reducers are functions that accept the store’s current state and an action and return the next state after applying any relevant updates. You’re still missing that last bit: you need to change our state.

The store’s role is to manage application state; it’s where the data lives, it controls access, and it allows components to listen for updates. What it doesn’t, and can’t, do is define how exactly its state should change in response to actions. That’s up to you to define, and reducers are the mechanism Redux provides to accomplish this.

2.9.1. Responding to actions in reducers

You’re correctly dispatching the CREATE_TASK action, indicating an event has occurred. But the action doesn’t specify how to handle this event. How should state update in response to the action? You stored your task objects in an array, so all you need to do is push an element on to the list. Reducers check the action’s type to determine if it should respond to it. This amounts to a simple conditional statement that describes how the state should update for a given action type. Figure 2.10 illustrates how the reducer responds to actions.

Figure 2.10. A look at your reducer in action. It takes two arguments, the current state of the store and the CREATE_TASK action, and returns the next state.

In this case, if the reducer receives an action of type CREATE_TASK, you expect the next state tree to have one more task in the list but be otherwise identical to the previous state. An action of any other type will result in an unchanged Redux store, because CREATE_TASK is all you’re listening for so far.

Update the tasks reducer to handle the CREATE_TASK action, as shown in the following listing.

Listing 2.19. src/reducers/index.js
...
export default function tasks(state = { tasks: mockTasks }, action) {
  if (action.type === 'CREATE_TASK') {                                 1
    return { tasks: state.tasks.concat(action.payload) };              2
  }

  return state;                                                        3
}

  • 1 Checks whether the action type is one that you care about
  • 2 If the action is CREATE_TASK, add the task to the array and return the result.
  • 3 Always fall back to returning the given state in case a reducer receives an action it can’t handle.

Now the tasks reducer updates state in response to an action. As you continue to add functionality and dispatch new actions, you’ll add more code like this that checks for a specific action type and conditionally applies any updates to application state.

At this point, you’ve completed an entire cycle within Redux’s unidirectional data flow! Once the store updates, your connected component, App, becomes aware of the new state and performs a new render. Let’s review the architecture diagram one last time to help it all sink in (figure 2.11).

Figure 2.11. The Redux architecture

You started by creating a store, passing in the tasks reducer as an argument. After being connected to the store, the views rendered the default state specified by the tasks reducer. When a user wants to create a new task, the connected component dispatches an action creator. That action creator returns an action containing a CREATE_TASK type and additional data. Finally, the reducer listens for the CREATE_TASK action type and determines what the next application state should look like.

2.10. Exercise

More than anything, we want to help you develop the intuition that will help you solve unique problems on your own. You now know about the store, actions, reducers, and what roles they play. Using what you’ve learned from implementing task creation, try making Parsnip even more awesome by allowing users to update the status of each task.

Tasks have a status field, which can be one of three values: Unstarted, In Progress, and Completed. If you open the browser to localhost:3000, you’ll see the UI already displays the status of each task, but users can now open a drop-down and choose a new status. See figure 2.12 for an example of the status selection UI.

Figure 2.12. An example status drop-down

Try your hand first at an implementation; then we’ll walk through how you can approach it. If the task seems daunting, try breaking the problem down until you have manageable, actionable steps. Before getting into any code, keep a few questions in mind:

  • What am I allowing users to do? What UI do I need to build to allow them to access these features?
  • Based on my requirements, what state might I need to track to fulfill them?
  • When and how does that state change?

2.11. Solution

As always, let’s start with a high-level description of what you want to accomplish, then work piece by piece toward an implementation. Your goal is to allow users to update a task’s status by selecting either Unstarted, In Progress, or Completed from a select input. Let’s break down the problem into manageable chunks:

  • Add a select input with the three available statuses. Tasks already have a status field, and you can declare the possible states as a constant.
  • When the user chooses a new status, dispatch an EDIT_TASK action with two pieces of data: the id of the task being updated and the desired status.
  • The tasks reducer should handle EDIT_TASK, update the status of the correct task, and return the updated state tree.
  • The view should re-render with the newly updated status.

Have you noticed how you tend to implement features in a particular order? It lines up nicely with the idea of a unidirectional data flow, one of the fundamental ideas in React and Redux. A user interaction triggers an action, you handle the action, and close the loop by re-rendering the view with any updated state.

2.11.1. The status drop-down

Start by adding the status drop-down to the Task component, as shown in the following listing.

Listing 2.20. src/components/Task.js
import React from 'react'

const TASK_STATUSES = [                             1
  'Unstarted',
  'In Progress',
  'Completed'
]

const Task = props => {
  return (
    <div className="task">
      <div className="task-header">
        <div>{props.task.title}</div>
        <select value={props.task.status}>          2
          {TASK_STATUSES.map(status => (
            <option key={status} value={status}>{status}</option>
          ))}
        </select>
      </div>
      <hr />
      <div className="task-body">{props.task.description}</div>
    </div>
  )
}

export default Task;

  • 1 Defines the list of possible statuses as a variable for clarity convenience
  • 2 Adds the status drop-down using the select and option elements

Now the user can interact with a drop-down that renders the correct values, but the task won’t be updated when an option is selected.

Tip

For the sake of simplicity, you defined TASK_STATUSES directly in the Task component, but it’s a common convention to define constants such as these in a separate file.

2.11.2. Dispatching an edit action

To indicate an event has occurred in your application—the user selecting a new status for a task—you’ll dispatch an action. You’ll create and export an action creator that builds the EDIT_TASK action. This is where you’ll determine the arguments to the action creator (editTask), and the shape of the action payload, as shown in the following listing.

Listing 2.21. src/actions/index.js
...
export function editTask(id, params = {}) {         1
  return {
    type: 'EDIT_TASK',
    payload: {
      id,
      params
    }
  };
}

  • 1 By using an action creator, you can clearly communicate that the EDIT_TASK requires two arguments: the ID of which task to edit, and a params object with any fields being updated.

Next import editTask in App, your container component, add any necessary action handling, and pass down an onStatusChange prop to be fired eventually by the Task component, as shown in the following listing.

Listing 2.22. src/App.js
...
import { createTask, editTask } from './actions';        1

class App extends Component {
 ...
 onStatusChange = (id, status) => {
   this.props.dispatch(editTask(id, { status }));        2
 }

 render() {
   return (
     <div className="main-content">
       <TasksPage
         tasks={this.props.tasks}
         onCreateTask={this.onCreateTask}
         onStatusChange={this.onStatusChange}            3
       />
     </div>
   );
 }
}
...

  • 1 Imports the new action creator
  • 2 Creates the onStatusChange handler, which dispatches the editTask action creator
  • 3 Passes onStatusChange down to TaskList

Next move on to the TasksPage component and pass onStatusChange down to TaskList and finally on to Task, as shown in the following listing.

Listing 2.23. src/components/TasksPage.js
   ...
return (
  <TaskList
    key={status}
    status={status}
    tasks={statusTasks}
    onStatusChange={this.props.onStatusChange}           1
  />
);
...

  • 1 Task is ultimately what calls this.props.onStatusChange with the correct arguments, so TaskList only needs to forward this prop along.

To reach the Task component, onStatusChange needs to travel through one more component: TaskList, as shown in the following listing.

Listing 2.24. src/components/TaskList.js
   ...
{props.tasks.map(task => {
  return (
    <Task
      key={task.id}
      task={task}
      onStatusChange={props.onStatusChange}          1
    />
  );
)}
...

  • 1 onStatusChange needs to be passed once more as a prop to reach Task.

Finally, in the Task component we can fire the props.onStatusChange callback when the value of the status drop-down changes, as shown in the following listing.

Listing 2.25. src/components/Task.js
...
const Task = props => {
  return (
    <div className="task">
      <div className="task-header">
        <div>{props.task.title}</div>
        <select value={props.task.status} onChange={onStatusChange}>     1
          {TASK_STATUSES.map(status => (
            <option key={status} value={status}>{status}</option>
          ))}
        </select>
      </div>
      <hr />
      <div className="task-body">{props.task.description}</div>
    </div>
  );

  function onStatusChange(e) {                                           2
    props.onStatusChange(props.task.id, e.target.value)
 }
}
...

  • 1 Adds a callback to run when the drop-down’s change event fires
  • 2 Calls onStatusChange with the ID of the updated task and the value of the new status

The only thing missing at this point is update logic. An action is dispatched that describes an intent to edit a task, but the task itself still needs to be updated by a reducer.

2.11.3. Handling the action in a reducer

The last step is to specify how the task should be updated in response to the EDIT_TASK action being dispatched. Update the tasks reducer to check for the newly created EDIT_TASK action and update the correct task, as shown in the following listing.

Listing 2.26. src/reducers/index.js
...
export function tasks(state = initialState, action) {
  ...
  if (action.type === 'EDIT_TASK') {                             1
    const { payload } = action;
    return {
      tasks: state.tasks.map(task => {                           2
        if (task.id === payload.id) {
          return Object.assign({}, task, payload.params);        3
        }

        return task;
      })
    }
  }

  return state;
}

  • 1 Checks whether the action passed in has a type that you want to handle
  • 2 Because the list of tasks is stored as an array, to update the right task iterate over the list of tasks with map, and if the current task matches the ID from the payload, update it with the new params.
  • 3 Uses Object.assign to update the task object by returning a new copy, not modifying the original object

First, you check whether the action being passed in is of type EDIT_TASK. If so, you iterate over the list of tasks, updating the relevant task and returning the remaining tasks without modification.

That completes the feature! Once the store updates, the connected components will perform another render and the cycle is ready to begin again.

You implemented a couple of relatively straightforward features, but in the process, you saw most of the core elements of Redux in action. It can be overwhelming, but it’s not critical (or even feasible) that you leave chapter 2 with an ironclad understanding of every new concept we’ve introduced. We’ll cover many of these individual ideas and techniques in greater depth later in the book.

Summary

  • Container components receive data from Redux and dispatch action creators, and presentational components accept data as props and handle markup.
  • Actions are objects describing an event. Action creators are functions that return actions.
  • Reducers are pure functions that update state in response to actions.
  • Side effects can be handled in action creators. Reducers, however, should be pure functions, meaning they don’t perform any mutations and always return the same value given the same inputs.
  • A configured Redux store can be made available to your app using react-redux and the Provider component.
  • The commands connect and mapStateToProps pass data from Redux into a React component as props.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.195.236