Chapter 4. Consuming an API

This chapter covers

  • Using asynchronous actions
  • Handling errors with Redux
  • Rendering loading states

If you’ve kept up so far with the book, or completed a few of the more basic tutorials online, you know that we’ve reached a point where things traditionally start to get a little trickier. Here’s where you are: a user interacts with the app and actions get dispatched to reflect certain events, such as creating a new task or editing an existing task. The data lives directly in the browser, which means you lose any progress if the user refreshes the page.

Perhaps without realizing, every action you’ve dispatched so far has been synchronous. When an action is dispatched, the store receives it immediately. These kinds of actions are straightforward, and synchronous code is generally easier to work with and think about. You know exactly the order things will execute based on where the code is defined. You execute line one and then line two, and so on. But you’d be hard-pressed to find any real-world JavaScript application that didn’t involve any asynchronous code. It’s fundamental to the language and required to do what this chapter is all about: talking with a server.

4.1. Asynchronous actions

To repeat a few fundamental Redux ideas

  • Actions are objects that describe an event, such as CREATE_TASK.
  • Actions must be dispatched to apply any updates.

Every action you dispatched in chapters 2 and 3 was synchronous, but, as it turns out, you often need a server to do anything useful. Specifically, you need to make AJAX requests perform actions such as fetch tasks when the app starts and save new tasks to a server, so they’ll persist between page refreshes. Virtually every real-world application is backed by a server, so clear patterns for interacting with an API are crucial for an application’s long-term health.

Figure 4.1 recaps how your dispatches have looked so far. You dispatch an action from the view, and the action is received immediately by the store. All this code is synchronous, meaning each operation will run only after the previous operation has completed.

Figure 4.1. A synchronous action being dispatched

By contrast, asynchronous, or async, actions are where you can add asynchronous code such as AJAX requests. When dispatching synchronous actions, you don’t have any room for extra functionality. Async actions provide a way to handle asynchronous operations and dispatch synchronous actions with the results when they become available. Async actions typically combine the following into one convenient package, which you can dispatch directly within your app:

  • One or more side effects, such as an AJAX request
  • One or more synchronous dispatches, such as dispatching an action after an AJAX request has resolved

Say you want to fetch a list of tasks from the server to render on the page when the app loads for the first time. You need to initiate the request, wait for the server to respond, and dispatch an action with any results. Figure 4.2 uses a fetchTasks async action, which you’ll implement later in the chapter. Notice the delay between the initial async action being dispatched and when the store finally receives an action to process.

Figure 4.2. An example asynchronous action dispatch

