How to write the perfect React component (a Theodo standard)

What is the perfect React component?

  • The component should have one purpose only, rendering
  • The component should be small and easily understandable
  • The component should rerender only if needed

How to create the perfect React component?

  1. Logic Functions
  2. Atomic Design
  3. Selectors and Reselectors
  4. Functions inside render

Logic Functions

  • Export your logic functions to an external service
    • Functions other than lifecycle methods should only return JSX objects
    • Logic functions can then be easily reused in other components
    • Logic functions can then be unit tested
    • Component is easy to read

Bad Example

// MyComponent.js
export default class MyComponent extends PureComponent {
   computeAndDoStuff = prop1, prop2 => {
      // Logic that returns something depending on the props passed
   }

   render() {
      <div>
         {this.computeAndDoStuff(this.props.prop1, this.props.prop2) && <span>Hello</span>}
      </div>
   }
}

MyComponent.propTypes = {
   prop1,
   prop2,
}

const mapStateToProps = state => {
   prop1: state.object.prop1,
   prop2: state.object.prop2,
}

export const MyComponentContainer = connect(mapStateToProps)(MyComponent)
  • Component is doing more than just rendering, it is doing logic inside
  • Component needs multiple snapshots to test the logic of the function

Good Example

// MyComponent.js
import { computeAndDoStuff } from '@services/computingService'

export default class MyComponent extends PureComponent {
   render() {
      <div>
         {computeAndDoStuff(this.props.prop1, this.props.prop2) && <span>Hello</span>}
      </div>
   }
}

MyComponent.propTypes = {
   prop1,
   prop2,
}

const mapStateToProps = state => {
   prop1: state.object.prop1,
   prop2: state.object.prop2,
}

export const MyComponentContainer = connect(mapStateToProps)(MyComponent)
// computingService.js
export computeAndDoStuff = prop1, prop2 => {
   // Logic that returns something depending on the props passed
}
  • ✔︎ Component has only one role, render
  • ✔︎ Component needs only 2 snapshots, depending on if the result of the function is true or false
  • ✔︎ Function can be unit tested directly from the service, without involving the component

Atomic Design

  • Follow the “Atomic Design Methodology
    • Components will be small enough (200 lines max) to be easily understandable
    • Components can be found easily and the architecture is straightforward for newcomers

Bad Example

// MyPage.js
import { MyComponent1, MyComponent2, MyField1, Myfield2 } from '@components'

export default class MyPage extends PureComponent {
   render() {
      <div>
         <MyComponent1 />
         <div>
            <MyField1 />
            <MyField2 />
            <MyField1 disabled />
            <MyField2 color={'blue'} />
         </div>
         <MyComponent2 />
      </div>
   }
}

export const MyPageContainer = connect(mapStateToProps)(MyPage)
  • Components are all coming from the same folder
  • If the page needs to be modified, new components will be created in the same folders without thinking of refactoring
  • Component may end up being really long
  • The structure of the page is not easily understandable

Good Example

// MyPage.js
import { MyOrganism1, MyOrganism2, MyOrganism3 } from '@organisms'

export default class MyPage extends PureComponent {
  render() {
     <div>
        <MyOrganism1 />
        <MyOrganism2 />
        <MyOrganism3 />
     </div>
  }
}
// MyOrganism1.js
import { MyMolecule1, MyMolecule2 } from '@molecules'

export default class MyOrganism1 extends PureComponent {
  render() {
     <div>
        <MyMolecule1 />
        <MyMolecule2 />
     </div>
  }
}
// MyMolecule1.js
import { MyAtom1, MyAtom2 } from '@atoms'

export default class MyMolecule1 extends PureComponent {
  render() {
     <div>
        <MyAtom1 />
        <MyAtom2 />
     </div>
  }
}
  • ✔︎ Page Component structure is understandable at first sight
  • ✔︎ When working on the Page again, it is easy to see if some components can be reused
  • ✔︎ For a new developer, it is easy to understand right away
  • ✔︎ Components stay small and easily testable

Selectors and Reselectors

  • Use selectors and reselectors
    • Components will handle only a few props (10 props max) to be easily understandable
    • Components will be completely decoupled from the shape of the store
    • Performance will be increased in case of computed derived data, thanks to reselectors memoisation
    • Selectors and reselectors can be easily tested

Bad Example

// Table.js
import ...

export default class Table extends PureComponent {
   constructor(props) {
      super(props)
      this.renderTable = this.renderTable.bind(this)
      this.calculateNewProps(...props)
   }
   
   componentWillUpdate(nextProps) {
      this.calculateNewProps(...nextProps)
   }
   
   calculateNewProps = (prop1, prop2, ..., prop15) => {
      // Logic that modifies the store for the table rendering
   }
   
   renderTable() {
      // Return JSX based on props
   }

   render() {
      this.renderTable()
   }
}

Table.propTypes = {
   prop1,
   prop2,
   ...
   prop15,
}

const mapStateToProps = state => {
   prop1: state.object.prop1, 
   prop2: state.object.prop2,
   ...
   prop15: state.object.prop15,
}

export const TableContainer = connect(mapStateToProps)(Table)
  • Component has too many props, it is really dependent on the store shape
  • Component is too long (was 300+)
  • Component is updating the store in its own lifecycle, which can cause race conditions

Good Example

// Table.js
import ...
import { getTableRows } from '@selectors'

export default class Table extends PureComponent {
   renderTable() {
      // Return JSX based on rows
   }

   render() {
      this.renderTable()
   }
}

Table.propTypes = {
   tableRows,
}

const mapStateToProps = state => {
   tableRows: getTableRows(state),
}

export const TableContainer = connect(mapStateToProps)(Table)
// selectors.js
import { createSelector } from 'reselect'

export const getTableRows = createSelector(
   getProp1,
   getProp2,
   ...,
   getProp15,
   (prop1, prop2, ..., prop15) => {
      // logic to return the table rows based on the props in the store
   }
)
  • ✔︎ Component is completely decoupled from stores shape
  • ✔︎ Component does not have any logic, its job is to render objects
  • ✔︎ Component is easy to read or revisit
  • ✔︎ Selector (data formatting) can be easily tested!

Functions inside render

  • Never create functions into the render(), use arrow functions
    • Functions defined into onClick or onChange methods will be recreated every time the action is triggered, causing rerendering and performance impact
    • Component will not rerender if the arrow function is defined outside of the render()
    • Arrow function have access to this without needing to be bound in the constructor

Bad Example

// MyPage.js
import doSomething from '@services'

export default class MyPage extends PureComponent {
   render() {
      <div>
         <Button onClick={() => doSomething(this.props.param))} />
      </div>
   }
}

MyPage.propTypes = {
   param,
}
  • Function is defined inside the render, a new instance will be created even if the props do not change
  • Performance loss

Good Example

// MyPage.js
import doSomething from '@services'

export default class MyPage extends PureComponent {
   onClick = () => doSomething(this.props.param)

   render() {
      <div>
         <Button onClick={this.onClick} />
      </div>
   }
}

MyPage
  • ✔︎ Function is defined outside of the render function
  • ✔︎ The component will render only once for a given param
  • ✔︎ The function onClick does not need to be bound, because the arrow function gives access to this

You liked this article? You'd probably be a good match for our ever-growing tech team at Theodo.

Join Us