Connect

The data-fetching decorator for Lore

connect

connect is a decorator created to simplify the process of providing data to components.

The basic usage looks like this:

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

connect(function(getState, props, context) {
  return {
    tweets: getState('tweet.find')
  }
})(
  createReactClass({
    propTypes: {
      tweets: PropTypes.object.isRequired
    }
  })
)

And full usage (showing all available options) looks like this:

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

connect(function(getState, props, context) {
  const { user } = context;
  return {
    tweets: getState('tweet.find', {
      where: {
        user: user.id
      }
    }, { forceFetchOnMount: true })
  }
}, {
  contextTypes: {
    user: PropTypes.object.isRequired
  },
  subscribe: true
})(
  createReactClass({
    propTypes: {
      tweets: PropTypes.object.isRequired
    },
    //...
  })
)

And a simplified view of the interface looks like this:

connect(select, options = {})(
  //...component...
)

The decorator accepts two arguments. The first is a function, called select, that controls what data gets passed to the component through props. The second argument is a set of options that affect the behavior of the decorator.

We'll explore each argument in depth below, starting with select.

select

The first argument, select, is a function that controls what data gets passed to the decorated component through props. In the example usage above, it's the function that looks like this:

function select(getState, props, context) {
  return {
    tweets: getState('tweet.find')
  }
}

The select function provides three arguments.

getState

The first argument is getState, which is a function responsible for retrieving data from the reducers, and invoking an action if that data doesn't exist. Typical usage looks like this:

getState('tweet.find', {
  where: {
    user: 1
  }
})

And a simplified view of the interface looks like this (note the third argument):

getState(blueprintKey, params, options)

The getState function accepts 3 arguments.

blueprintKey

The blueprintKey is a string that communicates which blueprint you want to invoke, and each blueprint is responsible for choosing how to use the other arguments.

For example, this example call invokes the find blueprint, and expresses that it wants to use it with the tweet data.

getState('tweet.find')

params

The second argument is params and is an object that each blueprint uses differently based on it's intended purpose.

To illustrate, let's expand on the example usage above, and provide a params object to communicate that we only wants the tweets where id of the user who created them is 1.

getState('tweet.find', {
  where: {
    user: 1
  }
})

Other blueprints will have different behavior, such as the byId blueprint, whose usage looks like this:

getState('tweet.byId', {
  id: 1
})

For that blueprint, there is no where attribute, but it does expect an id attribute.

options

The third argument is options and has the ability to modify the way the blueprint behaves.

To illustrate, let's expand on the example usage above one more time. Normally, as long as the data matching the query exists within the reducers, that data will be provided to the component, and the action will never be invoked a second time.

But sometimes, we might want to refetch the data. For example, let's say everytime the user navigates to the page, that data is re-fetched, guarenteeing that what the user sees is always fresh. This type of behavior would immitate the way a server-side rendered application behaves, and we can achieve that by providing a forceFetchOnMount attribute in the options object like this:

getState('tweet.find', {
  where: {
    user: 1
  }
}, { forceFetchOnMount: true })

How, every time the user navigates back to this page, or this component, the data will be refetched from the API.

props

The second argument for select is props, and is an object containing whatever props were passed to the component connect is decorating.

This is useful when you need to use those props to compose your getState call like this:

connect(function(getState, props, context) {
  const { user } = props;
  return {
    tweets: getState('tweet.find', {
      where: {
        user: user.id
      }
    })
  }
})

context

The third argument for select is context, which is useful when the data you need is not provided through props but is accessible from context. For example, if we want to fetch the tweets for the current user, we would use the context argument like this:

connect(function(getState, props, context) {
  const { user } = context;
  return {
    tweets: getState('tweet.find', {
      where: {
        user: user.id
      }
    })
  }
})
context can not be used correctly without first specifying the contextTypes in the options for connect.

options

The second argument to connect is options, and this object is used to tailor the behavior of connect. Example usage looks like this:

connect(function(getState, props, context) {
  // ...
}, {
  contextTypes: {
    user: PropTypes.object.isRequired
  },
  subscribe: false
})

There are two options available.

contextTypes

Sometimes you may need to provide data stored in context to the select function, to be used when determining what data to retrieve. In that situation, you need to describe what data you want to be included, and you do that through this option.

For example, let's say we want to fetch all the tweets for the current user, who is stored in context as user. We could do that like this:

connect(function(getState, props, context) {
  const { user } = context;
  return {
    tweets: getState('tweet.find', {
      where: {
        user: user.id
      }
    })
  }
}, {
  contextTypes: {
    user: PropTypes.object.isRequired
  }
})

subscribe

Typically, the only component that ever subscribes to changes in the Redux store is the Master component at /src/components/Master.js, which is often the root component of the application.

The reason for this is because the way React behaves means that whenever any component re-renders, all the children under it will also re-render. So if the root component of the application is subscribed to changes in the Redux store, then it guarantees the application will always re-render in it's entirety whenever data changes.

But if you wanted to manually subscribe a compoent to changes in the Redux store, you could do so like this:

connect(function(getState, props, context) {
  // ...data...
}, {
  subscribe: true
})(
 //...component...
)

There are two situations where you may want (or need) to do this:

  1. The first situation involves Dialogs.The practice Lore follows for dialogs renders them outside the main application, when means they aren't part of Master, and thus won't update when data changes. So dialogs need to subscribe to changes in the Redux store (though if you're using a hook like lore-hook-dialog-bootstrap it will happen automatically).
  2. The second situation involves performance optimization. For example, there may be times where (for whatever reason) you want to break the natural render cycle, and do not want a child component to re-render when the parent changes. In this case, you'll need to manually subscribe the child to changes in the Redux store, because you disconnected it from the default behavior.