How to create a three layer application with React

Photo by
João Silas on 
Unsplash

Splitting a Single Page Application into layers has a set of advantages:

  • a better separation of concerns
  • the layer implementation can be replaced
  • the UI layer can be hard to test. By moving the logic to other layers, it becomes easier to test.

Below we can see the diagram of an application split in the three main layers:

  • UI (aka Presentation)
  • Domain (aka Business)
  • Data Access

Application Layers

The showcase

I’ll take the case of an application managing a list of to-dos. The user is able to see and search for to-dos.

Check the full implementation on git-hub.

UI Layer

The UI layer is responsible for displaying data on the page, and for handling user interactions. The UI Layer is made up of components.

I split the page in the following components:

  • TodoContainer manages the communication between TodoSearch.TodoList and other external objects
  • TodoSearchForm is the form for searching to-dos
  • TodoList displays the list of to-dos
  • TodoListItem: displays a single to-do in the list

Components Tree

TodoSearch

The component uses the handleChange handler to read the input value on any change. TodoSearch exposes a new property: onSearch . It can be used by the parent component to handle the search click.

The component doesn’t communicate with any other external objects, except its parent. TodoSearch is a presentation component.

export default class TodoSearch extends React.Component {   constructor(props){    super(props);    this.search = this.search.bind(this);    this.handleChange = this.handleChange.bind(this);  }    componentWillMount() {    this.setState({text: ""});  }    search(){    const query = Object.freeze({ text: this.state.text });    if(this.props.onSearch)      this.props.onSearch(query);  }    handleChange(event) {    this.setState({text: event.target.value});  }    render() {    return <form>      <input onChange={this.handleChange} value={this.state.text} />      <button onClick={this.search} type="button">Search</button>    </form>;  }}Copy the code

TodoList

TodoList gets the list of todos to render using a property. It sends the todos, one by one, to the TodoListItem.

TodoList is a stateless functional component.

export default function TodoList(props) { function renderTodoItem(todo){ return <TodoListItem todo={todo} key={todo.id}></TodoListItem>; } return <div className="todo-list"> <ul> { props.todos.map(renderTodoItem) } </ul> </div>; }Copy the code

TodoListItem

TodoListItem displays the todo received as a parameter. It is implemented as a stateless functional component.

export default function TodoListItem(props){ return <li> <div>{ props.todo.title}</div> <div>{ props.todo.userName }</div> </li>; }Copy the code

TodoContainer

The TodoContainer is connected to the todoStore external object and subscribes to its change events.

It uses the onSearch handler to read the search criteria from TodoSearch. It then creates a filtered list using the todoStore and sends the new list to the TodoList component.

TodoContainer keeps all the UI state, the query object in this case.

import TodoList from "./TodoList.jsx"; import TodoSearch from "./TodoSearch.jsx"; export default class TodoContainer extends React.Component { constructor(props){ super(props); this.todoStore = props.stores.todoStore; this.search = this.search.bind(this); this.reload = this.reload.bind(this); } componentWillMount(){ this.todoStore.onChange(this.reload); this.todoStore.fetch(); this.query = null; this.setState({todos : []}); } reload(){ const todos = this.todoStore.getBy(this.query); this.setState({ todos }); } search(query){ this.query = query; this.reload(); } render() { return <div> <TodoSearch onSearch={this.search} /> <TodoList todos={this.state.todos} /> </div>; }}Copy the code

Domain Layer

The domain layer is made of domain stores. The main purposes of the domain store is to store the domain state and to keep it in sync with the server.

The application responsibilities are split between two domain stores:

  • TodoStore manages the to-do data objects
  • UserStore manages the user data objects

The domain store is a publisher. It emits events every time its state changes. Components can subscribe to these events and update the user interface.

TodoStore is the single source of truth regarding to-dos.

TodoStore

import MicroEmitter from 'micro-emitter'; import partial from "lodash/partial"; export default function TodoStore(dataService, userStore){ let todos = []; const eventEmitter = new MicroEmitter(); const CHANGE_EVENT = "change"; function fetch() { return dataService.get().then(setLocalTodos); } function setLocalTodos(newTodos){ todos = newTodos; eventEmitter.emit(CHANGE_EVENT); } function toViewModel(todo){ return Object.freeze({ id : todo.id, title : todo.title, userName : userStore.getById(todo.userId).name }); } function descById(todo1, todo2){ return parseInt(todo2.id) - parseInt(todo1.id); } function queryContainsTodo(query, todo){ if(query && query.text){ return todo.title.includes(query.text); } return true; } function getBy(query) { const top = 25; const byQuery = partial(queryContainsTodo, query); return todos.filter(byQuery) .map(toViewModel) .sort(descById).slice(0, top); } function onChange(handler){ eventEmitter.on(CHANGE_EVENT, handler); } return Object.freeze({ fetch, getBy, onChange }); }Copy the code

TodoStore is implemented using a factory function. The object has many methods but only three of them are public.

