Today we are publishing the second part of the material on writing clean code when developing React applications. Here are some more helpful tips.
โ
Read the first part
8. Convert duplicate elements to components
Converting duplicate elements into components suitable for reuse can be called "componentization" of such elements.
Each developer has his own reasons why he writes duplicate React code. This may be a deliberate action, or it may be an accident.
Whatever the reason for the appearance of the same code fragments in the application, the programmer should think about how to improve the situation.
For example, if someone has not made it a habit to get rid of duplicates, then they will most likely arise in his projects again and again. What kind of team player is the one who does this? It simply complicates the future life of its colleagues, who will be confused when they encounter duplicate code. They will get a special โgiftโ if they have to edit similar code fragments.
Take a look at the following example and think about how to improve it:
const SomeComponent = () => ( <Body noBottom> <Header center>Title</Header> <Divider /> <Background grey> <Section height={500}> <Grid spacing={16} container> <Grid xs={12} sm={6} item> <div className={classes.groupsHeader}> <Header center>Groups</Header> </div> </Grid> <Grid xs={12} sm={6} item> <div> <img src={photos.groups} alt="" className={classes.img} /> </div> </Grid> </Grid> </Section> </Background> <div> <Section height={500}> <Grid spacing={16} container> <Grid xs={12} sm={6} item> <div className={classes.labsHeader}> <Header center>Labs</Header> </div> </Grid> <Grid xs={12} sm={6} item> <div> <img src={photos.labs} alt="" className={classes.img} /> </div> </Grid> </Grid> </Section> </div> </Body> )
If now you need to change the grid parameters from
xs={12} sm={6}
to
xs={12} sm={4}
, then this task will not be particularly pleasant. The fact is that for this you have to edit the code in four places.
The beauty of the component approach is that it allows you to solve problems similar to the above, changing the code in only one place. In our case, this change will be reflected everywhere where grids are used:
const SomeComponent = ({ classes, xs = 12, sm = 6, md, lg }) => { const BodySection = ({ header, src }) => { const gridSizes = { xs, sm, md, lg } return ( <Section height={500}> <Grid spacing={16} container> <Grid {...gridSizes} item> <div className={classes.groupsHeader}> <Header center>{header}</Header> </div> </Grid> <Grid {...gridSizes} item> <div> <img src={src} alt="" className={classes.img} /> </div> </Grid> </Grid> </Section> ) } return ( <Body noBottom> <Header center>Title</Header> <Divider /> <Background grey> <BodySection header="Groups" src={photos.groups} /> </Background> <div> <BodySection header="Labs" src={photos.labs} /> </div> </Body> ) }
Even the minimal level of code conversion demonstrated here makes this code much more convenient in terms of reading and supporting it. He, at the same time, is a completely adequate way to solve the task assigned to him.
9. Strive to keep your components as simple as possible.
When working on sales applications, I sometimes came across not the need to strive for component simplicity, but the need to avoid situations in which components become too complex.
Here is an example of a component that is unnecessarily complicated. It is represented by the
ConfirmAvailability.js
file:
import React from 'react' import Grid from '@material-ui/core/Grid' import Typography from '@material-ui/core/Typography' import MenuItem from '@material-ui/core/MenuItem' import Select from '@material-ui/core/Select' import Time from 'util/time' export default class TimeZonePicker extends React.Component { state = { time: new Date(), offset: -(new Date().getTimezoneOffset() / 60), } componentDidMount() { this.props.setOffset(this.state.offset) } handleChange = (event) => { const d = new Date() d.setTime( d.getTime() + d.getTimezoneOffset() * 60 * 1000 + event.target.value * 3600 * 1000, ) this.setState({ time: d, offset: event.target.value, }) this.props.setOffset(event.target.value) } render() { const timezones = [] for (let i = -12; i <= 14; i++) { timezones.push( <MenuItem key={i} value={i}> {i > 0 ? '+' : null} {i} </MenuItem>, ) } return ( <React.Fragment> <Grid container justify="space-between"> <div> <Typography>Current time</Typography> <Typography variant="h6" gutterBottom> {Time.formatTime(this.state.time)} </Typography> </div> <div> <Typography>Set timezone</Typography> <Select value={this.state.offset} onChange={this.handleChange}> {timezones} </Select> </div> </Grid> </React.Fragment> ) } }
This component was conceived as a simple mechanism, but since it contains highly related logic, it is responsible for solving several problems. At the time this code was written, React hooks had not yet been released, but React included technologies such as higher-order components and render props. This means that we can simply use one of these patterns to simplify the component. This will allow us to demonstrate an approach to simplifying components without changing existing functionality.
Previously, all code was stored in a single file. Now we split it into two files. Here is the contents of the first file -
SelectTimeZone.js
:
import React from 'react' class SelectTimeZone extends React.Component { state = { time: new Date(), offset: -(new Date().getTimezoneOffset() / 60), } componentDidMount() { this.props.setOffset(this.state.offset) } handleChange = (event) => { const d = new Date() d.setTime( d.getTime() + d.getTimezoneOffset() * 60 * 1000 + event.target.value * 3600 * 1000, ) this.setState({ time: d, offset: event.target.value, }) this.props.setOffset(event.target.value) } getTimeZones = () => { const timezones = [] for (let i = -12; i <= 14; i++) { timezones.push( <MenuItem key={i} value={i}> {i > 0 ? '+' : null} {i} </MenuItem>, ) } return timezones } render() { return this.props.render({ ...this.state, getTimeZones: this.getTimeZones, }) } }
Here's what the second file looks like -
TimeZonePicker.js
:
import React from 'react' import Grid from '@material-ui/core/Grid' import Typography from '@material-ui/core/Typography' import MenuItem from '@material-ui/core/MenuItem' import Select from '@material-ui/core/Select' import Time from 'util/time' const TimeZonePicker = () => ( <SelectTimeZone render={({ time, offset, getTimeZones, handleChange }) => ( <Grid container justify="space-between"> <div> <Typography>Current time</Typography> <Typography variant="h6" gutterBottom> {Time.formatTime(time)} </Typography> </div> <div> <Typography>Set timezone</Typography> <Select value={offset} onChange={handleChange}> {getTimeZones()} </Select> </div> </Grid> )} /> ) export default TimeZonePicker
After processing, the project code turned out to be much cleaner than before. We extracted the logic from the presentation part of the component. Now, in addition, unit testing of the project will be greatly simplified.
10. Use useReducer when complicating useState
The more state fragments you have to process in the project, the more complicated the use of
useState
.
This, for example, might look like this:
import React from 'react' import axios from 'axios' const useFrogs = () => { const [fetching, setFetching] = React.useState(false) const [fetched, setFetched] = React.useState(false) const [fetchError, setFetchError] = React.useState(null) const [timedOut, setTimedOut] = React.useState(false) const [frogs, setFrogs] = React.useState(null) const [params, setParams] = React.useState({ limit: 50 }) const timedOutRef = React.useRef() function updateParams(newParams) { if (newParams != undefined) { setParams(newParams) } else { console.warn( 'You tried to update state.params but the parameters were null or undefined', ) } } function formatFrogs(newFrogs) { const formattedFrogs = newFrogs.reduce((acc, frog) => { const { name, age, size, children } = frog if (!(name in acc)) { acc[name] = { age, size, children: children.map((child) => ({ name: child.name, age: child.age, size: child.size, })), } } return acc }, {}) return formattedFrogs } function addFrog(name, frog) { const nextFrogs = { ...frogs, [name]: frog, } setFrogs(nextFrogs) } function removeFrog(name) { const nextFrogs = { ...frogs } if (name in nextFrogs) delete nextFrogs[name] setFrogs(nextFrogs) } React.useEffect(() => { if (frogs === null) { if (timedOutRef.current) clearTimeout(timedOutRef.current) setFetching(true) timedOutRef.current = setTimeout(() => { setTimedOut(true) }, 20000) axios .get('https://somefrogsaspi.com/api/v1/frogs_list/', { params }) .then((response) => { if (timedOutRef.current) clearTimeout(timedOutRef.current) setFetching(false) setFetched(true) if (timedOut) setTimedOut(false) if (fetchError) setFetchError(null) setFrogs(formatFrogs(response.data)) }) .catch((error) => { if (timedOutRef.current) clearTimeout(timedOutRef.current) console.error(error) setFetching(false) if (timedOut) setTimedOut(false) setFetchError(error) }) } }, []) return { fetching, fetched, fetchError, timedOut, frogs, params, addFrog, removeFrog, } } export default useFrogs
It will become much more convenient to work with this if you translate this code to use
useReducer
:
import React from 'react' import axios from 'axios' const initialFetchState = { fetching: false fetched: false fetchError: null timedOut: false } const initialState = { ...initialFetchState, frogs: null params: { limit: 50 } } const reducer = (state, action) => { switch (action.type) { case 'fetching': return { ...state, ...initialFetchState, fetching: true } case 'fetched': return { ...state, ...initialFetchState, fetched: true, frogs: action.frogs } case 'fetch-error': return { ...state, ...initialFetchState, fetchError: action.error } case 'set-timed-out': return { ...state, ...initialFetchState, timedOut: true } case 'set-frogs': return { ...state, ...initialFetchState, fetched: true, frogs: action.frogs } case 'add-frog': return { ...state, frogs: { ...state.frogs, [action.name]: action.frog }} case 'remove-frog': { const nextFrogs = { ...state.frogs } if (action.name in nextFrogs) delete nextFrogs[action.name] return { ...state, frogs: nextFrogs } } case 'set-params': return { ...state, params: { ...state.params, ...action.params } } default: return state } } const useFrogs = () => { const [state, dispatch] = React.useReducer(reducer, initialState) const timedOutRef = React.useRef() function updateParams(params) { if (newParams != undefined) { dispatch({ type: 'set-params', params }) } else { console.warn( 'You tried to update state.params but the parameters were null or undefined', ) } } function formatFrogs(newFrogs) { const formattedFrogs = newFrogs.reduce((acc, frog) => { const { name, age, size, children } = frog if (!(name in acc)) { acc[name] = { age, size, children: children.map((child) => ({ name: child.name, age: child.age, size: child.size, })), } } return acc }, {}) return formattedFrogs } function addFrog(name, frog) { dispatch({ type: 'add-frog', name, frog }) } function removeFrog(name) { dispatch({ type: 'remove-frog', name }) } React.useEffect(() => { if (frogs === null) { if (timedOutRef.current) clearTimeout(timedOutRef.current) timedOutRef.current = setTimeout(() => { setTimedOut(true) }, 20000) axios .get('https://somefrogsaspi.com/api/v1/frogs_list/', { params }) .then((response) => { if (timedOutRef.current) clearTimeout(timedOutRef.current) const frogs = formatFrogs(response.data) dispatch({ type: 'set-frogs', frogs }) }) .catch((error) => { if (timedOutRef.current) clearTimeout(timedOutRef.current) console.error(error) dispatch({ type: 'fetch-error', error }) }) } }, []) return { fetching, fetched, fetchError, timedOut, frogs, params, addFrog, removeFrog, } } export default useFrogs
Although this approach is probably not cleaner than using
useState
, as you can see by looking at the code, the new code is easier to maintain. This is due to the fact that when using
useReducer
programmer does not have to worry about state updates in different parts of the hook, since all these operations are defined in one place inside the
reducer
.
In the version of the code that uses
useState
, in addition to writing the logic, we need to declare functions inside the hook in order to figure out what the next part of the state should be. And when using
useReducer
you do not have to do this. Instead, everything falls into the
reducer
function. We just need to trigger an action of the appropriate type, and that, in fact, is all we need to worry about.
11. Use function declarations in controversial situations
A good example of using this recommendation is to create a
useEffect
cleanup
useEffect
:
React.useEffect(() => { setMounted(true) return () => { setMounted(false) } }, [])
An experienced React developer knows what the role of the returned function is, he will easily understand such code. But if you imagine that someone who is not very familiar with
useEffect
will read this code, it would be better to express their intentions in the code as clearly as possible. It is about using function declarations that can be given meaningful names. For example, this code can be rewritten like this:
React.useEffect(() => { setMounted(true) return function cleanup() { setMounted(false) } }, [])
This approach allows us to clearly describe what role the returned function plays.
12. Use Prettier
Prettier helps individual developers and teams maintain a consistent and consistent approach to code formatting. This tool helps save time and effort. It simplifies the execution of code reviews by reducing the number of reasons for discussing the style of programs. Prettier also encourages programmers to use clean code writing techniques. The rules applied by this tool are editable. As a result, it turns out that everyone can customize it as they see fit.
13. Strive to use shorthand for fragment declarations
The essence of this recommendation can be expressed by the following two examples.
Here's a shortened version of the fragment declaration:
const App = () => ( <> <FrogsTable /> <FrogsGallery /> </> )
Here is the full version:
const App = () => ( <React.Fragment> <FrogsTable /> <FrogsGallery /> </React.Fragment> )
14. Follow a certain order of placement of elements when writing code.
When I write code, I prefer to arrange some commands in a certain order. For example, I do this when importing files (the exception here is only importing
react
):
import React from 'react' import { useSelector } from 'react-redux' import styled from 'styled-components' import FrogsGallery from './FrogsGallery' import FrogsTable from './FrogsTable' import Stations from './Stations' import * as errorHelpers from '../utils/errorHelpers' import * as utils from '../utils/'
Looking at this code, someone might think that a special order is not observed here. After all, imported entities are not even sorted alphabetically. But arranging something in alphabetical order is only part of the command ordering scheme that I use.
In an effort to keep my projects code clean, I use the following rules that apply in the order they are followed:
- Import React.
- Import libraries (in alphabetical order).
- Absolute commands to import entities from a project (in alphabetical order).
- Relative import commands (in alphabetical order).
- Commands of the form
import * as
. - Commands of the form
import './<some file>.<some ext>'
.
And here is how I prefer to organize, for example, variables. Say - properties of objects:
const character = (function() { return { cry() {
If you follow certain rules for ordering entities when writing code - this will beneficially affect its purity.
Summary
We have provided you with tips for writing clean React application code. We hope you find something among them that is useful to you.
Dear readers! What recommendations would you add to the advice presented in this article?