Connect

The data-fetching decorator for Lore

Introduction

This section describes how to use connect, a decorator that greatly simplifies the process of requesting data from a REST API. It is designed to fetch data from the local cache if it exists, or automatically fetch it from the API if it doesn't.

The connect decorator is used throughout the Quickstart and looks like this:

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

connect(function(getState, props) {
  return {
    tweets: getState('tweet.find')
  }
})(
createReactClass({
  //...
})
)

What problem does it solve?

Whenever you create a model in Lore, the conventions will automatically create a set of actions and reducers to support basic CRUD operations. The actions know how to fetch data from the API, and the reducers know how to store it.

The problem(s) connect solves for lie in the orchestration of when to fetch data from the API.

Applications that consume REST APIs often cache data on the client-side in order to avoid making excessive API calls. This means the application follows a process like "only fetch data if you don't already have it".

Managing this logic means every component that needs data also needs to follow the steps below before requesting it:

  • Check if the data already exists in local cache
  • If it does, retrieve it and use it
  • If it doesn't, invoke the action to fetch that data

At first, that may not seem very complex to manage, but there's another check it gets a little trickier when we recall that React will re-render the application everytime data changes. And At first that may not seem very complex to manage, but consider the following scenario:

  1. This check is performed every time a component is rendered.
  2. Multiple components may request data (triggering multiple API calls)
  3. Some of those components may request the same data (triggering some duplicate API calls)
  4. Each time data from any API call comes back, the application will re-render (because that's what React is designed to do)
  5. Some of those components won't have the data they need yet, but will perform the check again (which will trigger additional API calls, for data that was already requested)
  6. Eventually, all the API calls will return, and the application will render in a final state (and make no more API calls)

This scenario highlights an issue; if guards aren't in place to know when data has already been requested, then there's the potential to make an API request every time a component renders, which can result in multiple API requests for the same data.

Additionally, depending on the number of requests and the rate that components are updated, this also has the potential to not only severely degrade the usability of your application, but to also apply unnecessary load to the API, which may in turn have negative side-effects like hitting rate limits or increased hosting costs for the application.

So something else we need include in our check is that actions should only be invoked if the API call to fetch that data is not already in-flight.

The connect decorator not only automatically performs this check and applies this guard, but also provides a means of breaking through it, for times when you need specialized control over the fetching logic, such as if you wanted to force data to be re-fetched every time a component was mounted.

How does it work?

Whenever you create a model in Lore, the conventions will automatically create a set of actions and reducers to support basic CRUD operations for fetching and storing data from the server.

For example, if you create a model called tweet, the framework creates the following actions:

  • tweet.create
  • tweet.destroy
  • tweet.update
  • tweet.find
  • tweet.get

And it also creates the following reducers:

  • tweet.find
  • tweet.byId
  • tweet.byCid

What the connect decorator needs next is a map that defines, when a component requests data, which reducer it should look in for that data, and also what action should be invoked if it doesn't find the data (so that we can retrieve it).

That map looks like this:

getStatereduceraction
tweet.findtweet.findtweet.find
tweet.byIdtweet.byIdtweet.get

To better understand the map, let's look at a typical usage example. Here we want to retrieve a list of tweets from the server.

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

connect(function(getState, props) {
  return {
    tweets: getState('tweet.find')
  }
})(createReactClass({ /*...*/ }))

The first thing we need is a way to describe the data we want. We do this using the getState function, and our description is the string we pass as the first argument; tweet.find. This string is the first column in the table (getState).

Next, the getState function needs to know which reducer to look for the data in. That's the second colum: reducer. From the table, we can see passing tweet.find to getState will cause it to look in the tweet.find reducer for the data (which in this case represents a query that includes no filter or pagination information).

Next, if no data exist in the reducer, we need to know which action to invoke to fetch it. That's what the third column is; the name of the action that should be invoked. In this case, if the data we want isn't found in the tweet.find reducer, then the tweet.find action will be invoked to fetch it.

Now while that example does use the same name for the getState, reducer, andaction, that's not always the case. If we were to request a specific tweet for example, we would use the follow getState call:

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

connect(function(getState, props) {
  return {
    tweets: getState('tweet.byId', {
      id: 1
    })
  }
})(createReactClass({ /*...*/ }))

In this example (and referencing the table) the tweet.byId string will cause the getState method to inspect the tweet.byId reducer, and invoke the tweet.get action that tweet has not yet been fetched.