DEV Community

Nesha Zoric
Nesha Zoric

Posted on

Building a Simple React App - Part 3

This is the third part of our series about building simple react application. In this part, our main topic will be connecting our application to RESTful API. For that, we will need to use async actions, another important concept. If you haven't read previous parts, you can find them on following links part 1, part 2.

Async actions

In order to use async actions, we need to inject middleware called thunk. Thunk allows us to write async actions (action creators). As you know, until now all actions just returned simple action object, which would be dispatched automatically. With the thunk, we get a possibility to control what and when will be dispatched, it provides us possibility to return the function from action which can call dispatch manually. You will see in a second what that means to us. First let's add that middleware, while we are here, we will add one more middleware (redux-logger) which will log each action as it gets dispatched along with application state before and after that action, pretty nice for debugging. First of all, install this two packages.

npm install --save redux-thunk redux-logger

And then inject them into the application.

// src/index.js

...
import { createStore, applyMiddleware } from 'redux'

import thunk from 'redux-thunk';
import logger from 'redux-logger';
...

let store = createStore(
  appReducer,
  applyMiddleware(logger, thunk)
);
...

So we just imported two middlewares we want to inject, added applyMiddleware function from redux. Inside createStore we added the second parameter where we defined which middlewares we want to be injected (applied). Ok, now when we resolved that, let's add our first async action.

Setup RESTful API server

We don't want our todos to be defined in the initial state, on our front-end, we want them to be fetched from some external resource. Instead of writing our RESTful API here we will use json-server. It is quite simple to setup, we will go through that process right now. First, we need to install json-server

npm install -g json-server

Then create db.json file which will represent our database, and json-server will create all CRUD actions over our resources defined in that file, and will change that file immediately. It is a great tool for front-end testing. We will create db.json file inside our project, just to group all stuff into one place.

// db.json

{
  "todos": [
    {
      "id": 1,
      "task": "This is simple API test task",
      "done": false
    },
    {
      "id": 2,
      "task": "This is simple API test task 2",
      "done": false
    },
    {
      "id": 3,
      "task": "This is simple API test task 3",
      "done": true
    }
  ]
}

This file is placed in the top folder (with package.json and README.md). If you take a look at this structure, you will see that it is pretty similar to one we've defined in reducers initial state (only task texts are different). Now we will start the server. Open new terminal tab and type:

# cd path-to-project/
json-server -p 9000 --watch db.json

You should see something like this.

And that is all, now you have all of the CRUD operations on todo resource, which are available through localhost:9000. Now we can really write our first async action, which would be fetching all todos and putting them into our state.

First async action and fetching data from API

// src/components/Home/TodoList/actions/todoActions.js

export const fetchTodosStart = () => ({
  type: types.FETCH_TODOS_START
});

export const fetchTodosError = (error: Error) => ({
  type: types.FETCH_TODOS_ERROR,
  error
});

export const fetchTodosSuccess = (todos: Array) => ({
  type: types.FETCH_TODOS_SUCCESS,
  payload: { todos }
});

export const fetchTodos = () => dispatch => {
  dispatch(fetchTodosStart());

  fetch(`${API_URL}/todos`)
    .then((response) => response.json())
    .then((body) => dispatch(fetchTodosSuccess(body)))
    .catch((error) => dispatch(fetchTodosError(error)));
}

We practically created four actions (action creators), three are simple actions returning just a action object, and one is async (fetchTodos) which dispatches other three when it should. We could theoretically use any of these three simple actions directly, but we won't need that. fetchTodosStart is simple action which purpose is just to notify system that fetchTodos action has started, fetchTodosError notifies system that some error occurred while fetching todos, and fetchTodosSuccess notifies system that todos are fetched and passes those fetched todos in action object.

Nothing new here, now let's take a look at fetchTodos. The first thing to note here is that this action doesn't return a simple object but a function, with dispatch as a parameter (getState is another parameter provided by thunk, but we don't need that here, so we don't store it anywhere). At the beginning, we dispatch signal that fetching has started. Then we do real fetch using fetch method from the native framework. If everything goes well, we dispatch success signal sending response body as a value of todos parameter, and if any error (catch part), we just dispatch error signal providing that error as a parameter. Nothing complicated, right? That is it, we created async action, which fetches data from the server, parses it (response.json() part) and notifies system on each "breakpoint". This pattern with three simple actions (as a help) will be followed by this article. It is not mandatory, you could do something like

