Let's compose React containers and feed data into components.
(supports ReactNative as well)
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.
npm i --save react-komposer
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
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.
You can customize the higher order component created by compose
in few ways. Let's discuss.
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);
};
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/
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.
const MyError = ({error}) => (<div>Error: {error.message}</div>);
const Clock = compose(onPropsChange, null, MyError)(Time);
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)
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);
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()
.
Checkout this test case for a proper example.
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.
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/
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
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/
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/
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.
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:
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.
You can see a real example in the Mantra's sample blog app.
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