Monday, 23 October, 2017 UTC


Summary

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
The post How to write the perfect React component (a Theodo standard) appeared first on Theodo.