How to write the perfect React component (a Theodo standard)
Clément Pasteau5 min read
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?
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