Skip to content

Instantly share code, notes, and snippets.

@awatson1978
Last active January 12, 2020 05:29
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/65e19fc45a2e2a76357001ad85051013 to your computer and use it in GitHub Desktop.
Save awatson1978/65e19fc45a2e2a76357001ad85051013 to your computer and use it in GitHub Desktop.
React Class to Pure Function Refactor

Refactoring from React Class to Pure Function

  1. Comment out import statements to material-ui and react-bootstrap.
import { CardText, Checkbox } from 'material-ui';
import { Table } from 'react-bootstrap';
  1. Remove React Mixin.
import { ReactMeteorData } from 'meteor/react-meteor-data';
import ReactMixin  from 'react-mixin';
  1. Add best practice imports.
// react goodies
import React, { useState } from 'react';

// utilities
import moment from 'moment-es6'
import _ from 'lodash';
let get = _.get;
let set = _.set;

// user interface
import { 
  Button,
  Card,
  Checkbox,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
  TableFooter,
  TablePagination,
  IconButton,
  FirstPageIcon,
  KeyboardArrowLeft,
  KeyboardArrowRight,
  LastPageIcon
} from '@material-ui/core';

// icons
// https://react-icons.netlify.com/#/
import { FaTags, FaCode, FaPuzzlePiece, FaLock  } from 'react-icons/fa';
  1. Add new styling pattern.
// We're using the new theme provider!
import { ThemeProvider, makeStyles } from '@material-ui/styles';
const useStyles = makeStyles(theme => ({
  button: {
    background: theme.background,
    border: 0,
    borderRadius: 3,
    boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
    color: theme.buttonText,
    height: 48,
    padding: '0 30px',
  }
}));
  1. Comment out old styling method.
// DONT USE PLAIN JSON OBJECTS
let styles = {
  button: {
    background: theme.background,
    border: 0,
    borderRadius: 3,
    boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
    color: theme.buttonText,
    height: 48,
    padding: '0 30px',
  },
  widget: {
    width: '100%'
  }
}
  1. Create pure component.
function MyFunctionalWidget(props){
  const classes = useStyles();

  return(<div id="myWidget" className={classes.widget}>
    Hello World!
  </div>);
}
  1. Add pagination and internal state.
  //---------------------------------------------------------------------
  // Pagination

  let rows = [];
  let rowsPerPageToRender = 5;
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(5);

  if(props.rowsPerPage){
    // if we receive an override as a prop, render that many rows
    // best to use rowsPerPage with disablePagination
    rowsPerPageToRender = props.rowsPerPage;
  } else {
    // otherwise default to the user selection
    rowsPerPageToRender = rowsPerPage;
  }

  let paginationCount = 101;
  if(props.count){
    paginationCount = props.count;
  } else {
    paginationCount = rows.length;
  }
  1. Copy over functions.
  function rowClick(id){ ... }
  function renderActionIconsHeader(){ ... }
  function renderActionIcons(procedure ){ ... }
  function onMetaClick(_id){ ... }
  function removeRecord(_id){ ... }
  function onActionButtonClick(id){ ... }

  function renderBarcode(id){ ... }
  function renderBarcodeHeader(){ ... }
  function renderSubject(name, type){ ... }
  function renderSubjectHeader(){ ... }
  1. Add function. Replace <tr> and <th> with <TableCell>. Replace this.props with props
// OLD 
renderBarcode(id){
  if (!this.props.hideBarcodes) 
    return (
      <td><span className="barcode">{id}</span></td>
    );
  } 
}

// NEW  
function renderBarcode(id){
  if (!props.hideBarcodes) {
    return (
      <TableCell><span className="barcode" >{id}</span></TableCell>
    );
  }
}
  1. Create mapping file.
flattenCondition = function(procedure, dateFormat){
  let result = {
    _id: '',
    id: '',
    identifier: '',
    status: '',
    code: '',
    codeDisplay: '',
    subject: '',
    subjectReference: '',
    performedStart: '',
    performedEnd: ''
  };

  if(!dateFormat){
    dateFormat = get(Meteor, "settings.public.defaults.dateFormat", "YYYY-MM-DD");
  }

  result._id =  get(procedure, 'id') ? get(procedure, 'id') : get(procedure, '_id');

  result.id = get(procedure, 'id', '');
  result.status = get(procedure, 'status', '');
  result.identifier = get(procedure, 'identifier[0].value');
  result.code = get(procedure, 'code.coding[0].code');

  if(get(procedure, 'subject')){
    result.subject = get(procedure, 'subject.display', '');
    result.subjectReference = get(procedure, 'subject.reference', '');
  } else if(get(procedure, 'patient')){
    result.subject = get(procedure, 'patient.display', '');
    result.subjectReference = get(procedure, 'patient.reference', '');
  }

  if(get(procedure, 'performedPeriod')){
    result.performedStart = moment(get(procedure, 'performedPeriod.start')).format(dateFormat);      
    result.performedEnd = moment(get(procedure, 'performedPeriod.end')).format(dateFormat);      
  }

  return result;
}
  1. Copy over HTML render block.
