Quickstart

A quick dive into getting started with Lore

Step 5: Show Optimistic Visual Cues

In this step we'll add an opacity effect to tweets to reflect when they're being created, updated or deleted.

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

What's the problem?

While it's great that we can now show tweets in the Feed immediately, there's currently no visual cue to distinguish between an optimistic tweet and a real tweet, and we're also exposing functionality like the "edit" and "delete" actions that can't be performed until the tweet has a real id.

How do we solve this?

To improve the experience, we're going to visually fade the tweet when it's being created, updated or deleted, and we're not going to show the edit or delete links until we have confirmation that the tweet actually exists.

Add Visual Cue for Optimistic Changes

Update the render() method of the Tweet component to look like this:

// src/components/Tweet.js
import PayloadStates from '../constants/PayloadStates';
...
render() {
  const { tweet, user } = this.props;
  const timestamp = moment(tweet.data.createdAt).fromNow().split(' ago')[0];
  const isOptimistic = (
    tweet.state === PayloadStates.CREATING ||
    tweet.state === PayloadStates.UPDATING ||
    tweet.state === PayloadStates.DELETING
  );

  return (
    <li className={"list-group-item tweet" + (isOptimistic ? " transition" : "")}>
      <div className="image-container">
        <img
          className="img-circle avatar"
          src={user.data.avatar} />
      </div>
      <div className="content-container">
        <h4 className="list-group-item-heading title">
          {user.data.nickname}
        </h4>
        <h4 className="list-group-item-heading timestamp">
          {'- ' + timestamp}
        </h4>
        <p className="list-group-item-text text">
          {tweet.data.text}
        </p>
        <IsOwner tweet={tweet}>
          <div className="tweet-actions">
            <EditLink tweet={tweet} />
            <DeleteLink tweet={tweet} />
          </div>
        </IsOwner>
      </div>
    </li>
  );
}

In the code above, we're examining the state of the tweet, and if it's in the process of being created, updated, or deleted, then we're going to apply the transition class, which will fade the tweet and the hide the actions.

Visual Check-in

If everything went well, your application should now look like this when tweets are being created, updated, or deleted:

Code Changes

Below is a list of files modified during this step.

src/components/Tweet.js

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import moment from 'moment';
import { connect } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import EditLink from './EditLink';
import DeleteLink from './DeleteLink';
import IsOwner from './IsOwner';

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

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

  propTypes: {
    tweet: PropTypes.object.isRequired,
    user: PropTypes.object.isRequired
  },

  render() {
    const { tweet, user } = this.props;
    const timestamp = moment(tweet.data.createdAt).fromNow().split(' ago')[0];
    const isOptimistic = (
      tweet.state === PayloadStates.CREATING ||
      tweet.state === PayloadStates.UPDATING ||
      tweet.state === PayloadStates.DELETING
    );

    return (
      <li className={"list-group-item tweet" + (isOptimistic ? " transition" : "")}>
        <div className="image-container">
          <img
            className="img-circle avatar"
            src={user.data.avatar} />
        </div>
        <div className="content-container">
          <h4 className="list-group-item-heading title">
            {user.data.nickname}
          </h4>
          <h4 className="list-group-item-heading timestamp">
            {'- ' + timestamp}
          </h4>
          <p className="list-group-item-text text">
            {tweet.data.text}
          </p>
          <IsOwner tweet={tweet}>
            <div className="tweet-actions">
              <EditLink tweet={tweet} />
              <DeleteLink tweet={tweet} />
            </div>
          </IsOwner>
        </div>
      </li>
    );
  }

})
);
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { connect } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import EditLink from './EditLink';
import DeleteLink from './DeleteLink';
import IsOwner from './IsOwner';

class Tweet extends React.Component {

  render() {
    const { tweet, user } = this.props;
    const timestamp = moment(tweet.data.createdAt).fromNow().split(' ago')[0];
    const isOptimistic = (
      tweet.state === PayloadStates.CREATING ||
      tweet.state === PayloadStates.UPDATING ||
      tweet.state === PayloadStates.DELETING
    );

    return (
      <li className={"list-group-item tweet" + (isOptimistic ? " transition" : "")}>
        <div className="image-container">
          <img
            className="img-circle avatar"
            src={user.data.avatar} />
        </div>
        <div className="content-container">
          <h4 className="list-group-item-heading title">
            {user.data.nickname}
          </h4>
          <h4 className="list-group-item-heading timestamp">
            {'- ' + timestamp}
          </h4>
          <p className="list-group-item-text text">
            {tweet.data.text}
          </p>
          <IsOwner tweet={tweet}>
            <div className="tweet-actions">
              <EditLink tweet={tweet} />
              <DeleteLink tweet={tweet} />
            </div>
          </IsOwner>
        </div>
      </li>
    );
  }

}

Tweet.propTypes = {
  tweet: PropTypes.object.isRequired,
  user: PropTypes.object.isRequired
};

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

  return {
    user: getState('user.byId', {
      id: tweet.data.user
    })
  };
})(Tweet);
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { connect } from 'lore-hook-connect';
import PayloadStates from '../constants/PayloadStates';
import EditLink from './EditLink';
import DeleteLink from './DeleteLink';
import IsOwner from './IsOwner';

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

  return {
    user: getState('user.byId', {
      id: tweet.data.user
    })
  };
})
class Tweet extends React.Component {

  static propTypes = {
    tweet: PropTypes.object.isRequired,
    user: PropTypes.object.isRequired
  };

  render() {
    const { tweet, user } = this.props;
    const timestamp = moment(tweet.data.createdAt).fromNow().split(' ago')[0];
    const isOptimistic = (
      tweet.state === PayloadStates.CREATING ||
      tweet.state === PayloadStates.UPDATING ||
      tweet.state === PayloadStates.DELETING
    );

    return (
      <li className={"list-group-item tweet" + (isOptimistic ? " transition" : "")}>
        <div className="image-container">
          <img
            className="img-circle avatar"
            src={user.data.avatar} />
        </div>
        <div className="content-container">
          <h4 className="list-group-item-heading title">
            {user.data.nickname}
          </h4>
          <h4 className="list-group-item-heading timestamp">
            {'- ' + timestamp}
          </h4>
          <p className="list-group-item-text text">
            {tweet.data.text}
          </p>
          <IsOwner tweet={tweet}>
            <div className="tweet-actions">
              <EditLink tweet={tweet} />
              <DeleteLink tweet={tweet} />
            </div>
          </IsOwner>
        </div>
      </li>
    );
  }

}

export default Tweet;

Next Steps

In the next section we'll hide tweets that have been deleted.