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.
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
By the end of the chapter, you’ll have something similar to figure 2.1.
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:
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.
{ 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', } ] }
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.
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.
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.
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.
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.
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;
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.
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.
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;
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.
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;
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.
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.
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.
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.
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.
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
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.
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.
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.
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 ...
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.
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.
export default function tasks(state = [], action) { 1 return state }
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.
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.
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; }
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!
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.
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:
Pause here to install the package: npm install –P react-redux.
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.
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') );
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.
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.
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.
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.
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);
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.
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:
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.
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.
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.
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;
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.
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.
... render() { console.log('props from App: ', this.props) 1 return ( ... ) } } ...
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:
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> ); } }
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.
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.
Actions and action creators are closely related and work together to dispatch actions to the store, but they fulfill different roles:
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.
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.
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', }, }; }
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.
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', }, ];
To finish the implementation, update the code in App.js to import and use your new action creator, as shown in the following listing.
... import { createTask } from './actions'; 1 class App extends Component { ... onCreateTask = ({ title, description }) => { this.props.dispatch(createTask({ title, description })); 2 } ... } ...
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.
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.
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.
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.
... 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.
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.
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.
... 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 }
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).
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.
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.
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:
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:
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.
Start by adding the status drop-down to the Task component, as shown in the following listing.
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;
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.
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.
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.
... export function editTask(id, params = {}) { 1 return { type: 'EDIT_TASK', payload: { id, params } }; }
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.
... 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> ); } } ...
Next move on to the TasksPage component and pass onStatusChange down to TaskList and finally on to Task, as shown in the following listing.
... return ( <TaskList key={status} status={status} tasks={statusTasks} onStatusChange={this.props.onStatusChange} 1 /> ); ...
To reach the Task component, onStatusChange needs to travel through one more component: TaskList, as shown in the following listing.
... {props.tasks.map(task => { return ( <Task key={task.id} task={task} onStatusChange={props.onStatusChange} 1 /> ); )} ...
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.
... 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) } } ...
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.
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.
... 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; }
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.
18.191.195.236