Let's compose React containers and feed data into components.
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);
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);
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.
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