Much of the confusion around async actions boils down to terminology. In this chapter (and throughout the book), we’ll try to be specific about the terms we use, to avoid overloading certain language. Here are the fundamental concepts along with examples:

  • ActionAn object that describes an event. The phrase synchronous action always refers to these action objects. If a synchronous action is dispatched, it’s received by the store immediately. Actions have a required type property and can optionally have additional fields that store data needed to handle the action:
    {
      type: 'FETCH_TASKS_SUCCEEDED',
      payload: {
        tasks: [...]
      }
    }
  • Action creatorA function that returns an action.
  • Synchronous action creatorAll action creators that return an action are considered synchronous action creators:
    function fetchTasksSucceeded(tasks) {
      return {
        type: 'FETCH_TASKS_SUCCEEDED',
        payload: {
          tasks: [...]
        }
      }
  • Async action creatorAn action creator that does contain async code (the most common example being network requests). As you’ll see later in the chapter, they typically make one or more API calls and dispatch one or more actions at certain points in the request’s lifecycle. Often, instead of returning an action directly, they may return synchronous action creators for readability:
    export function fetchTasks() {
      return dispatch => {
        api.fetchTasks().then(resp => {
          dispatch(fetchTasksSucceeded(resp.data));
        });
      };
    }

The syntax from the last example is a sneak peek at what’s ahead. Soon we’ll introduce the redux-thunk package, which allows you to dispatch functions in addition to standard action objects. Before you can dig into implementing async action creators, you need a simple server. Please go to the appendix and follow the instructions for setting up that server and installing two more dependencies: axios and redux-thunk. Don’t miss the important tweak required in the Redux DevTools configuration either! When you’ve finished, go on to the next section to learn how to dispatch async actions.

4.2. Invoking async actions with redux-thunk

You know you can pass an action object to the dispatch function, which will pass the action to the store and apply any updates. What if you don’t want the action to be processed immediately? What if you want to make a GET request for tasks and dispatch an action with the data from the response body? The first async action you’ll dispatch is to fetch a list of tasks from the server when the app loads. At a high level, you’ll do the following:

  • From the view, dispatch an asynchronous action to fetch tasks.
  • Perform the AJAX request to GET /tasks.
  • When the request completes, dispatch a synchronous action with the response.

We’ve been going on about async actions, but we’ve yet to show you the mechanism to accomplish them. You can transition the fetchTasks action creator from synchronous to asynchronous by returning a function instead of an object. The function you return from fetchTasks can safely perform a network request and dispatch a synchronous action with response data.

It’s possible to do this without another dependency, but you’ll find that the code to facilitate it may quickly become unmanageable. For starters, each of your components will need to be aware if they’re making a synchronous or asynchronous call, and if the latter, pass along the dispatch functionality.

The most popular option for handling async actions is redux-thunk, a Redux middleware. Understanding the ins and outs of middleware isn’t necessary now; we’ll cover middleware in depth in chapter 5. The most important takeaway is that adding the redux-thunk middleware allows us to dispatch functions as well as the standard action objects that you’re already used to. Within these functions, you’re safe to add any asynchronous code you might need.

4.2.1. Fetching tasks from a server

At this point, you have a functioning HTTP API, an AJAX library (axios), and the redux-thunk middleware, which will allow you to dispatch functions instead of action objects when you need to perform async operations such as network requests.

Currently, you’re rendering the page with a static list of tasks defined in the tasks reducer. Start by removing the list of mock tasks in src/reducers/index.js and adjust the initial state for the reducer, as shown in the following listing.

Listing 4.1. src/reducers/index.js
import { uniqueId } from '../actions'                      1
                                                           1
const mockTasks = [                                        1
  {
    id: uniqueId(),
    title: 'Learn Redux',
    description: 'The store, actions, and reducers, oh my!',
    status: 'Unstarted',
  },
  {
    id: uniqueId(),
    title: 'Peace on Earth',
    description: 'No big deal.',
    status: 'In Progress',
  },
  {
    id: uniqueId(),
    title: 'Foo',
    description: 'Bar',
    status: 'Completed',
  },
];

export function tasks(state = { tasks: [] }, action) {      2
  ...

  • 1 Remove the uniqueId import and mockTasks array completely.
  • 2 Replaces mockTasks with an empty array as the initial state for the tasks property

At a high-level, here’s what you need to add to fetch a list of tasks via AJAX:

  • When the app loads, dispatch an async action, fetchTasks, to fetch the initial tasks.
  • Make the AJAX call to /tasks.
  • When the request completes, dispatch a synchronous action, FETCH_TASKS_SUCCEEDED, with the result.

Figure 4.3 shows the fetchTasks async action creator you’re about to create in more detail.

Figure 4.3. The chronological flow of fetchTasks as an async action

As with synchronous actions, the goal of dispatching fetchTasks as an async action is to load tasks into the store, so you can render them on the page. The only difference here is that you fetch them from a server instead of relying on mock tasks defined directly in the code.

Keeping figure 4.3 in mind, let’s start from left to right by dispatching an action from the view. Import a soon-to-be-created fetchTasks action creator and dispatch it within the componentDidMount lifecycle method.

Listing 4.2. src/App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import TasksPage from './components/TasksPage';
import { createTask, editTask, fetchTasks } from './actions';  1

class App extends Component {
  componentDidMount() {
    this.props.dispatch(fetchTasks());                         2
  }

  ...

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

function mapStateToProps(state) {
  return {
    tasks: state.tasks
  };
}

  • 1 Imports the fetchTasks action creator from the actions module
  • 2 Dispatches the fetchTasks action from componentDidMount

It’s useful to think about apps in terms of data dependencies. You should ask yourself, “What data does this page or page section need to render successfully?” When you’re working in the browser, componentDidMount is the appropriate lifecycle callback to initiate AJAX requests. Because it’s configured to run whenever a component is first mounted to the DOM, it’s at this point that you can begin to fetch the data to populate that DOM. Furthermore, this is an established best practice in React.

Next, head to src/actions/index.js, where you’ll need two things:

  • An implementation of fetchTasks that performs the AJAX call
  • A new synchronous action creator, fetchTasksSucceeded, to dispatch the tasks from the server response into the store

Use axios to perform the AJAX request. Upon a successful response, the body of that response will be passed to the synchronous action creator to be dispatched, as shown in the following listing.

Listing 4.3. src/actions/index.js
import axios from 'axios';

...

export function fetchTasksSucceeded(tasks) {          1
  return {
    type: 'FETCH_TASKS_SUCCEEDED',
    payload: {
      tasks
    }
  }
}

export function fetchTasks() {
  return dispatch => {                                2
    axios.get('http://localhost:3001/tasks')          3
      .then(resp => {
        dispatch(fetchTasksSucceeded(resp.data));     4
      });
  }
}

  • 1 A new synchronous action will be dispatched if the request completes successfully.
  • 2 fetchTasks returns a function instead of an action.
  • 3 Makes the AJAX request
  • 4 Dispatches a synchronous action creator

The biggest shift in listing 4.3 from any of the actions you’ve worked with so far is that fetchTasks returns a function, not an action object. The redux-thunk middleware is what makes this possible. If you attempted to dispatch a function without the middleware applied, Redux would throw an error because it expects an object to be passed to dispatch.

Within this dispatched function, you’re free to do the following:

  • Make an AJAX request to fetch all tasks.
  • Access the store state.
  • Perform additional asynchronous logic.
  • Dispatch a synchronous action with a result.

Most async actions tend to share these basic responsibilities.

4.2.2. API clients

While you let async actions start to sink in, let’s cover a common abstraction for interacting with servers. In listing 4.3, you used axios for the first time to make a GET request to the /tasks endpoint. This is fine for the time being, but as your application grows you’ll start to run into a few issues. What if you change the base URL for the API from localhost:3001 to localhost:3002? What if you want to use a different AJAX library? You have to update that code in only one place now, but imagine if you had 10 AJAX calls. What about 100?

To address these questions, you can abstract those details into an API client and give it a friendly interface. If you’re working with a team, future developers won’t have to worry about specifics around ports, headers, the AJAX library being used, and so on.

Create a new api/directory with a single index.js file. If you’re working in a large application with many difference resources, it can make sense to create multiple files per resource, but a single index.js file will be fine to get us started.

In src/api/index.js, create a fetchTasks function that will encapsulate your API call, and configure axios with basic headers and a base URL, as shown in the following listing.

Listing 4.4. src/api/index.js
    import axios from 'axios';

const API_BASE_URL = 'http://localhost:3001';      1

const client = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',            2
  },
});

