Quickstart

A quick dive into getting started with Lore

Step 4: Add Optimistic Values

In this step fix the error and restore functionality to the application.

You can view the finished code for this step by checking out the optimistic.4 branch of the completed project.

What's the problem?

Currently when you create a tweet, this error appears in the console:

Invalid call to "getState('user.byId')". Missing required attribute "id".

Why is this happening?

This error is happening because we're trying to render a tweet that doesn't have a user property yet. The code throwing the error is the connect decorator in the Tweet, shown below:

export default connect(function(getState, props) {
  const tweet = props.tweet;

  return {
    user: getState('user.byId', {
      id: tweet.data.user
    })
  };
})
export default connect(function(getState, props) {
  const tweet = props.tweet;

  return {
    user: getState('user.byId', {
      id: tweet.data.user
    })
  };
})(Tweet);
@connect(function(getState, props) {
  const tweet = props.tweet;

  return {
    user: getState('user.byId', {
      id: tweet.data.user
    })
  };
})

When we create data, the API will set the user property to the user who created it, and the createdAt date to the timestamp of when the tweet was saved to the database.

But currently, we're rendering this data before those fields are assigned, which means tweet.data.user is undefined.

How do we solve this?

There are three ways we can solve this:

  1. We could add logic to the Tweet component so that it behaves differently when no user field exists, like showing a generic avatar photo.
  2. We could create a new component called OptimisticTweet, specifically designed for rendering partial data, and show that instead of Tweet when appropriate.
  3. Since we know what values the API will assign to the missing properties, we can simply add them, and make it so that even optimistic tweets can be fully rendered.

We're going to go with the third option, and add the missing fields ourselves.

Add Optimistic Values

Open the CreateButton component, import the user from context, and update the onClick() callback to look like this:

  // src/components/CreateButton.js
  import _ from 'lodash';
  ...
  export default createReactClass({
    displayName: 'CreateButton',

    contextTypes: {
      user: PropTypes.object.isRequired
    },

    onClick() {
      const { user } = this.context;

      lore.dialog.show(function() {
        return lore.dialogs.tweet.create({
          blueprint: 'optimistic',
          request: function(data) {
            return lore.actions.tweet.create(_.defaults({
              user: user.id,
              createdAt: new Date().toISOString()
            }, data)).payload;
          }
        });
      });
    },

    ...

  });
// src/components/CreateButton.js
import _ from 'lodash';
...
class CreateButton extends React.Component {
  ...
  onClick() {
    const { user } = this.context;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.create({
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.create(_.defaults({
            user: user.id,
            createdAt: new Date().toISOString()
          }, data)).payload;
        }
      });
    });
  }
  ...
}

CreateButton.contextTypes = {
  user: PropTypes.object.isRequired
};

export default CreateButton;
// src/components/CreateButton.js
import _ from 'lodash';
...
class CreateButton extends React.Component {

  static contextTypes = {
    user: PropTypes.object.isRequired
  };

  ...

  onClick() {
    const { user } = this.context;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.create({
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.create(_.defaults({
            user: user.id,
            createdAt: new Date().toISOString()
          }, data)).payload;
        }
      });
    });
  }
  ...
}

export default CreateButton;

Since we know the tweet is being created by the current user, the first thing we do is get the user from context. Then, instead of passing data directly to the create action, we're setting the user and createdAt properties to what we know they'll be after the API request returns.

To be clear, we're not actually assigning values to these fields, in the sense of telling the API what they should be.

While these fields will be included the body of the PUT request, the API will ignore them, and set user based on the API token, and createdAt based on the timestamp of when the tweet was saved to the database.

All we're doing is here providing values that will allow the tweet to be rendered correctly in the interim, between the time when the request is sent, and when it comes back from the server with official values.

Try it Out

If you now refresh the browser, and create a tweet, you'll notice the application not only works again, but that the Tweet immediately appears in the Feed.

Visual Check-in

If everything went well, your application should now look like this (exactly the same).

Code Changes

Below is a list of files modified during this step.

src/components/CreateButton.js

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';

export default createReactClass({
  displayName: 'CreateButton',

  contextTypes: {
    user: PropTypes.object.isRequired
  },

  onClick() {
    const { user } = this.context;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.create({
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.create(_.defaults({
            user: user.id,
            createdAt: new Date().toISOString()
          }, data)).payload;
        }
      });
    });
  },

  render() {
    return (
      <button
        type="button"
        className="btn btn-primary btn-lg create-button"
        onClick={this.onClick}>
        +
      </button>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import CreateTweetDialog from './CreateTweetDialog';

class CreateButton extends React.Component {

  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

  onClick() {
    const { user } = this.context;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.create({
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.create(_.defaults({
            user: user.id,
            createdAt: new Date().toISOString()
          }, data)).payload;
        }
      });
    });
  }

  render () {
    return (
      <button
        type="button"
        className="btn btn-primary btn-lg create-button"
        onClick={this.onClick}>
        +
      </button>
    );
  }

}

CreateButton.contextTypes = {
  user: PropTypes.object.isRequired
};

export default CreateButton;
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import CreateTweetDialog from './CreateTweetDialog';

class CreateButton extends React.Component {

  static contextTypes = {
    user: PropTypes.object.isRequired
  };

  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

  onClick() {
    const { user } = this.context;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.create({
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.create(_.defaults({
            user: user.id,
            createdAt: new Date().toISOString()
          }, data)).payload;
        }
      });
    });
  }

  render () {
    return (
      <button
        type="button"
        className="btn btn-primary btn-lg create-button"
        onClick={this.onClick}>
        +
      </button>
    );
  }

}

export default CreateButton;

Next Steps

In the next section we'll add a visual cue when tweets are being created, updated or deleted.