I favor factory functions over classes. For more on the topic, take a look at How to build reliable objects with factory functions in JavaScript.

Use is separated from construction. All dependencies of TodoStore are declared as input parameters.

TodoStore emits events on every change: eventEmitter.emit(CHANGE_EVENT) . A micro event emitter library MicroEmitter is used.

Bellow is an example of to-do domain data object:

{id : 1, title: "This is a title", userId: 10, completed: false }Copy the code

Data Access Layer

The data service encapsulates the communication with the server API.

TodoDataService

export default function TodoDataService(){    const url = "https://jsonplaceholder.typicode.com/todos";        function toJson(response){      return response.json();    }Copy the code
function get() {      return fetch(url).then(toJson);    }        function add(todo) {      return fetch(url, {        method: "POST",        body: JSON.stringify(todo),      }).then(toJson);    }        return  Object.freeze({      get,      add    });  }Copy the code

Both get and add public methods return a promise.

A promise is a reference to an asynchronous call. It may resolve or fail somewhere in the future.

Application Entry Point

The main.js is the application a single entry point. This is the place where:

  • all objects are created and dependencies injected (aka the Composition Root)
  • all components are created
import React from "react"; import ReactDOM from 'react-dom'; import TodoDataService from "./dataaccess/TodoDataService"; import UserDataService from "./dataaccess/UserDataService"; import TodoStore from "./stores/TodoStore"; import UserStore from "./stores/UserStore"; import TodoContainer from "./components/TodoContainer.jsx"; (function startApplication(){ const userDataService = UserDataService(); const todoDataService = TodoDataService(); const userStore = UserStore(userDataService); const todoStore = TodoStore(todoDataService, userStore); const stores = { todoStore, userStore }; function loadStaticData(){ return Promise.all([userStore.fetch()]); } function mountPage(){ ReactDOM.render( <TodoContainer stores={stores} />, document.getElementById('root')); } loadStaticData().then(mountPage); }) ();Copy the code

Task Runner

All components and factory functions are exported from modules. Gulp with Browserify are used to bundle all modules together. There is only one entry point in the application, the main.js file, so the scripts task, running browserify, contains only this entry.

ESLint is used for linting. All the rules are defined in the .eslintrc.json file.

Execute npm gulp to run the gulp default task. It executes the eslint and then scripts tasks.

A watch task is defined to monitor .js and .jsx file changes and rerun eslint and scripts tasks.

var gulp = require('gulp')var eslint = require('gulp-eslint'); var babelify = require('babelify'); var browserify = require('browserify'); var source = require('vinyl-source-stream'); var distFolder = "./dist"; gulp.task('eslint', function () { gulp.src(["components/*.jsx", "dataaccess/*.js", "stores/*.js", "tools/*.js", "main.js"]) .pipe(eslint()) .pipe(eslint.format()); }); gulp.task('scripts', function () { return browserify({ entries: 'main.js' }) .transform(babelify.configure({ presets : ["es2015", "react"] })) .bundle() .pipe(source('scripts.js')) .pipe(gulp.dest(distFolder)); }); gulp.task('watch', function () { gulp.watch(["components/*.jsx", "dataaccess/*.js", "stores/*.js", "tools/*.js", "main.js"], [ "eslint", "scripts" ]); }); gulp.task( 'default', [ "eslint", "scripts" ] )Copy the code

Testing

The Jest framework is used for testing. All test files will be named with the .test.js suffix. Execute npm test to run all the tests.

The TodoStore takes in all dependencies as parameters. We can mock the TodoDataService and UserStore dependencies and test the todoStore object in isolation.

Below is a test for the todoStore.getBy() method.

import TodoStore from ".. /stores/TodoStore"; test("TodoStore can filter by title text", function() { //arrage const allTodos = [ { id: 1, title : "title 1" }, { id: 2, title : "title 2" }, { id: 3, title : "title 3" } ]; const todoDataService = { get : function(){ return Promise.resolve(allTodos); }}; const userStore = { getById : function(){ return { name : "Test" }; }}; const todoStore = TodoStore(todoDataService, userStore); const query = { text: "title 1" }; const expectedOutputTodos = [ { id: 1, title : "title 1" , userName : "Test"} ]; //act todoStore.fetch().then(function makeAssertions(){ //assert expect(expectedOutputTodos).toEqual(todoStore.getBy(query)); }); });Copy the code

Conclusion

The three layers architecture offers a better separation and understanding of the layer’s purpose.

Components are used to split the page into smaller pieces that are easier to manage and reuse.

The domain store manages the domain state.

Data services communicates with external APIs.

Use is separated from construction. All objects and components are created in the main.js. The rest of the application is designed assuming that all objects have been built.

Check out the full implementation on git-hub.

For more on the JavaScript functional side take a look at:

Discover the power of closures in JavaScript

Discover the power of first class functions

How point-free composition will make you a better functional programmer

How to make your code better with intention-revealing function names

Here are a few function decorators you can write from scratch

Make your code easier to read with Functional Programming