export function fetchTasks() {                     3
  return client.get('/tasks');                     3
}                                                  3

  • 1 Defines a constant for the API’s base URL
  • 2 The Content-Type header is required by json-server for PUT requests.
  • 3 Exports a named fetchTasks function that will make the call

Here you’re hardcoding the base URL for the API. In a real-world application, you’d likely get this from a server, so the value can be different based on the environment, like staging or production.

With fetchTasks, you’re encapsulating the request method as well as the URL for the endpoint. If either change, you only have to update code in one place. Note that axios.get returns a promise, which you can call .then and .catch on from within an async action creator.

Now that you’re exporting a fetchTasks function that wraps the API call, head back to src/actions/index.js and replace the existing API call with the new fetchTasks function, as shown in the following listing.

Listing 4.5. src/actions/index.js
import * as api from '../api';                    1

...

export function fetchTasks() {
  return dispatch => {
    api.fetchTasks().then(resp => {               2
      dispatch(fetchTasksSucceeded(resp.data));
    });
  };
}

...

  • 1 Imports all available API methods
  • 2 Uses the friendlier interface for making an AJAX call

Not only are the details of the request safely hidden away, the fetchTasks action creator is also clearer and more concise. By extracting an API client, you’ve improved encapsulation, future maintainability, and readability at the cost of the overhead of another module to manage. Creating new abstractions isn’t always the right answer, but in this case, it seems like a no-brainer.

4.2.3. View and server actions

You now know a few things about synchronous and asynchronous actions, but there’s one more concept that can help you form a clearer picture around how updates are happening in our applications. Typically you have two entities that can modify application state: users and servers. Actions can be divided into two groups, one for each actor: view actions and server actions.

  • View actions are initiated by users. Think FETCH_TASKS, CREATE_TASK, and EDIT_TASK. For example, a user clicks a button and an action is dispatched.
  • Server actions are initiated by a server. For example, a request successfully completes, and an action is dispatched with the response. When you implemented fetching tasks via AJAX, you introduced your first server action: FETCH_TASKS_SUCCEEDED.
Note

Certain developers like to organize view and server actions in separate directories, with the argument being it can help to break up larger files. It’s not a requirement, and we won’t do it in this book.

Getting back to the code, you have a server action, FETCH_TASKS_SUCCEEDED, which you dispatch with the list of tasks sent back from the server. Server actions are initiated by a server event like a response, but they still behave like any other action. They get dispatched and then handled by a reducer.

Let’s wrap up your initial fetching logic by updating the tasks reducer to handle receiving tasks from the server. It’s also safe to remove the mockTasks array at this point. Because you can remove the mockTasks array, you can use an empty array as the initial state for the reducer, as shown in the following listing.

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

  ...

  if (action.type === 'FETCH_TASKS_SUCCEEDED') {                2
    return {
      tasks: action.payload.tasks,
    };
  }

  return state;
}

  • 1 Make sure to pass an empty array as the initial state for tasks.
  • 2 The reducer now listens for the server action.

Notice how you didn’t have to make any updates to the view? That’s by design! Your React components don’t particularly care where the tasks come from, which allows you to totally change your strategy for acquiring tasks with relatively low effort. Keep this in mind as you continue to move through the book. When possible, you should be building each piece of your app with a clear interface—changing one piece (such as how you get a list of tasks to initially render) shouldn’t affect another (the view).

At this point you’ve introduced most of the conceptual heavy lifting that chapter 4 has to offer, so let’s do a quick recap before you move on to persisting new tasks to the server. We’ve covered the following:

  • The use of asynchronous actions and redux-thunk. The redux-thunk package allows you to dispatch functions instead of objects, and inside those functions you can make network requests and dispatch additional actions when any requests complete.
  • The role of synchronous actions. Dispatching an action object with a type and payload is considered a synchronous action, because the store receives and processes the action immediately after dispatch.
  • Users and servers are the two actors that can modify state in your applications. As a result, you can group actions into view actions and server actions.

4.3. Saving tasks to the server

Now that you’re fetching tasks from the server when the app loads, let’s update creating tasks and editing tasks to be persisted on the server. The process will be similar to what you’ve seen with fetching tasks.

Let’s start by saving new tasks. You already have a framework in place: when a user fills out the form and submits, you dispatch the createTask action creator, the store receives the CREATE_TASK action, the reducer handles updating state, and the changes are broadcast back to the UI.

The createTask command needs to return a function instead of an object. Within that function, you can make your API call and dispatch an action when a response is available. Here’s a quick look at the high-level steps:

  • Convert the synchronous createTask action creator into an async action creator.
  • Add a new method to your API client, which will send a POST request to the server.
  • Create a new server action, CREATE_TASK_SUCCEEDED, whose payload will be a single task object.
  • In the createTask action creator, initiate the request, and dispatch CREATE_TASK_SUCCEEDED when the request finishes. For now, you can assume it will always be successful.

Remove the uniqueId function. It was originally meant as a stopgap until you could create tasks on a server, which would be responsible for adding an ID.

Now you’re there! Create a new function for creating tasks in the API client, as shown in the following listing.

Listing 4.7. src/api/index.js
...
export function createTask(params) {
  return client.post('/tasks', params);          1
}

  • 1 A POST request is required to add or update data on the server.

Now you can modify the createTask action creator to return a function, as shown in the following listing.

Listing 4.8. src/actions/index.js
   import * as api from '../api';

...