// OLD
export class MyFunctionalWidget extends React.Component {
  getMeteorData() {}
  render(){
    return(
      <Table id='conditionsTable' hover >
        <thead>
          <tr>
            { this.renderCheckboxHeader() } 
            { this.renderActionIconsHeader() }
            { this.renderIdentifierHeader() }
            { this.renderPatientNameHeader() }
            { this.renderAsserterNameHeader() }
            { this.renderStatusHeader() }
          
            <th className='snomedCode'>Code</th>
            <th className='snomedDisplay'>Condition</th>

            { this.renderVerificationHeader() }
            { this.renderSeverityHeader() }
            { this.renderEvidenceHeader() }
            { this.renderDateHeader('Start') }
            { this.renderDateHeader('End') }
          </tr>
        </thead>
        <tbody>
          { tableRows }
        </tbody>
      </Table>
    );
  }
}


// NEW
function MyFunctionalWidget(props){
  return(
    <div>
      <Table id='conditionsTable'>
        <TableHead>
          <TableRow>
            { renderCheckboxHeader() } 
            { renderActionIconsHeader() }
            { renderIdentifierHeader() }
            { renderPatientNameHeader() }
            { renderAsserterNameHeader() }
            { renderStatusHeader() }
            { renderSnomedCodeHeader() }
            { renderSnomedDisplayHeader() }          
            { renderVerificationHeader() }
            { renderSeverityHeader() }
            { renderEvidenceHeader() }
            { renderDateHeader('Start') }
            { renderDateHeader('End') }
          </TableRow>
        </TableHead>
        <TableBody>
          { tableRows }
        </TableBody>
      </Table>
    { paginationFooter }
    </div>
  );
}
  1. Table columns and headers should have their own functions, with prop override to enable/disable.

  2. Add pagination.


  let tableRows = [];
  let proceduresToRender = [];
  let dateFormat = "YYYY-MM-DD";

  if(props.showMinutes){
    dateFormat = "YYYY-MM-DD hh:mm";
  }
  if(props.dateFormat){
    dateFormat = props.dateFormat;
  }

  if(props.procedures){
    if(props.procedures.length > 0){     
      let count = 0;    
      props.procedures.forEach(function(procedure){
        if((count >= (page * rowsPerPageToRender)) && (count < (page + 1) * rowsPerPageToRender)){
          proceduresToRender.push(flattenProcedure(procedure, dateFormat));
        }
        count++;
      });  
    }
  }

  if(proceduresToRender.length === 0){
    console.log('No procedures to render');
    // footer = <TableNoData noDataPadding={ props.noDataMessagePadding } />
  } else {
    for (var i = 0; i < proceduresToRender.length; i++) {
      tableRows.push(
        <TableRow className="procedureRow" key={i} onClick={ rowClick.bind(this, proceduresToRender[i]._id)} style={{cursor: 'pointer'}} hover="true" >            
          { renderToggle() }
          { renderActionIcons(proceduresToRender[i]) }
          { renderIdentifier(proceduresToRender.identifier ) }
          { renderStatus(proceduresToRender[i].status)}
          { renderCategory(proceduresToRender[i].categoryDisplay)}
          { renderCode(proceduresToRender[i].code)}
          { renderCodeDisplay(proceduresToRender[i].codeDisplay)}          
          { renderSubject(proceduresToRender[i].subject)}
          { renderSubjectReference(proceduresToRender[i].subjectReference)}
          { renderPerformer(proceduresToRender[i].performerDisplay)}
          { renderBodySite()}
          { renderPerformedStart(proceduresToRender[i].performedStart)}
          { renderPerformedEnd(proceduresToRender[i].performedEnd)}
          { renderNotes(proceduresToRender[i].notesCount)}
          { renderBarcode(proceduresToRender[i]._id)}
          { renderActionButton(proceduresToRender[i]) }
        </TableRow>
      );    
    }
  }

  1. Audit all props referenced in the function, and make sure they are registered as PropTypes.
ConditionsTable.propTypes = {
  data: PropTypes.array,
  conditions: PropTypes.array,
  query: PropTypes.object,
  paginationLimit: PropTypes.number,
  disablePagination: PropTypes.bool,

  renderCheckboxes: PropTypes.bool,
  renderActionIcons: PropTypes.bool,
  renderIdentifier: PropTypes.bool,
  renderPatientName: PropTypes.bool,
  renderDates: PropTypes.bool,

  onCellClick: PropTypes.func,
  onRowClick: PropTypes.func,
  onMetaClick: PropTypes.func,
  onRemoveRecord: PropTypes.func,
  onActionButtonClick: PropTypes.func,
  showActionButton: PropTypes.bool,
  actionButtonLabel: PropTypes.string,

  rowsPerPage: PropTypes.number,
  dateFormat: PropTypes.string,
  showMinutes: PropTypes.bool,
  renderEnteredInError: PropTypes.bool
};

ConditionsTable.defaultProps = {
  renderCheckboxes: true,
  renderActionIcons: true,
  renderIdentifier: true,
  renderPatientName: true,
  renderDates: true,
  rowsPerPage: 10,
  dateFormat: "YYYY-MM-DD",
  showMinutes: false
}
  1. Make sure the export function is correct.
export default MyFunctionalWidget;
  1. Remove any commented code that has been confirmed to be working in refactor location.
  2. Compile and debug.

General Pattern Cleanups

  • Make sure to replace any .bind('this') statements with .bind(this)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment