The first step in refactoring Blaze to React is to sketch out the skelaton of the new React component, based on the Blaze template. There are a few concrete steps to do first. We save data flows, methods, styling, and other conserations for steps 2 and 3. In this first phase, we're looking to implement structural elements and to make sure that our component compiles within the build pipeline.
First step is to grab the name from the template that's going to be refactored. In this example, quick_rate_system
.
<template name="quick_rate_system">
</template>
Snake case isn't generally considered a best practice in React, so the typical process would be to convert quick_rate_system
to QuickRateSystem
.
We begin by sketching out a basic React template.
import React from 'react';
export class QuickRateSystem extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return(<div></div>)
}
}
You then want to go through the existing Blaze template, and identify functionality and dependencies that it's going to need import. We generally see four or five different groupings of imports... React specific helper libraries for managing the DOM, Meteor libraries for data management, utility libraries, UI components, and data visualization libraries.
// render methods and scaffolding
import React from 'react';
import { ReactMeteorData } from 'meteor/react-meteor-data';
import ReactMixin from 'react-mixin';
import PropTypes from 'prop-types';
// core Meteor functionality and data management
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { HTTP } from 'meteor/http';
// utility libraries
import { get, set } from 'lodash';
import moment from 'moment';
// UI components (pure)
import { CardActions, CardText, RaisedButton, TextField } from 'material-ui';
import { Row, Col } from 'react-bootstrap';
The pattern for linking files and connecting a React component to the rest of the client side rendering infrastructure differs between build systems. For best results in Meteor, one wants to export each component, and then define a default export at the end of the file.
export class HelperComponent extends React.Component {
constructor(props) { }
render() {
return(<div>Hello! I'm helping!</div>)
}
}
export class QuickRateSystem extends React.Component {
constructor(props) { }
render() {
return(<div>
<HelperComponent />
</div>)
}
}
export default QuickRateSystem;
You then want to copy over a lot of the Blaze template. Aim for the big structural elements, as some of the finer grained rendering logic may get reimplemented when using React.
export class QuickRateSystem extends React.Component {
constructor(props) { }
render() {
return(<div>
<!-- migrate Blaze template elements here-->
</div>)
}
}
Lastly, you'll want to sketch out the inputs that the template currently accepts, and create PropTypes out of them. PropTypes are one approach to type checking; and are a good refactor path for completely migrating to TypeScript. For pure components, one wants to accept the data as a prop. Boolean toggles to control rendering options facilitates reusability. Passing in functions allow for the best reusability.
RawOrderTable.propTypes = {
id: PropTypes.string,
apiVersion: PropTypes.string,
data: PropTypes.array,
displayTitle: PropTypes.bool,
displayMessage: PropTypes.bool,
onClick: PropTypes.func,
onOtherEvent: PropTypes.func
};
When we say 'reactive displays', we're generally talking about tables, dashboards, reports, and other pages which have live data sources and don't have user inputs or forms. In this section, we're interested in hooking up the client side data cache to the rendered output. The user input, forms, and data cycle round trip will be handled in the next section.
You then need to decide whether you're creating a pure component, or a composite component with a data store. The difference is that pure components don't have side effects and have high-reusability; while the composite components often introduce a dependency to the build infrastructure and client side data storage model. In the case of refactoring Blaze components to React within Meteor, we generally use the ReactMeteorData
library mixin, which provides bindings to the local Minimongo store.
That library then attaches the getMeteorData()
method to the React component, which will re-render the component whenever the underlying minimongo cursors update. The data object it returns becomes available as this.data
within the render()
method.
Putting all these things together, and we get the following structure for a page.
import React from 'react';
import { ReactMeteorData } from 'meteor/react-meteor-data';
import ReactMixin from 'react-mixin';
import PropTypes from 'prop-types';
import QuickRateSystem from '../QuickRateSystem';
export class QuickRatesPage extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
getMeteorData(){
let data = {
title: "Lorem Ipsum"
}
return data;
}
render() {
return(<div>
{ this.data.title }
<QuickRateSystem />
</div>)
}
}
ReactMixin(QuickRatesPage.prototype, ReactMeteorData)
export default QuickRatesPage;
You then want to spend time building up the data model for the component. If you're working on a pure component, you'll want to use this.state
. But for most Blaze components which rely on Session variables and Mongo cursors, this is done in this.data
and the getMeteorData()
function.
In the following example, we demonstrate how to toggle elements on/off based on Session variables and props, populate pure components with records from our minimongo cursors, and otherwise wire up the rendered DOM elements with dynamic data.
Session.setDefault('message', '')
export class QuickRatesPage extends React.Component {
getMeteorData(){
let data = {
title: "Lorem Ipsum",
subtitle: "dolar set et...",
message: false,
quickRatesVisible: false,
records: []
}
if(Session.get('message'){
data.message = Session.get('message')
}
if(Rates.find().count() > 0){
data.quickRatesVisible = true;
data.records = Rates.find().fetch();
}
return data;
}
render() {
let message;
let quickrates;
if(this.data.quickRatesVisible){
quickrates = <QuickRateSystem data={this.data.records} />
}
return(<Card>
<CardTitle title={this.data.title} subtitle={this.data.subtitle} />
{quickrates}
{message}
</Card>)
}
}
For pure components, you can do much of the same thing using this.props
. As you can see, this is simpler and cleaner and more akin to traditional React patterns.
export class QuickRatesPage extends React.Component {
render() {
let message;
let quickrates;
if(this.props.quickRatesVisible){
quickrates = <QuickRateSystem data={this.props.records} />
}
return(<Card>
<CardTitle title={this.props.title} subtitle={this.props.subtitle} />
{quickrates}
{this.props.message}
</Card>)
}
}
You may also want to consider defensive programming, and ensuring that every prop or data element passed into the component has a default value. The lodash helper function get
is a marvelous tool for plucking deeply nested fields and specifying a default value if that field doesn't exist.
export class QuickRatesPage extends React.Component {
render() {
let message;
let quickrates;
if(get(this, 'props.quickRatesVisible'))){
quickrates = <QuickRateSystem data={get(this, 'props.records', [])} />
}
return(<Card>
<CardTitle title={ get(this, 'props.title') } subtitle={ get(this, 'props.subtitle') } />
{quickrates}
{ get(this, 'props.message') }
</Card>)
}
}
At some point, you may want to import styles, apply a theme, and otherwise begin styling your application. Of course you can use legacy CSS management techniques by adding className
attribute to elements, and using the same CSS classes as were used in the original Blaze component. For many instances, this is the best approach.
But keep in mind that many React libraries provide robust theming functionality. And inline styling allows for video-game like control over the UI. In the following example, we show four different common types of styling. In order of occurance: importing a theme from Material UI, creating a component wide style
object, using a className
, and just-in-time inline styling.
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import {blue400, blue600} from 'material-ui/styles/colors';
const muiTheme = getMuiTheme({
palette: {
primary1Color: blue400,
primary2Color: blue600,
pickerHeaderColor: blue600
}
});
const styles = {
card: {
backgroundColor: theme.palette.background.paper
},
title: {
color: "#222222"
},
subtitle: {
color: "#222222"
}
};
export class QuickRatesPage extends React.Component {
render() {
let data = get(this, 'props.data', []);
let listitems = [];
data.forEach(item, function(text){
listitems.push(<li className="listitem">{text}</li>);
})
return(<div>
<MuiThemeProvider muiTheme={muiTheme}>
<Card style={styles.card}>
<CardTitle
title="Lorem Ipsum"
titleStyle={style.title}
subtitle="set dolar et..."
subtitleStyle={style.subtitle}
/>
<ul style={{cursor: 'pointer'}}>
{listitems}
</ul>
</Card>
</MuiThemeProvider>
</div>)
}
}
The first step in refactoring a DDP pub/sub into a REST endpoint is to identify the files where the subscription and publication exist. Publications are generally kept in the server
; but could be in a shared
directory or the imports
directory if it's wrapped in a if(Meteor.isServer)
statement.
Meteor.publish("generated_invoices_payable", function(filter, company_id) {
filter["client_id"] = company_id;
return Clear_View_Invoices_Payable.find(filter);
});
Subscriptions are generally kept in client
, but may also be present in shared
or imports
directories if they're wrapped in if(Meteor.isClient)
statements. Publications and subscriptions may also be located in packages within the packages
directory. And in advanced cases, it's also possible to do server-to-server DDP subscriptions.
Meteor.subscribe('generated_invoices_payable')
In short, publications are usually on the server, but subscriptions can be anywhere. Also, more than one component or device may subscribe to a single publication.
Importantly, you will want to grab the name of the publication. In this example it is generated_invoices_payable
In order to build a REST endpoint, we want a Meteor library that gives us fine grained control over the endpoint. We do so by adding the following packages to the .meteor/packages
file.
simple:json-routes
prime8consulting:meteor-oauth2-server
Alternatively, you can run the following commands:
meteor add simple:json-routes
meteor add prime8consulting:meteor-oauth2-server
Now that JsonRoutes is installed, we can begin rewriting the subscription into an endpoint. We like to keep the original subscription in the file (commented out) so that implementers can refer back to the original subscription. We've created a RestHelpers
object which abstracts the REST operations into a pattern that mostly mimics the publish
method. You should mostly be able to copy the contents of the publish
callback function into either the RestHelpers.returnGetResponseAfterAccessCheck
or RestHelpers.returnPostResponseAfterAccessCheck
functions.
// Meteor.publish("generated_invoices_payable", function(filter, company_id) {
// filter["client_id"] = company_id;
// return Clear_View_Invoices_Payable.find(filter);
// });
import { get } from 'lodash';
import { RestHelpers } from '../RestHelpers';
//==========================================================================================
// POST /generated_invoices_payable
JsonRoutes.add("post", "/" + RestHelpers.apiVersion + "/generated_invoices_payable", function (req, res, next) {
RestHelpers.logging(req, '/generated_invoices_payable');
RestHelpers.setHeaders(res);
RestHelpers.oauthServerCheck(req);
RestHelpers.returnPostResponseAfterAccessCheck(req, function(filter, pagination){
filter.client_id = get(req, 'query.company_id')
filter.client_id = get(req, 'query.client_id')
return Clear_View_Invoices_Payable.find(filter).fetch();
})
});
Publish inputs should be mapped onto REST query parameters using the ?
, =
, and &
syntax. The filter
object is created automatically from general purpose query parameters such as page
and items_per_page
. Be sure to add .fetch() when returning data from a cursor.
With those things in place, we're now ready to refactor the subscription. In the following example, we're replacing a DDP subscription with a HTTP POST operation on a 15 second polling interval.
// Meteor.subscribe('generated_invoices_payable')
import { HTTP } from 'meteor/http';
Meteor.setInterval(function(){
HTTP.post('/base/generated_invoices_payable?company_id=' + get(this, 'props.company_id') + '&client_id=' + get(this, 'props.client_id'), {
body: filter
}, function(error, result){
if(error)console.log('POST /generate_invoices_receivable error', error)
if(result)console.log('POST /generate_invoices_receivable result', result)
})
}, 15000)
We then want to put the data received from the DDP to REST refactor into the same place that the Meteor.subscribe()
function puts it... namely, into Minimongo. Since each publication is mapped to a cursor, and we're returning an array using the .fetch()
method, we can generally assume that the GET and POST response data - if present - will be an array of values we can loop through. We do that looping, and place each record into the minimongo cursor, checking for duplicates along the way, and updating existing records as needed.
HTTP.get('/base/generated_invoices_payable), {
body: filter
}, function(error, payload){
if(error)console.log('POST /generate_invoices_receivable error', error)
if(payload){
payload.forEach(function(record){
if(Clear_View_Invoices_Payable.findOne({_id: record._id}){
Clear_View_Invoices_Payable._collection.update({_id: record._id}, {$set: record);
} else {
Clear_View_Invoices_Payable._collection.insert(record);
})
}
})