function createTaskSucceeded(task) {                                 1
  return {
    type: 'CREATE_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function createTask({ title, description, status = 'Unstarted' }) {
  return dispatch => {
    api.createTask({ title, description, status }).then(resp => {
      dispatch(createTaskSucceeded(resp.data));                      2
    });
  };
}

  • 1 Creates a new synchronous action creator
  • 2 Loads the newly created object into the store

You know that reducers handle updates to state, so update the tasks reducer to handle the CREATE_TASK_SUCCEEDED action. After that, you’ll be up to four action handlers, so now is as good a time as any to merge each if statement into a friendlier switch statement, as shown in the following listing. This is a common Redux pattern.

Listing 4.9. src/reducers/index.js
export default function tasks(state = { tasks: [] }, action) {
  switch (action.type) {                                         1
    case 'CREATE_TASK': {
      return {
        tasks: state.tasks.concat(action.payload),
      };
    }
    case 'EDIT_TASK': {
      const { payload } = action;
      return {
        tasks: state.tasks.map(task => {
          if (task.id === payload.id) {
            return Object.assign({}, task, payload.params);
          }

          return task;
        }),
      };
    }
    case 'FETCH_TASKS_SUCCEEDED': {
      return {
        tasks: action.payload.tasks,
      };
    }
    case 'CREATE_TASK_SUCCEEDED': {                             2
      return {
        tasks: state.tasks.concat(action.payload.task),
      };
    }
    default: {
      return state;
    }
  }
}

  • 1 Moves to a switch statement instead of a long if-else chain
  • 2 Shows the new action handler

A switch statement is a slightly friendlier syntax when there are a significant number of cases to handle. This is the structure that you’ll typically see most often in Redux reducers, but using a switch statement isn’t a hard requirement.

Play around in the browser and create new tasks. When you refresh, they should appear again for the initial render. The big idea here is that the createTask action creator now returns a function instead of an object. The newly created task isn’t received by the store immediately after being dispatched, but instead is dispatched to the store after the POST request to /tasks has completed.

4.4. Exercise

Task updates are the last feature you need to hook up to the server. The process for fetching, creating, and now editing tasks is nearly identical. This exercise is a good way to test whether you’re ready to connect the dots on your own.

We’ll outline the high-level steps needed to make everything work, but as a challenge, see if you can implement the code before glancing through the listings in the solution section. The requirements are the following:

  • Add a new API function for updating tasks on the server.
  • Convert the editTask action creator from synchronous to asynchronous.
  • Within editTask, kick off an AJAX request.
  • When the request is complete, dispatch an action with the updated object that comes back as part of the server response.

4.5. Solution

The first thing you’ll do is add a new API function, editTask, as shown in the following listing.

Listing 4.10. src/api/index.js
export function editTask(id, params) {
  return axios.put(`${API_BASE_URL}/tasks/${id}`, params);         1
}

  • 1 Uses an ES2015 template string to easily construct the URL

Now that you have a function you can import to make the right AJAX request, create a new async action creator to make the request to the server, and a new synchronous action creator to indicate the request has completed, as shown in the following listing.

Listing 4.11. src/actions/index.js
...

function editTaskSucceeded(task) {                          1
  return {
    type: 'EDIT_TASK_SUCCEEDED',
    payload: {
      task,
    },
  };
}

export function editTask(id, params = {}) {
  return (dispatch, getState) => {
    const task = getTaskById(getState().tasks.tasks, id);   2
    const updatedTask = Object.assign({}, task, params);    2

    api.editTask(id, updatedTask).then(resp => {
      dispatch(editTaskSucceeded(resp.data));
    });
  };
}

function getTaskById(tasks, id) {
  return tasks.find(task => task.id === id);
}

  • 1 Creates a new synchronous action creator for edits
  • 2 Merges the new properties into the existing task object

For each action that requires a network request (meaning you’re dealing with an async action), you’ll need at least one synchronous action creator to indicate where you are in the request/response lifecycle. Here, that’s editTaskSucceeded, which indicates the request has completed successfully and passes data from the response body on to the reducer.

Because json-server requires a full object to be passed along for PUT requests, you must grab the task out of the store and merge in the new properties yourself, as shown in the following listing.

Listing 4.12. src/reducers/index.js
export default function tasks(state = { tasks: [] }, action) {
  switch (action.type) {

    ...

    case 'EDIT_TASK_SUCCEEDED': {                      1
      const { payload } = action;
      return {
        tasks: state.tasks.map(task => {
          if (task.id === payload.task.id) {           2
            return payload.task;
          }

          return task;
        }),
      };
    }
    default: {
      return state;
    }
  }
}

  • 1 Handles the new server action
  • 2 Replaces the old task with the updated one

As you may have noticed, the process for saving updates to tasks is similar to the process for creating tasks. All interactions that trigger an async operation (usually a network request) tend to have these same high-level happenings:

  • A user interacts with the UI in some way, triggering a dispatch.
  • A request is started.
  • When the request finishes, an action is dispatched with response data.

Go ahead and update a few tasks. The UI should be responsive, given that your server is running locally, and the work that the server is doing is inexpensive. But that won’t always be the case. Requests in real-world applications inevitably will take longer due to latency or expensive operations, which means you need a kind of user feedback while a request is completing. Segue!

4.6. Loading states

As UI programmers, you always want to keep your users well-informed of what’s happening in the application. Users have an expectation that certain things take time to complete, but they won’t forgive being left in the dark. When creating user experiences, user confusion should be one of the primary things you try to eliminate completely.

Enter loading states! You’ll use Redux to track the status of a request and update the UI to render the proper feedback when a request is in progress. One obvious place to start is during the initial fetch for tasks when the page loads.

4.6.1. The request lifecycle

With network requests, there are two moments in time that you care about: when the request starts, and when it completes. If you model these events as actions, you end up with three distinct action types that help describe the request-response lifecycle. Using fetching tasks as an example, note the following are the three action types:

  • FETCH_TASKS_STARTED—Dispatched when a request is initiated. Typically used to render a loading indicator (which you’ll do in this section).
  • FETCH_TASKS_SUCCEEDED—Dispatched when a request is completed successfully. Takes data from the response body and loads it into the store.
  • FETCH_TASKS_SUCCEEDED—Dispatched when a request fails for any reason, such as a network failure or a response with a non-200 status code. Payloads often include an error message from the server.

Right now, the fetchTasks action creator accounts for only one moment in time, when the request completes. Figure 4.4 is what fetchTasks might look like if you also want to track when the request was initiated.

Figure 4.4. The fetchTasks async action creator with support for loading states

Now your async action creator, fetchTasks, will be responsible for three things:

  • Dispatching an action to signify a request has been started
  • Performing the request
  • Dispatching a second action with response data when the request completes

Think of async actions as orchestrations. They typically perform several individual tasks in pursuit of fulfilling a larger goal. Your goal is to fetch a list of tasks, but it takes several steps to get there. That’s the role of the fetchTasks async action creator: to kick off everything that needs to get done to accomplish that goal.

Your store has only a single property, tasks:

{
  tasks: [...]
}

To render a loading indicator when a request for tasks is in progress, you need to keep track of more state. Here’s how you want the store to track request status:

{
  tasks: {
    isLoading: false,
    tasks: [...]
  }
}

This structure is much more flexible, because it allows you to group the task objects, along with any kind of additional state or metadata. In any real-world application, you’d be much more likely to come across a structure like this. The specific naming convention of the keys is only one pattern you can choose to adopt, but you’re not obligated to use it.

4.6.2. Adding the loading indicator

You have a few things to take care of in the code to move toward this new state structure. To start, update the tasks reducer to take a new initial state, and update the existing action handlers, as shown in the following listing.

Listing 4.13. src/reducers/index.js
const initialState = {                                       1
  tasks: [],                                                 1
  isLoading: false,                                          1
};                                                           1

export default function tasks(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_TASKS_SUCCEEDED': {
      return {
        ...state,                                            2
        isLoading: false,                                    2
        tasks: action.payload.tasks,                         2
      };
    }
    case 'CREATE_TASK_SUCCEEDED': {
      return {
        ...state,                                            3
        tasks: state.tasks.concat(action.payload.task),
      };
    }
    case 'EDIT_TASK_SUCCEEDED': {
      const { payload } = action;
      const nextTasks = state.tasks.map(task => {
        if (task.id === payload.task.id) {
          return payload.task;
        }

        return task;
      });
      return {
        ...state,                                           3
        tasks: nextTasks,
      };
    }
    default: {
      return state;
    }
  }
}

  • 1 Defines the new initial state for the reducer
  • 2 Returns the next state with the list of tasks from the payload
  • 3 Includes any existing state when updating the list of tasks

