Connect
The data-fetching decorator for Lore
The data-fetching decorator for Lore
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
.
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.
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.
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')
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.
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.
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
}
})
}
})
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 thecontextTypes
in theoptions
forconnect
.
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.
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
}
})
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:
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).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.