Quickstart

A quick dive into getting started with Lore

Step 3: Alternative Approach

In this step we'll look at an alternative approach to hiding components that doesn't use decorators.

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

Why an alternative approach?

While decorators can provide a concise way to add behavior to an application, they're not very easy to understand compared to a simple React component. Conceptually, they're functions that return a function that return a component that renders YOUR component, which can be difficult to visualize.

In this section, we'll introduce an alternative way of hiding components based on authorization rules, and we'll use a simple component instead of a decorator.

Remove Authorization Decorators from Links

Start by removing the UserCanEditTweet decorator from the EditLink, and removing the UserCanDeleteTweet decorator from the DeleteLink.

Once you do this, the "edit" and "delete" links should be visible for all tweets.

Create an IsOwner Component

Next, create a new component called IsOwner:

lore generate component IsOwner

Then replace the code with this:

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

export default createReactClass({
  displayName: 'IsOwner',

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

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

  render() {
    const { tweet, children } = this.props;
    const { user } = this.context;

    if (tweet.data.user === user.id) {
      return children;
    }

    return null;
  }

});
import React from 'react';
import PropTypes from 'prop-types';

class IsOwner extends React.Component {

  render() {
    const { tweet, children } = this.props;
    const { user } = this.context;

    if (tweet.data.user === user.id) {
      return children;
    }

    return null;
  }

}

IsOwner.propTypes = {
  tweet: PropTypes.object.isRequired
};

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

export default IsOwner;
import React from 'react';
import PropTypes from 'prop-types';

class IsOwner extends React.Component {

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

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

  render() {
    const { tweet, children } = this.props;
    const { user } = this.context;

    if (tweet.data.user === user.id) {
      return children;
    }

    return null;
  }

}

export default IsOwner;

Similar to the decorators we created previously, this component expects to receive a tweet as a prop, along with the user from context.

In the render() method, we compare the current user to the user who created the tweet. If they match, we render whatever children (other components) were provided. If they don't, we render nothing.

Use the Component

To see how we use this it, open the Tweet component. Import IsOwner, and wrap our edit and delete links with it like this:

// src/components/Tweet.js
...
import IsOwner from './IsOwner';
...
  render() {
    ...
      <IsOwner tweet={tweet}>
        <div className="tweet-actions">
          <EditLink tweet={tweet} />
          <DeleteLink tweet={tweet} />
        </div>
      </IsOwner>
    ...
  }
...

With this change in place, refresh the page, and once again, the "edit" and "delete" links should only be visible on tweets created by the current user.

Visual Check-in

If everything went well, your application should now look like this.

Code Changes

If you chose to follow, below is a list of files modified during this step.

src/components/IsOwner.js

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

export default createReactClass({
  displayName: 'IsOwner',

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

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

  render() {
    const { tweet, children } = this.props;
    const { user } = this.context;

    if (tweet.data.user === user.id) {
      return children;
    }

    return null;
  }

});
import React from 'react';
import PropTypes from 'prop-types';

class IsOwner extends React.Component {

  render() {
    const { tweet, children } = this.props;
    const { user } = this.context;

    if (tweet.data.user === user.id) {
      return children;
    }

    return null;
  }

}

IsOwner.propTypes = {
  tweet: PropTypes.object.isRequired
};

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

export default IsOwner;
import React from 'react';
import PropTypes from 'prop-types';

class IsOwner extends React.Component {

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

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

  render() {
    const { tweet, children } = this.props;
    const { user } = this.context;

    if (tweet.data.user === user.id) {
      return children;
    }

    return null;
  }

}

export default IsOwner;

src/components/EditLink.js

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

export default createReactClass({
  displayName: 'EditLink',

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

  onClick() {
    const { tweet } = this.props;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  },

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';

class EditLink extends React.Component {

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

  onClick() {
    const { tweet } = this.props;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  }

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

}

EditLink.propTypes = {
  tweet: PropTypes.object.isRequired
};

export default EditLink;
import React from 'react';
import PropTypes from 'prop-types';

class EditLink extends React.Component {

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

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

  onClick() {
    const { tweet } = this.props;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  }

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

}

export default EditLink;

src/components/DeleteLink.js

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

export default createReactClass({
  displayName: 'DeleteLink',

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

  onClick() {
    const { tweet } = this.props;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.destroy(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.destroy(tweet).payload;
        }
      });
    });
  },

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        delete
      </a>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';

class DeleteLink extends React.Component {

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

  onClick() {
    const { tweet } = this.props;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.destroy(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.destroy(tweet).payload;
        }
      });
    });
  }

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        delete
      </a>
    );
  }

}

DeleteLink.propTypes = {
  tweet: PropTypes.object.isRequired
};

export default DeleteLink;
import React from 'react';
import PropTypes from 'prop-types';

class DeleteLink extends React.Component {

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

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

  onClick() {
    const { tweet } = this.props;

    lore.dialog.show(function() {
      return lore.dialogs.tweet.destroy(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.destroy(tweet).payload;
        }
      });
    });
  }

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        delete
      </a>
    );
  }

}

export default DeleteLink;

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 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];

    return (
      <li className="list-group-item tweet">
        <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 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];

    return (
      <li className="list-group-item tweet">
        <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 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];

    return (
      <li className="list-group-item tweet">
        <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 learn how to display new tweets at the top of the Feed.