First, you made sure to set the isLoading flag to false by default when you defined the initial state for the reducer. This is always good practice, because it prevents any loading indicators from rendering when they’re not supposed to. Let other actions that indicate a request has started set a flag like this to true.

When you handle the FETCH_TASKS_SUCCEEDED action, the obvious change is to update the array of tasks. You also have to remember to indicate the request is complete, so that any loading indicators are hidden, which you did by toggling the isLoading flag to false.

One important update you need to make is all the way up in the creation of the store, in index.js. Previously, you passed the tasks reducer directly to createStore. This was fine when all you had in your state object was a single array of tasks, but now you’re moving toward a more complete structure with additional state that lives alongside the task objects themselves.

Create a small root reducer that takes the entire contents of the store (state) and the action being dispatched (action), and passes only the piece of the store that the tasks reducer cares about, state.tasks, as shown in the following listing.

Listing 4.14. index.js
...
import tasksReducer from './reducers';

const rootReducer = (state = {}, action) => {       1
  return {
    tasks: tasksReducer(state.tasks, action),       2
  };
};

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk)),
);

  • 1 A rootReducer function accepts the current state of the store and an action.
  • 2 Passes the tasks data and the action being dispatched to the tasks reducer

Adding a root reducer like this sets you up for the future as well. As you add more features to Parsnip, and, as a result, have more data to track in Redux, you can add new top-level properties to the store and create reducers that operate only on relevant data. Eventually you’ll add the ability for users to have different projects, and each project will have its own tasks. The top level of the redux store might look like this:

{
  tasks: {...},
  projects: {...}
}

To configure the store, all you’d need to do is add a line of code to the root reducer:

const rootReducer = (state = {}, action) => {
  return {
    tasks: tasksReducer(state.tasks, action),
    projects: projectsReducer(state.projects, action),
  };
};

This allows each reducer to not care about the overall shape of the store, only the slice of data that it operates on.

Note