fetch(`${API_URL}/todos`)
  .then((response) => response.json())
  .then((body) => dispatch({
    type: types.FETCH_TODOS_SUCCESS,
    payload: { todos: body }
  })
  .catch((error) => dispatch({
    type: types.FETCH_TODOS_ERROR,
    payload: { error }
  });

But I find it more readable when it is separated. We haven't yet defined API_URL constant.

// src/utils/configConstants.js

export const API_URL = 'http://localhost:9000';

And of course, we need to import that constant in todoActions.js

// src/components/Home/TodoList/actions/todoActions.js

import { API_URL } from '../../../../utils/configConstants';

Right now we are getting an error in our front-end application (Failed to compile. "export 'FETCH_TODOS_SUCCESS' (imported as 'types') was not found in '../constants'.") That is because we haven't defined constants, and yet we use them. So let's define that.

// src/components/Home/TodoList/constants.js

export const FETCH_TODOS_START = 'FETCH_TODOS_START';
export const FETCH_TODOS_ERROR = 'FETCH_TODOS_ERROR';
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';

Next step is to add reducer handler for this action(s), otherwise, all those signals would be useless. We will do this just by adding new case inside todoReducer.

case types.FETCH_TODOS_SUCCESS:
  return state.set('todos', [...action.payload.todos]);

Nice and simple, just exchange state.todos with the new array containing data received from action object. We haven't handled FETCH_TODOS_ERROR and FETCH_TODOS_START, currently, they are not in our main focus. You could handle error signal in some global way, or locally to your todoReducer, it depends on you, however you want to. Start signal can be useful for something like rendering loading the bar on screen or disabling some option until action is finished, just notice that no END signal is sent, so you will have to handle end on success and on error. The circle is now complete, all we need to do now is to actually make a use of this.

We won't need anymore that initial state defined in todoReducer (that was just test data), so let's delete it.

// src/components/Home/TodoList/reducers/todoReducer.js

...
const TodoState = new Record({
  todos: []
});
...

If you look at your application now, there won't be any todos on the screen, exactly what we wanted. Now let's fetch. Where would we add this part of the the code. If you remember from last part where we talked about presentational and container components, we said that those container components should handle data fetching, so we need to change our TodoListContainer.

// src/components/Home/TodoList/TodoListContainer.jsx

import React, { Component } from 'react';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import { setTodoDone, deleteTodo, addTodo, fetchTodos } from './actions/todoActions';
import TodoList from './TodoList';

class TodoListContainer extends Component {

  componentDidMount() {
    this.props.fetchTodos();
  }

  render() {
    return <TodoList {...this.props} />
  }

}

const mapStateToProps = state => ({
  todos: state.todoReducer.todos
});

const mapDispatchToProps = dispatch => bindActionCreators({
  setTodoDone,
  deleteTodo,
  addTodo,
  fetchTodos,
}, dispatch)


export default connect(mapStateToProps, mapDispatchToProps)(TodoListContainer);

Most parts stayed the same, we linked fetchTodos action in our mapDispatchToProps (and imported it at the top). But now simple connect wrapper isn't enough for us, we need something more, something that will actually fetch data in some moment. That's why we created a new component (real TodoListContainer) and used lifecycle method componentDidMount in which fetching is actually called. Its render method is just simple returning TodoList with all received props sent down. So it is still just a wrapper, only "smart" wrapper which does something before rendering wrapped component. Now if you go to your browser and look at the application you should see three todos defined in our db.json.

And our logger middleware is logging each action on our console, as you can see, only FETCH_TODOS_START and FETCH_TODOS_SUCCESS is logged (first logged action you can disregard, it is just a log for fetchTodos which doesn't actually need to be logged). If you try to add, modify or delete any todo now, it will still work as it worked before, but won't be saved to the database, that is because those actions just change reducer, neither one is actually "talking" to an external source (API), let's fix that.

Adding new todo

export const addTodoStart = () => ({
  type: types.ADD_TODO_START
});

export const addTodoError = (error: Error) => ({
  type: types.ADD_TODO_ERROR,
  error
});

export const addTodoSuccess = (todo: Object) => ({
  type: types.ADD_TODO_SUCCESS,
  payload: {
    todo
  }
})

export const addTodo = (task: String) => dispatch => {
  dispatch(addTodoStart());

  fetch(`${API_URL}/todos`, { 
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      task,
      done: false,
    })
  })
    .then((response) => response.json())
    .then((body) => dispatch(addTodoSuccess(body)))
    .catch((error) => dispatch(addTodoError(error)));
}

We replaced addTodo action with an async one, also we added already familiar three methods (start, error and success actions) as helpers. The interesting thing here is that todo creating is moved from reducer into the action, actually, it is moved to API, but because of default API behavior we have to provide all parameters (can't create default value on API - which is what we would do in a real application). It is pretty much same as fetchTodo action, on start it dispatches start signal, after that, it hits API endpoint, only difference is that here we need to send POST method, set header for Content-Type so that API knows how we formatted data which we send, and last but not least we need to send real data in body as JSON encoded string. After that, we get a response, parse it as JSON, and dispatch body as new todo object with success signal, or in case of error, just dispatch error signal with that error. Why we dispatch value returned from the server instead of object we created? Simple, the server will automatically create an id, which we need for modification and removal, so we need to wait for the server to give us complete object, which we will then store in reducer. Let's see reducer modifications to support this.

// old case
case types.ADD_TODO:
  return state.set('todos', [ ...state.todos, { id: getLastId(state.todos) + 1, task: action.payload.task, done: false } ]);

// new case
case types.ADD_TODO_SUCCESS:
  return state.set('todos', [...state.todos, action.payload.todo]);

It is actually simplified, reducer doesn't need to generate id, or an object anymore (it shouldn't generate resources anyway). That is it. Try now adding new todo and refreshing page, it persists.

Deleting todo

export const deleteTodoStart = () => ({
  type: types.DELETE_TODO_START,
});

export const deleteTodoError = (error: Error) => ({
  type: types.DELETE_TODO_ERROR,
  error
});

export const deleteTodoSuccess = (id: Number) => ({
  type: types.DELETE_TODO_SUCCESS,
  payload: {
    id
  }
});

export const deleteTodo = (id: Number) => dispatch => {
  dispatch(deleteTodoStart());

  fetch(`${API_URL}/todos/${id}`, {
    method: 'DELETE',
  })
    .then((response) => dispatch(deleteTodoSuccess(id)))
    .catch((error) => dispatch(deleteTodoError(error)));
}

As it goes for deleteTodo, it is pretty much the same. Helper methods (actions) are there, same as always, nothing new there, and bind-together action deleteTodo is also same as others, only difference is http method, and the fact that we don't need to parse response body (it is empty), we just need to know that response was returned successfully without error (valid status code), and we can dispatch success signal. Reducer hasn't changed at all, the only thing that changed is the name of constant on which handler is called, renamed from DELETE_TODO into DELETE_TODO_SUCCESS.

Updating todo

export const setTodoDoneStart = () => ({
  type: types.SET_TODO_DONE_START
})

export const setTodoDoneError = (error: Error) => ({
  type: types.SET_TODO_DONE_ERROR,
  error
});

export const setTodoDoneSuccess = (id: Number, done: Boolean) => ({
  type: types.SET_TODO_DONE_SUCCESS,
  payload: {
    id,
    done
  }
})

// Changed from id: Number into todo: Object to use PUT /todos/:id, avoid creating custom api routes
export const setTodoDone = (todo: Object, done: Boolean) => dispatch => {
  dispatch(setTodoDoneStart());

  fetch(`${API_URL}/todos/${todo.id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ ...todo, done })
  })
    .then((response) => dispatch(setTodoDoneSuccess(todo.id, done)))
    .catch((error) => dispatch(setTodoDoneError(error)));
}

Same goes for setTodoDone, everything stays the same as before. Here we use PUT method, to use default API update method because we are avoiding custom API routes (in a real application you would probably have separate route only for setting done, which would get only an id). Reducer hasn't been changed for this either (only constant name). For this we have to change a little bit call to the method (because we changed the interface, it doesn't get the only id anymore), so we need to modify a little bit Todo component. Inside Todo render method we just need to change our setDone handler, instead of () => setDone(todo.id, !todo.done), we want () => setDone(todo, !todo.done). And that is all. Now we completely migrated our application to use RESTful API for all data operations.

Conclusion

In this part, we connected our application to RESTful API and adapted all actions to actually hit API endpoints, and change data on the server. One thing you could do in a real application is to extract fetch call into a helper method (or a class) so that you can easily replace the library that you are using for http requests. Another thing that may be useful in real examples is normalizr, it won't be discussed here but I encourage you to take a look. Next part will be the final part of this series, and it will show you usage of selectors, and also we will focus a little on application styling.

Originally published at Kolosek blog.

Top comments (0)