react-komposer-fork

Compose React containers and feed data into components.

Usage no npm install needed!

<script type="module">
  import reactKomposerFork from 'https://cdn.skypack.dev/react-komposer-fork';
</script>

README

react-komposer

Let's compose React containers and feed data into components.
(supports ReactNative as well)

TOC

Why?

Lately, in React we tried to avoid states as possible we can and use props to pass data and actions. So, we call these components Dumb Components or UI components.

And there is another layer of components, which knows how to fetch data. We call them as Containers. Containers usually do things like this:

  • Request for data (invoke a subscription or just fetch it).
  • Show a loading screen while the data is fetching.
  • Once data arrives, pass it to the UI component.
  • If there is an error, show it to the user.
  • It may need to refetch or re-subscribe when props changed.
  • It needs to cleanup resources (like subscriptions) when the container is unmounting.

If you want to do these your self, you have to do a lot of repetitive tasks. And this is good place for human errors.

Meet React Komposer

That's what we are going to fix with this project. You simply tell it how to get data and clean up resources. Then it'll do the hard work you. This is a universal project and work with any kind of data source, whether it's based Promises, Rx.JS observables or even Meteor's Tracker.

Installation

npm i --save react-komposer

Basic Usage

Let's say we need to build a clock. First let's create a component to show the time.

const Time = ({time}) => (<div>Time is: {time}</div>);

Now let's define how to fetch data for this:

const onPropsChange = (props, onData) => {
  const handle = setInterval(() => {
    const time = (new Date()).toString();
    onData(null, {time});
  }, 1000);

  const cleanup = () => clearInterval(handle);
  return cleanup;
};

On the above function, we get data for every seconds and send it via onData. Additionally, we return a cleanup function from the function to cleanup it's resources.

Okay. Now it's time to create the clock:

import { compose } from 'react-komposer';
const Clock = compose(onPropsChange)(Time);

That's it. Now render the clock to the DOM.

import ReactDOM from 'react-dom';
ReactDOM.render(<Clock />, document.body);

See this in live: https://jsfiddle.net/arunoda/jxse2yw8

Additional Benefits

Other than main benefits, now it's super easy to test our UI code. We can easily do it via a set of unit tests.

  • For that UI, simply test the plain react component. In this case, Time (You can use enzyme for that).
  • Then test onPropsChange for different scenarios.

API

You can customize the higher order component created by compose in few ways. Let's discuss.

Handling Errors

Rather than showing the data, something you need to deal with error. Here's how to use compose for that:

const onPropsChange = (props, onData) => {
  // oops some error.
  onData(new Error('Oops'));
};

Then error will be rendered to the screen (in the place where component is rendered). You must provide a JavaScript error object.

You can clear it by passing a some data again like this:

const onPropsChange = (props, onData) => {
  // oops some error.
  onData(new Error('Oops'));

  setTimeout(() => {
    onData(null, {time: Date.now()});
  }, 5000);
};

Detect props changes

Some times can use the props to custom our data fetching logic. Here's how to do it.

const onPropsChange = (props, onData) => {
  const handle = setInterval(() => {
    const time = (props.timestamp)? Date.now() : (new Date()).toString();
    onData(null, {time});
  }, 1000);

  const cleanup = () => clearInterval(handle);
  return cleanup;
};

Here we are asking to make the Clock to display timestamp instead of a the Date string. See:

ReactDOM.render((
  <div>
    <Clock timestamp={true}/>
    <Clock />
  </div>
), document.body);

See this in live: https://jsfiddle.net/arunoda/7qy1mxc7/

Change the Loading Component

const MyLoading = () => (<div>Hmm...</div>);
const Clock = compose(onPropsChange, MyLoading)(Time);

This custom loading component receives all the props passed to the component as well. So, based on that, you can change the behaviour of the loading component as well.

Change the Error Component

const MyError = ({error}) => (<div>Error: {error.message}</div>);
const Clock = compose(onPropsChange, null, MyError)(Time);

Compose Multiple Containers

Sometimes, we need to compose multiple containers at once, in order to use different data sources. Checkout following examples:

const Clock = composeWithObservable(composerFn1)(Time);
const MeteorClock = composeWithTracker(composerFn2)(Clock);

export default MeteorClock;

For the above case, we've a utility called composeAll to make our life easier. See how to use it:

export default composeAll(
  composeWithObservable(composerFn1),
  composeWithTracker(composerFn2)
)(Time)

Pure Containers

react-komposer checks the purity of payload, error and props and avoid unnecessary render function calls. That means we've implemented shouldComponentUpdate lifecycle hook and follows something similar to React's shallowCompare.

If you need to turn this functionality you can turn it off like this:

// You can use `composeWithPromise` or any other compose APIs
// instead of `compose`.
const Clock = compose(onPropsChange, null, null, {pure: false})(Time);

Ref to base component

In some situations you need to get a ref to base component you pass to react-komposer. You can enable a ref with the withRef option:

// You can use `composeWithPromise` or any other compose APIs
// instead of `compose`.
const Clock = compose(onPropsChange, null, null, {withRef: true})(Time);

The base component will then be accessible with getWrappedInstance().

Disable functionality

Sometimes, when testing we may need to disable the functionality of React Komposer. If needed, we can do it like this:

import { disable } from 'react-komposer';
disable();

You need to do this, before composing any containers. After that, the composed container will render nothing.

This is pretty useful when used with React Storybook.

You can also revert this behaviour with:

disable(false);

Anyway, you need to create containers again.

Change Default Components