Using a root reducer like this is so common that redux exports a combineReducers function, which accomplishes the same thing as the rootReducer function you just wrote. Once you add a few more properties to your state object, you’ll switch to the more standard combineReducers, but for now it’s worth understanding how this process works under the hood.

Next, run through the following familiar process of adding a new action by updating the relevant action, reducer, and component:

  • Add and dispatch a new synchronous action creator, fetchTasksStarted.
  • Handle the FETCH_TASKS_STARTED action in the tasks reducer.
  • Update the TasksPage component to render a loading indicator when the fetch is in progress.

First, dispatch the new action. For dramatic effect, you’ll add a setTimeout of two seconds before you dispatch fetchTasksSucceeded, which indicates the request has completed, as shown in the following listing. Because the server responds almost instantly when running on your local machines, this delay gives you a chance to get a good feel for the loading state.

Listing 4.15. src/actions/index.js
function fetchTasksStarted() {
  return {
    type: 'FETCH_TASKS_STARTED',
  };
}

export function fetchTasks() {
  return dispatch => {
    dispatch(fetchTasksStarted());                    1

    api.fetchTasks().then(resp => {
      setTimeout(() => {                              2
        dispatch(fetchTasksSucceeded(resp.data));     2
      }, 2000);                                       2
    });
  };
}

  • 1 Dispatches the fetchTasksStarted action creator to signify a request is in progress
  • 2 setTimeout ensures the loading indicator will stay on the page for more than a fraction of a second.

Now as part of your async action, you have two synchronous dispatches to track the request lifecycle. One indicates when the request starts, the other when it completes. Next, handle the FETCH_TASKS_STARTED action by setting the isLoading property to true in the reducer, as shown in the following listing.

Listing 4.16. src/reducers/index.js
...
export default function tasks(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_TASKS_STARTED': {
      return {
        ...state,
        isLoading: true,            1
      };
    }
    ...
  }
}

  • 1 Sets the isLoading flag to true, which we’ll eventually use in a React component to conditionally render a loading indicator.

Finally, you’ll update two components, App and TaskPage. In App, pass the value of isLoading to TasksPage via mapStateToProps, as shown in the following listing.

Listing 4.17. src/App.js
...
class App extends Component {
  ...
  render() {
    return (
      <div className="main-content">
        <TasksPage
          tasks={this.props.tasks}
          onCreateTask={this.onCreateTask}
          onStatusChange={this.onStatusChange}
          isLoading={this.props.isLoading}            1
        />
      </div>
    );
  }
}
function mapStateToProps(state) {
  const { tasks, isLoading } = state.tasks;           2
  return { tasks, isLoading };
}
...

  • 1 Passes the isLoading prop down to the TasksPage
  • 2 Updates mapStateToProps to pull isLoading out of the store and passes it to App as a prop

Finally, in TasksPage, you can check whether a request for tasks is in progress, then render a loading indicator if necessary, as shown in the following listing.

Listing 4.18. src/components/TasksPage.js
class TasksPage extends Component {
  ...
  render() {
    if (this.props.isLoading) {
      return (
        <div className="tasks-loading">
          Loading...                      1
        </div>
      );
    }
    ...
  }

  • 1 Adds a fancy loading animation here

Tracking the status of a request to show a loading indicator isn’t necessarily required, but it’s become part of the expected experience in most modern web apps. This is one example of the increasingly dynamic requirements of modern web apps, but Redux was created in part to solve exactly these kinds of problems.

4.7. Error handling

With network requests, there are two moments in time you care about: the request starting and the request finishing. The invoking of the fetchTasks request will always be represented by the FETCH_TASKS_STARTED action, but request completion can trigger one of two dispatches. You’ve handled only the success case so far, but proper error handling is crucial for delivering the best experience possible.

Users are never thrilled when something goes wrong, but having an operation fail and being left in the dark is much worse than seeing an error with some feedback. Taking one last look at the async action diagram in figure 4.5, the second dispatch can now fork depending on the outcome of the request.

Figure 4.5. The command fetchTasks now handles the three types of actions that describe a request.

4.7.1. Dispatching an error action

You have plenty of ways to implement error handling in Redux. At a high level, these are the things you’ll need:

  • An action that dispatches an error message
  • Somewhere to store the error message in the Redux store
  • A React component to render the error

To keep things simple, you’ll dispatch a single action, FETCH_TASKS_FAILED, with the error payload. When an error is present, you’ll render the message at the top of the page, which will look something like figure 4.6.

Figure 4.6. Rendering an error message

Start from the outside-in by creating a new file for a FlashMessage component, as shown in listing 4.19. Its purpose is to accept an error message as a prop and display it in the DOM.

Listing 4.19. src/components/FlashMessage.js
import React from 'react';                     1

export default function FlashMessage(props) {
  return (
    <div className="flash-error">
      {props.message}
    </div>
  );
}

Error.defaultProps = {
  message: 'An error occurred',                2
};

