Connect

The data-fetching decorator for Lore

Connect

This is a component that emulates the behavior of connect, but is intended to be used as a component within the render function, and not as a decorator.

Example usage looks like this:

import { Connect } from 'lore-hook-connect';

export default createReactClass({
  render() {
    return (
      <Connect callbacks={(getState, props) => {
        return {
          tweets: getState('tweet.find')
        }
      }}>
      {(props) => {
        const { tweets } = props;

        return (
          <ul>
          {tweets.data.map(function(tweet) {
            return (
              <li>{tweet.data.text}</li>
            );
          })}
          </ul>
        );
      }}
      </Connect>
    );
  }
})

When is this useful?

The primary motivation for creating this component grew from a repeated need to render many-to-many data.

An example to illustrate:

Let's say we have an API with the following data:

{
  tweets: [
    {
      id: 1,
      user: 1,
      text: '@lore is a React framework'
    }
  ],
  users: [
    {
      id: 1,
      username: 'jchansen'
    },
    {
      id: 2,
      username: 'lore'
    }
  ],
  mentions: [
    {
      id: 1,
      tweet: 1,
      user: 2
    }
  ]
}

This data describes two users on Fake Twitter (jchansen and lore) and one tweet by jchansen where he mentions @lore by saying @lore is a React framework.

In this example, we want to render all tweets where @lore is mentioned, using the component below.

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { connect } from 'lore-hook-connect';

export default createReactClass({
  displayName: 'Tweet',

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

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

    return (
      <li>{tweet.data.text}</li>
    );
  }
}))

If this example, if we use only the connect decorator, we will need two components, mocked out below.

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { connect } from 'lore-hook-connect';
import Mention from './Mention';

export default connect((getState, props) => {
  return {
    mentions: getState('mention.find', {
      where: {
        user: 2
      }
    })
  }
})(
createReactClass({
  displayName: 'Mentions',

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

  render() {
    const { mentions } = this.props;

    return (
      <ul>
        {mentions.data.map((mention) => {
          return (
            <Mention
              key={mention.id}
              mention={mention}
            />
          )
        })}
      </ul>
    );
  }
}))
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { connect } from 'lore-hook-connect';
import Tweet from './Tweet';

export default connect((getState, props) => {
  const { mention } = props;

  return {
    tweet: getState('tweet.byId', {
      id: mention.data.tweet
    })
  }
})(
createReactClass({
  displayName: 'Mention',

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

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

    return (
      <Tweet tweet={tweet} />
    );
  }
}))

The problem (or at least the motivation for Connect) comes from the fact that the Mention component is really just transforming a mention into a tweet. In other words, we created a component just to transform data.

As a one-off, it's not too annoying, but in an application with a lot of many-to-many endpoints like mentions, it can start to feel like a lot of tedious boilerplate, as a great many of these "transform" components are required.

To illustrate the value of Connect, let's do that same example again, but this time we'll do it one component.

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { connect, Connect } from 'lore-hook-connect';
import Tweet from './Tweet';

export default connect((getState, props) => {
  return {
    mentions: getState('mention.find', {
      where: {
        user: 2
      }
    })
  }
})(
createReactClass({
  displayName: 'Mentions',

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

  render() {
    const { mentions } = this.props;

    return (
      <ul>
        {mentions.data.map((mention) => {
          return (
            <Connect key={mention.id} callback={(getState) => {
              return {
                tweet: getState('tweet.byId', {
                  id: mention.data.tweet
                })
              }
            }}>
              {(props) => {
                const { tweet } = props;
                return (
                  <Tweet tweet={tweet} />
                );
              }}
            </Connect>
          );
        })}
      </ul>
    );
  }
}))

In this example, it's just simpler. We create a component called Mentions intended to render each Tweet that mentions @lore (the user with the id of 2). And using the Connect component, we're able to do whatever transformations we need inside the render method, and return a Tweet when we're ready.

Now if we no longer need to render mentions, we can delete just this one file, instead of two, and the logic is (arguable) easier to understand as well, since we don't need to jump through multiple components to understand how a mention became a tweet.