Features

Key features that make up the main value proposition for Lore

Optimistic Updates

Useful for providing the application with a feeling a snappy responsiveness by assuming server calls will be successful.

Visualization

This video demonstrates what optimistic updates looks like. Screenshots are from the Simply Social prototype that Invision provides you when you sign up for an account.

Usage

While optimistically updating data can provide a good user experience, implementing it can be a little tricky, as you are adding data to an application before it exists, and then you need to sync that data with a server response when it comes back. If done incorrectly, you can easily end up with duplicate data in your application, and you may end up seeing the optimistic data being displayed next to the real data instead of being merged with it.

Let's go through an example to illustrate the issue to demonstrate how Lore addresses it.

Let's say you have an API with a /posts endpoint, and you want to create a new post. Somewhere in your application you'll make a call to the post.create action like this:

lore.action.post.create({
  title: 'Cornbread is Yummy'
})

This will then send a network request to the API server that looks like this:

POST http://api.example.com/posts

{
  title: 'Cornbread is Yummy'
}

Now while that network request is in flight, the create blueprint is still going to fire action to the Redux store describing the data the user just "created". That action will look like this:

action = {
  type: 'ADD_POST',
  payload: {
    id: undefined,
    cid: 'c1',
    state: 'CREATING',
    data: {
      title: 'Cornbread is Yummy'
    },
    error: {}
  }
}

The action.type field tells the reducers that we need to ADD this POST to the Redux store. Once that happens, the application will rerender. Now let's say we have a component renders a list of posts:

@connect(function(getState, props) {
  return {
    posts: getState('post.find')
  };
})
class Component extends React.Component {

  static propTypes = {
    posts: React.PropTypes.object.isRequired
  };

  renderPost: function(post) {
    return (
      <Post key={post.id} post={post} />
    );
  },

  render() {
    var posts = this.props.posts;

    return (
      <ul>
        {posts.data.map(this.renderPost)}
      </ul>
    )
  }
};

One problem we'll hit quickly is that React will throw a warning because post.id is undefined, which means the key won't exist. It won't necessary damage the user experience, but we do want to fix React warnings. This is one use for the cid field; acting as a fallback when the id doesn't exist. So if we change our renderPost to this the warning will go away (we'll use the cid if the id is undefined):

@connect(function(getState, props) {
  return {
    posts: getState('post.find')
  };
})
class Component extends React.Component {

  static propTypes = {
    posts: React.PropTypes.object.isRequired
  };

  renderPost: function(post) {
    return (
      <Post key={post.id || post.cid} post={post} />
    );
  },

  render() {
    var posts = this.props.posts;

    return (
      <ul>
        {posts.data.map(this.renderPost)}
      </ul>
    )
  }
};

Now that's a problem but it's not the problem. The problem happens when the real data comes back from the server. Let's pretend this is the response from the API server:

201 https://api.myapp.com/posts

{
  id: 1,
  title: 'Cornbread is Yummy'
}

The issue is this: the create method needs to emit an action to populate the Redux store with server response, but how do we let the reducers know this is the same data? Normally an id is used to anchor data, to say this and that are the same thing. But we added our optimistic data before the id existed...so what do we do?

The answer lies in the cid field Lore adds to all data that gets fetched or created. To illustrate let's take a look at a simplified blueprint for the create method:

module.exports = function create(params) {
  return function(dispatch) {
    var model = new Post(params);

    model.save().then(function() {
      dispatch({
        type: ActionTypes.UPDATE_POST,
        payload: payload(model, PayloadStates.RESOLVED)
      });
    }).catch(function(response) {
      var error = response.data;

      dispatch({
        type: ActionTypes.UPDATE_POST,
        payload: payload(model, PayloadStates.ERROR_CREATING, error)
      });
    });

    return dispatch({
      type: ActionTypes.ADD_POST,
      payload: payload(model, PayloadStates.CREATING)
    });
  };
};

See the call to create the Post at the top, that looks like var model = new Post(params)? The Post constructor there is an instance of lore-models, the interface and behavior of which is heavily inspired by Backbone's Models and Collections. Once the post instance is created, the data for the resulting model looks like this:

model = {
  id: undefined,
  cid: 'c1',
  attributes: {
    title: 'Cornbread is Yummy'
  }
}

So the cid is actually created when the Post model is created. These models are used as the AJAX abstraction tier for Lore, and when we invoke model.save() the network request is sent to the API server to create the Post resource. The dispatch call at the bottom of the create method creates and dispatches the action containing the optimistic data.

Once the network request comes back, the promise after model.save() is fulfilled, and we dispatch another action. But this time the data in the model looks like this:

model = {
  id: 1,
  cid: 'c1',
  attributes: {
    id: 1,
    title: 'Cornbread is Yummy'
  }
}

The trick here is that the _same function_ dispatches both the optimistic update AND the update once real data exists. Because of this, we're able to hold onto the original cid, and our second action will look like this:

action = {
  type: 'UPDATE_POST',
  payload: {
    id: 1,
    cid: 'c1',
    state: 'RESOLVED',
    data: {
      id: 1,
      title: 'Cornbread is Yummy'
    },
    error: {}
  }
}

Now the reducers in the Redux store know they should UPDATE the POST, and because all data fetched or created will have a cid the reducers can match data based on cid if the id doesn't exist. And that's exactly what happens. The first ADD_POST action is added to the Redux store, and the store can tell it's missing an id. So when the second UPDATE_POST action is dispatched later, the reducers can use to cid field to know what data is the same and merge it together.

It's important to re-iterate that this method only works because the optimistic and real responses are part of the same method context. This approach DOES NOT work for WebSockets, because there is no equivalent "wait for server response" option. Instead, the cid needs to be sent to the server and included _in the response_. Provided you do that, you can handle optimistic updates using WebSockets without any issues, and the same principle to add and update/merge the data applies.