Skip to content

Client vs Server state

Henri Schumacher edited this page Apr 2, 2015 · 7 revisions

One of the more difficult mindsets is how you combine state that is collected from the server with state that is related to your UI. An example of this would be todos. Lets say you fetch todos from the server. While this is happening you want to indicate in the UI that a fetching of todos is pending, and if something goes wrong it should display an error with the possibility to try again. Also, when you add a new todo you want to do an optimistic update that tells the UI that the todo is in pending state. If the save is successful you want to indicate that, and if it is an error you also want that indicated. So how would you handle this with a Baobab tree?

As we know we need a tree and an event hub.

tree.js

var Baobab = require('baobab');

var tree = new Baobab({
  todos: [],
  isPending: false,
  error: null
});

module.exports = tree;

events.js

var Emmett = require('emmett');
module.exports = new Emmett();

Let us start with our fetching of todos:

fetchTodos.js

var tree = require('./tree.js');
var ajax = require('ajax'); // Some promise based ajax API
module.exports = function() {
  tree.merge({
    isPending: true,
    error: null
  });
  return ajax.get('/todos')
    .success(function (todos) {
      tree.merge({
        todos: todos,
        isPending: false
      });
    })
    .error(function (error) {
      tree.merge({
        error: error,
        isPending: false
      });
    });
};

We most certainly want to start loading these todos immediately, but if an error occurs we want to be able to retry, so we have to hook this up to an event.

main.js

var events = require('./events');
var fetchTodos = require('./fetchTodos.js');

events.on('fetchTodos', fetchTodos);

events.emit('fetchTodos');

Now, this was an easy one. We could easily put UI state, isPending and error, as properties on the tree and the server state, todos, as an other property. But what happens when we try to add a new todo?

The todos being fetched now has the structure: { id: '123', title: 'foo', completed: false }. We have to add some more properties to indicate if it is pending or has an error.

addTodo.js

var tree = require('./tree.js');
var ajax = require('ajax'); // Some promise based ajax lib
var utils = require('./utils.js');

var addTodo = function (title) {

  // Create the client version of the object with
  // a temporary ID for reference. We also add
  // two extra properties which the UI needs
  var todo = {
    id: utils.createId(),
    title: title,
    completed: false,
    isPending: true,
    error: null
  };

  // Optimistic update is done to the todos
  // array, instantly displaying it
  tree.select('todos').unshift(todo);

  // Since we want to reference our todo later, we
  // create a cursor pointing to it
  var todoCursor = tree.select('todos', {id: todo.id});

  // Now we post the data the server actually requires
  return ajax.post('/todos', {
    title: todo.title,
    completed: todo.completed
  })

  // On success we add the returned ID and
  // toggle the pending state
  .success(function (todoId) {
    todoCursor.merge({
      id: todoId,
      isPending: false
    });
  })

  // On error we turn off the pending
  // state and attach the error
  .error(function (error) {
    todoCursor.merge({
      isPending: false,
      error: error
    });
  });
};

module.exports = addTodo;

And lets hook up the event:

main.js

var events = require('./events');
var fetchTodos = require('./fetchTodos.js');
var addTodo = require('./addTodo.js');

events.on('fetchTodos', fetchTodos);
events.on('addTodo', addTodo);

events.emit('fetchTodos');

So what we did here was to combine server state and UI state. This is perfectly okay because we control what is going to the server and what we make available to the client. When we do not abstract simple concepts like objects and arrays into complex abstractions, like a Model and a Collection, the code is a lot easier to read and understand. Now our components has gotten the following functionality:

  • Show loading indication while fetching todos
  • Show error when fetching of todos fail
  • Fetch todos
  • Read todos
  • Add new todo
  • Show loading indication when saving new todo
  • Show error when saving of new todo failed

This is some of the more complex scenarios we face as developers, but the state tree and the event hub creates this "sweet spot" of decoupling and scalability.