React context withTracker is awesome!


#21

Just started using React in Meteor for a new app, and this thread was very useful.

I’ve modified things to reduce the amount of boilerplate I’d have to write, and this might be a useful addition to the conversation. (Or I might learn something about how I should be doing things).

Basically, I can write components that consume context like this (the context is added to props) and write providers concentrating on just getting the data its schema:

some-component.jsx

import React from 'react';
import PropTypes from 'prop-types';

import { InjectConnectionStatusContextInto, connectionStatusPropTypes } from '/imports/client/providers/connection-status-provider';

export class ConnectionStatusInfo extends React.Component {
	render() {
		const { connected, status, retryCount } = this.props.connectionStatus;
		return (
			<React.Fragment>
				<h3>Connection Information</h3>

				<p>Status: <code>{status}</code></p>

				<p>Connected: <code>{connected.toString()}</code></p>
			</React.Fragment>
		);
	}
}
ConnectionStatusInfo.propTypes = {
	connectionStatus: PropTypes.shape(connectionStatusPropTypes).isRequired,
};

export default InjectConnectionStatusContextInto(ConnectionStatusInfo);

The provider:

import PropTypes from 'prop-types';

import { Meteor } from 'meteor/meteor';

import createReactiveContext from './create-reactive-context';

export const connectionStatusPropTypes = {
	retryCount: PropTypes.number.isRequired,
	status: PropTypes.string.isRequired,
	connected: PropTypes.bool.isRequired,
};

const propTypes = {
	connectionStatus: PropTypes.shape(connectionStatusPropTypes).isRequired,
};

function getProps() {
	return {
		connectionStatus: Meteor.connection.status()
	};
}

export const {
	ConnectionStatusProvider, ConnectionStatusConsumer,
	ConnectionStatusContext, InjectConnectionStatusContextInto
} = createReactiveContext('ConnectionStatus', propTypes, getProps);

I’ve dumped all the boilerplate into a function createReactiveContext which is shown here:

import React from 'react';
import PropTypes from 'prop-types';
import _ from 'underscore';

import { withTracker } from 'meteor/react-meteor-data';

function injectContext(ContextConsumer, WrappedComponent, preferContext) {
	// context overwrites props if preferContext = true
	return props => {
		return (
			<ContextConsumer>
				{context => preferContext ? <WrappedComponent {...props} {...context}/> : <WrappedComponent {...context} {...props}/>}
			</ContextConsumer>
		);
	};
}

export default function createReactiveContext(contextName, propTypes, reactivePropFunction,
	{ preferContext = true, namedComponents = true } = {}
) {
	const Context = React.createContext(contextName);

	class ReactiveProvider extends React.Component {
		render() {
			return (
				<Context.Provider value={this.props}>
					{this.props.children}
				</Context.Provider>
			);
		}
	}

	ReactiveProvider.propTypes = {
		children: PropTypes.node.isRequired
	};
	_.extend(ReactiveProvider.propTypes, propTypes);

	const Provider = withTracker(reactivePropFunction)(ReactiveProvider);
	const Consumer = Context.Consumer;

	const InjectContextInto = Component => injectContext(Consumer, Component, preferContext);

	if (namedComponents) {
		return {
			[`${contextName}Provider`]: Provider,
			[`${contextName}Consumer`]: Consumer,
			[`${contextName}Context`]: Context,
			[`Inject${contextName}ContextInto`]: InjectContextInto,
		};
	} else {
		return { Provider, Consumer, Context, InjectContextInto };
	}
}

… that’s as currently written in my codebase.

Comments very much welcome. (Actually, comments sought… because I’m not sure if this is the best way.)


#22

Last time I tried to use the React Context it did not play very well with React Router - the latter simply silently failed. Any click on a <Link /> changed the address bar but did not render the actual route. No errors anywhere.

Just an FYI for anyone scratching his head because of this.


#23

Just ran into this issue.
I think the issue is update blocking described in react-router guide:

Simple solution for me was to add withRouter to the AccountProvider (using the OP example):

import { withRouter } from 'react-router-dom';

export const AccountProvider = withRouter(withAccount(Provider));

#24

Hello there!

Such a useful and neat pattern you shared @captainn. Thanks for that!

I wonder if you have any caching solution pattern you use that you’d like to share? Or anyone??

At the moment, I’m fetching data from the DB every time I go to a certain page, with React Router, so that definitely requires some improvements and I’m thinking of dropping the router all together. What do you folks think?


#25

I load data into a ground db instance over methods, usually on the container element for a route, in componentDidMount (then the reactive meteor source does its magic to reflow the data). You could also put a timeout on that, so that it would only re-request data after a certain amount of time has passed. You’d have to store that info somewhere, maybe in a separate offline collection, or redux or something. If you use redux or a memory only store, it would allow a refresh of the page to fetch new data.