  • 1 Even though you don’t reference React directly in this file, the React object needs to be in scope to use JSX.
  • 2 A default error message is set.

In the App component, pass along the yet-to-be-created error property from the redux store in mapStateToProps, as shown in the following listing.

Listing 4.20. src/App.js
...
import FlashMessage from './components/FlashMessage';

class App extends Component {
  ...
  render() {
    return (
      <div className="container">
        {this.props.error &&
          <FlashMessage message={this.props.error} />}     1
        <div className="main-content">
          <TasksPage
            tasks={this.props.tasks}
            onCreateTask={this.onCreateTask}
            onStatusChange={this.onStatusChange}
            isLoading={this.props.isLoading}
          />
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  const { tasks, isLoading, error } = state.tasks;          2
  return { tasks, isLoading, error };                       2
}

export default connect(mapStateToProps)(App);

  • 1 Conditionally renders the FlashMessage component
  • 2 Adds more map-StateToProps logic for passing data from the store into React

Because this.props.error is null for now, nothing will happen yet in the UI. You’ll need to create a new synchronous action creator, fetchTasksFailed. You already have code to handle when the request promise resolves successfully, so go ahead and add a catch block to handle when the promise is rejected.

To make testing error handling easier, manually reject a promise in the then block, so that you’re guaranteed to make it into the catch block, as shown in the following listing.

Listing 4.21. src/actions/index.js
function fetchTasksFailed(error) {
  return {
    type: 'FETCH_TASKS_FAILED',
    payload: {
      error,
    },
  };
}

export function fetchTasks() {
  return dispatch => {
    dispatch(fetchTasksStarted());

    api
      .fetchTasks()
      .then(resp => {
        // setTimeout(() => {                                  1
        //   dispatch(fetchTasksSucceeded(resp.data));         1
        // }, 2000);                                           1
        throw new Error('Oh noes! Unable to fetch tasks!'));   2
      })
      .catch(err => {
        dispatch(fetchTasksFailed(err.message));               3
      });
    };
  };
  ...

  • 1 Comments out the success handler for now
  • 2 Manually rejects the promise, to test the code in the catch block
  • 3 Dispatches another synchronous action with an error message

Finally, handle the update logic in the tasks reducer. This is a two-part change: add an error property to the initial state definition, then add a handler for the FETCH_TASKS_FAILED action. The case statement will mark the request as complete by setting isLoading to false and set the error message, as shown in the following listing.

Listing 4.22. src/reducers/index.js
const initialState = {
  tasks: [],
  isLoading: false,
  error: null,                          1
};

export default function tasks(state = initialState, action) {
  switch (action.type) {
    ...
    case 'FETCH_TASKS_FAILED': {        2
      return {                          2
        ...state,                       2
        isLoading: false,               2
        error: action.payload.error,    2
      };
    }
    ...
    default: {
      return state;
    }
  }
}

  • 1 Sets error to be null by default
  • 2 Indicates the request is complete by setting the isLoading flag and error value

All said and done, it’s clear that fetching a list of tasks to render on the page is much more than making a GET request. These are the realities of modern web app development, but tools like Redux are here to help. You can handle tracking complex state to provide the best user experiences possible.

When first learning Redux, the stereotypical first tutorial is a todo app. It all seems so simple! Dispatch an action, update state in a reducer. But the question quickly turns to “How do you do anything useful?” Turns out you can’t do much in a web application without being backed by a server.

Async actions are one of the real challenges for a budding Redux user. Compared to chapter 2, where only synchronous actions are found, the complexity level in this chapter was ramped up significantly. Hopefully you now have a sense for how to properly handle asynchronous code in Redux.

By using redux-thunk, you took advantage of middleware without needing to understand what that meant. The next chapter lifts the veil on middleware and shows you all there is to see.

Summary

  • The difference between dispatching asynchronous and synchronous actions
  • How redux-thunk enables the dispatching of functions, which can be used to perform side effects, like network requests
  • How API clients can reduce duplication and improve reusability
  • The two conceptual groups of actions: view actions and server actions
  • The three important moments during the lifecycle of a remote API call: start, successful completion, and failure
  • Rendering errors to improve overall user experience
..................Content has been hidden....................

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