react-connect
TypeScript icon, indicating that this package has built-in type declarations

0.4.0 • Public • Published

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); // Just regular redux store here.
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.

Package Sidebar

Install

npm i react-connect

Weekly Downloads

5

Version

0.4.0

License

ISC

Last publish

Collaborators

  • mpodlasin