It is possible to change default error and loading components globally. So, you don't need(if needed) to set default components in every composer call.

Here's how do this:

import {
  setDefaultErrorComponent,
  setDefaultLoadingComponent,
} from 'react-komposer';

const ErrorComponent = () => (<div>My Error</div>);
const LoadingComponent = () => (<div>My Loading</div>);

setDefaultErrorComponent(ErrorComponent);
setDefaultLoadingComponent(LoadingComponent);

This is very important if you are using this in a React Native app, since, this project has no default components for React Native. So, you can set default components like above at the very beginning.

Using with XXX

Using with Promises

For this, you can use the composeWithPromise instead of compose.

import {composeWithPromise} from 'react-komposer'

// Create a component to display Time
const Time = ({time}) => (<div>{time}</div>);

// Assume this get's the time from the Server
const getServerTime = () => {
  return new Promise((resolve) => {
    const time = new Date().toString();
    setTimeout(() => resolve({time}), 2000);
  });
};

// Create the composer function and tell how to fetch data
const composerFunction = (props) => {
  return getServerTime();
};

// Compose the container
const Clock = composeWithPromise(composerFunction)(Time, Loading);

// Render the container
ReactDOM.render(<Clock />, document.getElementById('react-root'));

See this live: https://jsfiddle.net/arunoda/8wgeLexy/

Using with Meteor

For that you need to use composeWithTracker method instead of compose. Then you can watch any Reactive data inside that.

import {composeWithTracker} from 'react-komposer';
import PostList from '../components/post_list.jsx';

function composer(props, onData) {
  if (Meteor.subscribe('posts').ready()) {
    const posts = Posts.find({}, {sort: {_id: 1}}).fetch();
    onData(null, {posts});
  };
};

export default composeWithTracker(composer)(PostList);

In addition to above, you can also return a cleanup function from the composer function. See following example:

import {composeWithTracker} from 'react-komposer';
import PostList from '../components/post_list.jsx';

const composerFunction = (props, onData) => {
  // tracker related code
  return () => {console.log('Container disposed!');}
};

// Note the use of composeWithTracker
const Container = composeWithTracker(composerFunction)(PostList);

For more information, refer this article: Using Meteor Data and React with Meteor 1.3

Using with Rx.js Observables

import {composeWithObservable} from 'react-komposer'

// Create a component to display Time
const Time = ({time}) => (<div>{time}</div>);

const now = Rx.Observable.interval(1000)
  .map(() => ({time: new Date().toString()}));

// Create the composer function and tell how to fetch data
const composerFunction = (props) => now;

// Compose the container
const Clock = composeWithObservable(composerFunction)(Time);

// Render the container
ReactDOM.render(<Clock />, document.getElementById('react-root'));

Try this live: https://jsfiddle.net/arunoda/Lsdekh4y/

Using with Redux


const defaultState = {time: new Date().toString()};
const store = Redux.createStore((state = defaultState, action) => {
  switch(action.type) {
    case 'UPDATE_TIME':
      return {
        ...state,
        time: action.time
      };
    default:
      return state;
  }
});

setInterval(() => {
  store.dispatch({
    type: 'UPDATE_TIME',
    time: new Date().toString()
  });
}, 1000);


const Time = ({time}) => (<div><b>Time is</b>: {time}</div>);

const onPropsChange = (props, onData) => {
  onData(null, {time: store.getState().time});
  return store.subscribe(() => {
    const {time} = store.getState();
    onData(null, {time})
  });
};

const Clock = compose(onPropsChange)(Time);

ReactDOM.render(<Clock />, document.getElementById('react'))

Try this live: https://jsfiddle.net/arunoda/wm6romh4/

Extending

Containers built by React Komposer are, still, technically just React components. It means that they can be extended in the same way you would extend any other component. Checkout following examples:

const Tick = compose(onPropsChange)(Time);
class Clock extends Tick {
  componentDidMount() {
    console.log('Clock started');

    return super();
  }
  componentWillUnmount() {
    console.log('Clock stopped');

    return super();
  }
};
Clock.displayName = 'ClockContainer';

export default Clock;

Remember to call super when overriding methods already defined in the container.

Stubbing

It's very important to stub Containers used with react-komposer when we are doing isolated UI testing. (Specially with react-storybook). Here's how you can stub composers:

First of all, this is only work if you are using composeAll utility.

At the very beginning of your initial JS file, set the following code.

import { setStubbingMode } from 'react-komposer';
setStubbingMode(true);

In react-storybook, that's the .storybook/config.js file.

Then all your containers will look like this:

With no stub

If you need, you set a stub composer and pass data to the original component bypassing the actual composer function. You can do this, before using the component which has the container.

import { setComposerStub } from 'react-komposer';
import CommentList from '../comment_list';
import CreateComment from '../../containers/create_comment';

// Create the stub for the composer.
setComposerStub(CreateComment, (props) => {
  const data = {
    ...props,
    create: () => {},
  };

  return data;
});

In react-storybook you can do this when you are writing stories.

Here, CreateComment container is using inside the CommentList container. We simply set a stubComposer, which returns some data. That data will be passed as props to the original component behind CreateComment container.

This is how looks like after use the stub.

With stub

You can see a real example in the Mantra's sample blog app.

Caveats

SSR

In the server, we won't be able to cleanup resources even if you return the cleanup function. That's because, there is no functionality to detect component unmount in the server. So, make sure to handle the cleanup logic by yourself in the server.

Composer Rerun on any prop change

Right now, composer function is running again for any prop change. We can fix this by watching props and decide which prop has been changed. See: #4