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

October 23, 2017 0 Comments

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

 

 


  • The component should have one purpose only, rendering

  • The component should be small and easily understandable

  • The component should rerender only if needed


  • 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


// 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
// 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
  • 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


// 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
// 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
  • 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


// 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
// 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!
  • 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


// 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
// 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


Tag cloud