React Connect
State management agnostic decorator for loosely coupled React containers
or
Better way to connect React component to an arbitrary store
or
Alternative to react-redux
, mobx-react
and similar packages
Motivation
Let's start with redux
containers
A typical redux
container looks something like this:
const Container = connect(
state => ({
someProps: state.someState
}),
dispatch => ({
onClick() {
dispatch({type: 'SOME_ACTION'});
}
})
)(props => (
<div onClick={props.onClick}>{props.someProps}</div>
));
Note that you tightly coupled your component with a redux
store. Even if you used selectors which hide store internals, these selectors expect that state object of certain shape will always be provided. If some day you decide to switch to mobx
, or you'll just want to use the same container in different redux
app, you might have a problem.
Not a problem! - you say, since you still can export raw, stateless component itself, or even get access to it via Container.WrappedComponent
, where it is always exposed.
That is true, but in complex applications, especially those trying to improve their rendering performance, you will often find yourself making large number of small containers, which themselves will be rendered inside other containers. In fact, to efficiently render list of elements in redux
, the best way is to make each element of a list container itself:
export const List = ({list}) => (
<ol>
{list.map(elementId => {
// Element is a redux container!
return (<Element id={elementId} key={elementId} />);
})}
</ol>
);
Note that even though List
is exported as a pure component, at this point it is impossible to render it without providing redux
store in context. As was mentioned before, this component is not even tied to redux
library in general, but to a state object of very specific shape.
Generally speaking, from now on List
component will not be usable in any other application, even redux
one.
This is very bad place to be. React promises deep componentization - if I created List
of Element
components once, I should not have to write it again ever. I should be able to just take it and render it in any other application, whether it's using redux
, mobx
or any other state management solution (even totally custom one). I should be able to create small, deeply nested containers to not take performance and readability penalty of having just one, big container at the root of the app, while still having my containers not tied to any specific data structure or library and being able to share them between projects without any additional work (this is what we mean, when we say "components" after all).
react-connect
allows to do just that. It preserves the notion of container, but with a twist. Here is redux
container from first snippet, rewritten to react-connect
:
import { container } from 'react-connect';
const Container = container('Container', props => (
<div onClick={props.onClick}>{props.someProps}</div>
));
It actually looks very much like regular component. So how you feed it data? Where did these redux
mappers go?
Let's say I want to connect this container to redux
store. What I need is a link
between store and my container:
import { reduxLink } from 'react-connect';
const containerLink = reduxLink({
mapStateToProps: (state) => ({
someProps: state.someState
}),
mapDispatchToProps: (dispatch) => ({
onClick() {
dispatch({type: 'SOME_ACTION'});
}
})
});
Nothing new here. Just our redux
mappers, which we wrapped in reduxLink
function. This function returns what we call a link. Note that this link might land in the same file as our container, but it is probably good idea to keep it somewhere else (say in links
folder, where each link will be in a file matching name of container that it provides data for).
This way even though we defined how to connect container to store, container is still free of any dependencies to this store or even redux
in general. We can write multiple links to the very same container and choose relevant one when we render our app.
Let's do just that:
import { Links, Provider } from 'react-connect';
const store = createStore(someReducer);
const links = new Links();
links.addLink(Container, containerLink);
render(
<Provider links={links} context={{store}} >
<Container />
</Provider>
)
This again looks similar to how you would render redux app. Novelty is Links
object via which you specify which container will be fed data with which links. You then use Provider
, where you put defined links and - in case of redux
app - you provide store in context.
Bear in mind that Provider
and Links
themselves still have no idea that you make redux app. This elements will not change when rendering mobx
or any other map. Sometimes you will just put something else in context (if it's needed). The real change is writing and applying other links to a component.
Feeding container other data
So did we achieve anything? We had to do a little bit of extra work and our app got more complex with yet another concept of links and additional folder with them.
Let's start with the simplest case. Let's just render container with some static data. We might need it for an entry in storybook (just to see how it looks during styling) or for snapshot tests.
As was said earlier our List
with Element
components was impossible to render without redux
store in context. Let's rewrite it to react-connect
and try render it with some static data:
const Element = container('Element', props => (
<li>{props.elementName}</li>
));
const List = container('List', props => (
<ol>
{list.map(elementId => <Element id={elementId} key={elementId} />)}
</ol>
));
Before we used reduxLink
to connect container with data. But we can simply pass data as an object:
links
.addLink(Element, { elementName: 'some name' })
.addLink(List, { list: [1, 2, 3, 4] });
Because every container can accept links - just like Provider
- you can now render List
:
render(<List links={links} />);
This will result in following markup:
<ol>
<li>some name</li>
<li>some name</li>
<li>some name</li>
<li>some name</li>
</ol>
Because we passed static props as a link to Element
, every component has the same content. What if we wanted to render real list?
You can provide function instead of static object. Function will receive own props with which component was rendered. In case of Element
- id
prop. We can use this id to access real data from list:
const list = ['first name', 'second name', 'third name'];
links
.addLink(List, { list: list.map((_, id) => id) })
.addLink(Element, ({id}) => ({ elementName: list[id] }));
List
component gets a list of element ids, which are then used to retrieve data from the list.
This results in a markup:
<ol>
<li>first name</li>
<li>second name</li>
<li>third name</li>
</ol>
So we rendered container without the need for redux
store, or any kind of high concept state management library for that matter.