Skip to content

Instantly share code, notes, and snippets.

@awatson1978
Last active June 9, 2019 06:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save awatson1978/55710db224a36b17e0c4a95ada60ddbe to your computer and use it in GitHub Desktop.
Save awatson1978/55710db224a36b17e0c4a95ada60ddbe to your computer and use it in GitHub Desktop.
Blaze to React Refactor

Step 1 - Template Skeleton

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.

Step 1.1 - Decide on template name

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.

Step 1.2 - Define react template

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>)
  }
}

Step 1.3 - Import dependencies

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';

Step 1.4 - exports and default export

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;

Step 1.5 - Sketch out div elements in render() method

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>)
  }
}

Step 1.6 - PropTypes / Typescript (inputs)

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
};

Step 2 - Reactive Displays

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.

Step 2.1 - Data Management - getMeteorData()

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;

Step 2.2 - render() fields based on this.data

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>)
  }
}

Step 2.3 - this.props

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>)
  }
}

Step 2.4 - Defensive Programming

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>)
  }
}

Step 2.5 - Styling & Theming

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>)
  }
}

Step 3 - DDP to REST Refactor

Step 3.1 - Identify the publication and subscription

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

Step 3.2 - Install JSON Routes

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

Step 3.3 - REST Endpoints (aka JSON Routes)

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.

Step 3.4 - HTTP.post()

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)

Step 3.4 - Putting Data Back into Minimongo

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);            
          })
        }
    })

Step 4 - Forms

Step 4.1 - this.state

Step 4.2 - formData

Step 4.2 - flatten JSON to formData

Step 4.3 - form inputs

Step 4.4 - change internal state on user input

Step 4.5 - map internal state back to son

Step 4.6 - upsert data into Minimongo

Step 4.7 - Other lifecycle methods

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment