Сегодня мы публикуем вторую часть материала о написании чистого кода при разработке React-приложений. Вот ещё несколько полезных советов.
8. Преобразуйте дублирующиеся элементы в компоненты
Преобразование дублирующихся элементов в компоненты, подходящие для многократного использования, можно назвать «компонентизацией» таких элементов.
У каждого разработчика имеются собственные причины, по которым он пишет дублирующийся React-код. Это может быть намеренным действием, а может быть и случайностью.
Какой бы ни была причина появления в приложении одинаковых фрагментов кода, программисту стоит подумать о том, как улучшить ситуацию.
К примеру, если некто не завёл у себя привычку избавляться от дубликатов — то они, с высокой долей вероятности, будут возникать в его проектах снова и снова. Что за командный игрок тот, кто так поступает? Он попросту усложняет будущую жизнь своих коллег, которые будут путаться, встречая дублирующийся код. Особенный же «подарок» им достанется в том случае, если им придётся подобные фрагменты кода редактировать.
Взглянем на следующий пример и подумаем о том, как его улучшить:
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>
)
Если сейчас понадобится поменять параметры сетки с xs={12} sm={6}
на xs={12} sm={4}
, то выполнение этой задачи особо приятным не окажется. Дело в том, что для этого придётся редактировать код в четырёх местах.
Красота компонентного подхода в том, что он позволяет решать задачи, подобные вышеописанной, изменив код всего в одном месте. В нашем случае это изменение отразится везде, где используются сетки:
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>
)
}
Даже минимальный уровень преобразования кода, продемонстрированный здесь, делает этот код гораздо более удобным в плане его чтения и поддержки. Он, в то же время, представляет собой совершенно адекватный способ решения поставленной перед ним задачи.
9. Стремитесь к тому, чтобы ваши компоненты были бы как можно проще
Работая над продашкн-приложениями я иногда сталкивался не с необходимостью стремления к простоте компонентов, а с необходимостью в том, чтобы избегать ситуаций, в которых компоненты становятся слишком сложными.
Вот пример компонента, который неоправданно усложнён. Он представлен файлом ConfirmAvailability.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'
/**
* Средство для выбора часового пояса. Автоматически определяет часовой пояс, основываясь на настройках устройства клиента, но, кроме того, выводит
* часы, используя выясненный часовой пояс. Делается это для проверки правильности определения часового пояса. Если он определён неправильно - пользователь может установить его самостоятельно.
*
* ПРИМЕЧАНИЕ: Будьте осторожны с методом Date().getTimezoneOffset(). Он выполняет два действия нестандартно:
* 1. Разница во времени выражена в минутах.
* 2. Разница местного времени и UTC - это не то же самое, что разница UTC и местного времени. Это означает, что разница будет представлена отрицательным вариантом
* ожидаемого значения в формате UTC.
*/
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>
)
}
}
Этот компонент был задуман как простой механизм, но так как он содержит сильно связанную логику, он оказывается ответственным за решение нескольких задач. В то время, когда был написан этот код, хуки React ещё не были выпущены, но в React присутствовали такие технологии, как компоненты высшего порядка и render props. Это значит, что мы можем просто воспользоваться одним из этих паттернов для того, чтобы упростить компонент. Это позволит нам продемонстрировать подход к упрощению компонентов без изменения существующего функционала.
Раньше весь код хранился в единственном файле. Теперь мы разбили его на два файла. Вот содержимое первого файла — SelectTimeZone.js
:
import React from 'react'
/**
* Средство для выбора часового пояса. Автоматически определяет часовой пояс, основываясь на настройках устройства клиента, но, кроме того, выводит
* часы, используя выясненный часовой пояс. Делается это для проверки правильности определения часового пояса. Если он определён неправильно - пользователь может установить его самостоятельно.
*
* ПРИМЕЧАНИЕ: Будьте осторожны с методом Date().getTimezoneOffset(). Он выполняет два действия нестандартно:
* 1. Разница во времени выражена в минутах.
* 2. Разница местного времени и UTC - это не то же самое, что разница UTC и местного времени. Это означает, что разница будет представлена отрицательным вариантом
* ожидаемого значения в формате UTC.
*/
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,
})
}
}
Вот как выглядит второй файл — 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
После переработки код проекта оказался гораздо чище, чем прежде. Мы извлекли логику из презентационной части компонента. Теперь, кроме того, значительно упростится модульное тестирование проекта.
10. Используйте useReducer при усложнении useState
Чем больше фрагментов состояния приходится обрабатывать в проекте — тем сильнее усложняется использование useState
.
Это, например, может выглядеть так:
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
Работать с этим всем станет гораздо удобнее в том случае, если перевести данный код на использование 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
Хотя такой подход, вероятно, не будет чище, чем использование useState
, что видно при взгляде на код, новый код легче поддерживать. Это происходит из-за того, что при применении useReducer
программисту не приходится беспокоиться об обновлениях состояния в разных частях хука, так как все эти операции определены в одном месте внутри reducer
.
В версии кода, в которой используется useState
, нам, в дополнение к написанию логики, нужно объявлять функции внутри хука для того, чтобы выяснить то, какой должна быть следующая часть состояния. А при использовании useReducer
этого делать не приходится. Вместо этого всё попадает в функцию reducer
. Нам лишь нужно вызвать действие соответствующего типа, и это, собственно говоря, всё, о чём надо беспокоиться.
11. Используйте объявления функций в неоднозначных ситуациях
Хороший пример использования этой рекомендации представляет собой создание механизма очистки useEffect
:
React.useEffect(() => {
setMounted(true)
return () => {
setMounted(false)
}
}, [])
Опытный React-разработчик знает о том, какова роль возвращаемой функции, он легко поймёт подобный код. Но если представить, что данный код будет читать тот, кто не очень хорошо знаком с useEffect
, лучше будет выражать свои намерения в коде максимально ясно. Речь идёт об использовании объявлений функций, которым можно давать осмысленные имена. Например, этот код можно переписать так:
React.useEffect(() => {
setMounted(true)
return function cleanup() {
setMounted(false)
}
}, [])
Подобный подход позволяет чётко описать то, какую роль играет возвращаемая функция.
12. Используйте Prettier
Prettier помогает индивидуальным разработчикам и командам придерживаться единого и неизменного подхода к форматированию кода. Этот инструмент способствует экономии времени и сил. Он упрощает выполнение код-ревью за счёт снижения числа поводов для обсуждения стиля программ. Prettier, кроме того, подталкивает программистов к использованию методик написания чистого кода. Правила, применяемые этим инструментом, поддаются редактированию. В результате оказывается, что каждый может настроить его так, как считает нужным.
13. Стремитесь к использованию сокращённой записи объявлений фрагментов
Суть этой рекомендации можно выразить следующими двумя примерами.
Вот сокращённый вариант объявления фрагмента:
const App = () => (
<>
<FrogsTable />
<FrogsGallery />
</>
)
Вот полный вариант:
const App = () => (
<React.Fragment>
<FrogsTable />
<FrogsGallery />
</React.Fragment>
)
14. Придерживайтесь определённого порядка размещения элементов при написании кода
Я, когда пишу код, предпочитаю располагать некоторые команды в определённом порядке. Например, я это делаю при импорте файлов (исключением тут является лишь импорт 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/'
Глядя на этот код кто-то может подумать, что особого порядка тут не наблюдается. Ведь импортированные сущности даже не отсортированы по алфавиту. Но расстановка чего-либо в алфавитном порядке — лишь часть той схемы упорядочения команд, которой я пользуюсь.
Я, стремясь к чистоте кода моих проектов, использую следующие правила, применяемые в порядке их следования:
- Импорт React.
- Импорт библиотек (в алфавитном порядке).
- Абсолютные команды импорта сущностей из проекта (в алфавитном порядке).
- Относительные команды импорта (в алфавитном порядке).
- Команды вида
import * as
. - Команды вида
import './<some file>.<some ext>'
.
А вот как я предпочитаю организовывать, например, переменные. Скажем — свойства объектов:
const character = (function() {
return {
cry() {
//
},
eat() {
//
},
hop() {
//
},
jump() {
//
},
punch() {
//
},
run() {
//
},
scratch() {
//
},
scream() {
//
},
sleep() {
//
},
walk() {
//
},
yawn() {
//
},
}
})()
Если следовать неким правилам упорядочения сущностей при написании кода — это благотворно скажется на его чистоте.
Итоги
Мы представили вашему вниманию советы по написанию чистого кода React-приложений. Надеемся, вы нашли среди них что-то такое, что вам пригодится.
Уважаемые читатели! Какими рекомендациями вы дополнили бы советы, представленные в этом материале?
Автор: ru_vds