[Перевод] 14 советов по написанию чистого React-кода. Часть 1
Написание чистого кода — это навык, который становится обязательным на определённом этапе карьеры программиста. Особенно этот навык важен тогда, когда программист пытается найти свою первую работу. Это, по существу, то, что делает разработчика командным игроком, и то, что способно либо «завалить» собеседование, либо помочь его успешно пройти. Работодатели, принимая кадровые решения, смотрят на код, написанный их потенциальными сотрудниками. Код, который пишет программист, должен быть понятен не только машинам, но и людям.
В материале, первую часть перевода которого мы публикуем сегодня, представлены советы по написанию чистого кода React-приложений. Актуальность этих советов тем выше, чем больше размер проекта, в котором применяются изложенные в них принципы. В маленьких проектах, вероятно, можно обойтись и без применения этих принципов. Принимая решение о том, что нужно в каждой конкретной ситуации, стоит руководствоваться здравым смыслом.
1. Деструктурируйте свойства
Деструктурирование свойств (в англоязычной терминологии React их называют «props») — это хороший способ сделать код чище и улучшить возможности по его поддержке. Дело в том, что это позволяет чётко выражать или объявлять то, что использует некая сущность (вроде компонента React). При этом такой подход не принуждает разработчиков вчитываться в реализацию компонента для того, чтобы выяснить состав свойств, связанных с ним.
Деструктурирование свойств, кроме того, даёт программисту возможность задавать их значения по умолчанию. Подобное встречается довольно-таки часто:
import React from 'react'
import Button from 'components/Button'
const MyComponent = ({ placeholder = '', style, ...otherProps }) => {
return (
)
}
export default MyComponent
Одно из самых приятных следствий применения деструктурирования в JavaScript, которое мне удалось обнаружить, заключается в том, что оно позволяет поддерживать различные варианты параметров.
Например, у нас имеется функция authenticate
, которая принимала в качестве параметра token
, используемый для аутентификации пользователей. Позже понадобилось сделать так, чтобы она принимала бы сущность jwt_token
. Эта необходимость была вызвана изменением структуры ответа сервера. Благодаря применению деструктурирования можно легко организовать поддержку обоих параметров и при этом не сталкиваться с необходимостью менять большую часть кода функции:
// до рефакторинга
async function authenticate({ user_id, token }) {
try {
const response = await axios.post('https://someapi.com/v1/auth/', {
user_id,
token,
})
console.log(response)
return response.data
} catch (error) {
console.error(error)
throw error
}
}
// после рефакторинга
async function authenticate({ user_id, jwt_token, token = jwt_token }) {
try {
const response = await axios.post('https://someapi.com/v1/auth/', {
user_id,
token,
})
console.log(response)
return response.data
} catch (error) {
console.error(error)
throw error
}
}
Сущность jwt_token
будет оцениваться в тот момент, когда код дойдёт до token
. В результате, если jwt_token
окажется действительным токеном, и сущность token
окажется равной undefined
, в token
попадёт значение jwt_token
. Если же в token
уже было какое-то значение, не являющееся по правилам JS ложным (то есть — некий реальный токен), то в token
просто останется то, что там уже было.
2. Размещайте файлы компонентов в продуманной структуре папок
Взглянем на следующую структуру директорий:
- src
- components
- Breadcrumb.js
- CollapsedSeparator.js
- Input
- index.js
- Input.js
- utils.js
- focusManager.js
- Card
- index.js
- Card.js
- CardDivider.js
- Button.js
- Typography.js
В состав навигационных цепочек (breadcrumbs) могут входить разделители (separators). Компонент CollapsedSeparator
импортируется в файле Breadcrumb.js
. Это даёт нам знание о том, что в реализации рассматриваемого проекта они связаны. Однако тот, кто не владеет этой информацией, может предположить, что Breadcrumb
и CollapsedSeparator
— это пара совершенно самостоятельных компонентов, которые никак друг с другом не связаны. Особенно — если у CollapsedSeparator
нет неких чётких признаков того, что этот компонент связан с компонентом Breadcrumb
. Среди таких признаков, например, может быть префикс Breadcrumb
, применяющийся в имени компонента, что может превратить имя в нечто вроде BreadcrumbCollapsedSeparator.js
.
Так как мы знаем о том, что Breadcrumb
и CollapsedSeparator
связаны друг с другом, то мы, возможно, зададимся вопросом о том, почему они не помещены в отдельную папку, вроде Input
и Card
. При этом мы можем начать делать различные предположения о том, почему материалы проекта имеют именно такую структуру. Скажем, тут можно подумать о том, что эти компоненты поместили на верхний уровень проекта для того, чтобы, заботясь о тех, кто будет работать с проектом, помочь им быстро найти эти компоненты. В результате взаимоотношения частей проекта выглядят для нового разработчика довольно-таки туманно. Применение методик написания чистого кода должно давать прямо противоположный эффект. Речь идёт о том, что благодаря им новый разработчик получает возможность читать чужой код и мгновенно схватывать суть ситуации.
Если использовать в нашем примере хорошо продуманную структуру директорий, то получится примерно следующее:
- src
- components
- Breadcrumb
- index.js
- Breadcrumb.js
- CollapsedSeparator.js
- Input
- index.js
- Input.js
- utils.js
- focusManager.js
- Card
- index.js
- Card.js
- CardDivider.js
- Button.js
- Typography.js
Теперь уже неважно то, сколько будет создано компонентов, связанных с компонентом Breadcrumb
. До тех пор, пока их файлы будут располагаться в той же директории, что и Breadcrumb.js
, мы будем знать о том, что они связаны с компонентом Breadcrumb
:
- src
- components
- Breadcrumb
- index.js
- Breadcrumb.js
- CollapsedSeparator.js
- Expander.js
- BreadcrumbText.js
- BreadcrumbHotdog.js
- BreadcrumbFishes.js
- BreadcrumbLeftOvers.js
- BreadcrumbHead.js
- BreadcrumbAddict.js
- BreadcrumbDragon0814.js
- BreadcrumbContext.js
- Input
- index.js
- Input.js
- utils.js
- focusManager.js
- Card
- index.js
- Card.js
- CardDivider.js
- Button.js
- Typography.js
Вот как работа с подобными структурами выглядит в коде:
import React from 'react'
import Breadcrumb, {
CollapsedSeparator,
Expander,
BreadcrumbText,
BreadcrumbHotdog,
BreadcrumbFishes,
BreadcrumbLeftOvers,
BreadcrumbHead,
BreadcrumbAddict,
BreadcrumbDragon0814,
} from '../../../../../../../../../../components/Breadcrumb'
const withBreadcrumbHotdog = (WrappedComponent) => (props) => (
)
const WorldOfBreadcrumbs = ({
BreadcrumbHotdog: BreadcrumbHotdogComponent,
}) => {
const [hasFishes, setHasFishes] = React.useState(false)
return (
(
{({ breadcrumbFishes }) => (
{JSON.stringify(results, null, 2)}
{hasFishes
? breadcrumbFishes.map ((fish) => (
<>
{fish}
>
))
: null}
)}
)}
/>
)
}
export default withBreadcrumbHotdog (WorldOfBreadcrumbs)
3. Давайте компонентам имена, используя стандартные соглашения об именовании
Использование неких стандартов при именовании компонентов упрощает тому, кто не является автором проекта, чтение кода этого проекта.
Например, к именам компонентов высшего порядка (higher order component, HOC) обычно добавляют префикс with
. К таким именам компонентов привыкли многие разработчики:
import React from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import getDisplayName from 'utils/getDisplayName'
const withFreeMoney = (WrappedComponent) => {
class WithFreeMoney extends React.Component {
giveFreeMoney() {
return 50000
}
render() {
return (
)
}
}
WithFreeMoney.displayName = `withFreeMoney(${getDisplayName(
WrappedComponent,
)}$)`
hoistNonReactStatics(WithFreeMoney, WrappedComponent)
return WithFreeMoney
}
export default withFreeMoney
Предположим, некто решит отступить от этой практики и сделать так:
import React from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import getDisplayName from 'utils/getDisplayName'
const useFreeMoney = (WrappedComponent) => {
class WithFreeMoney extends React.Component {
giveFreeMoney() {
return 50000
}
render() {
return (
)
}
}
WithFreeMoney.displayName = `useFreeMoney(${getDisplayName(
WrappedComponent,
)}$)`
hoistNonReactStatics(WithFreeMoney, WrappedComponent)
return WithFreeMoney
}
export default useFreeMoney
Это — совершенно работоспособный JavaScript-код. Имена тут составлены, с технической точки зрения, верно. Но префикс use
принято использовать в других ситуациях, а именно — при именовании хуков React. В результате, если некто пишет программу, которую планируется показывать кому-то ещё, ему стоит внимательно относиться к именам сущностей. Особенно это актуально для тех случаев, когда кто-то просит посмотреть его код и помочь ему решить какую-то проблему. Дело в том, что тот, кто будет читать чужой код, вполне возможно, уже привык к определённой схеме именования сущностей.
Отступления от общепринятых стандартов затрудняют понимание чужого кода.
4. Избегайте «ловушки логических значений»
Программисту стоит проявлять исключительную осторожность в том случае, если некие выходные данные зависят от каких-то примитивных логических значений, и на основе анализа этих значений принимаются какие-то решения. Подобное намекает на низкое качество кода. Это принуждает разработчиков вчитываться в код реализации компонентов или других механизмов для получения точного представления о том, какой именно смысл имеет то, что получается в результате работы этих механизмов.
Предположим, мы создали компонент Typography
, который может принимать следующие опции: 'h1'
, 'h2'
, 'h3'
, 'h4'
, 'h5
', 'h6'
, 'title'
, 'subheading'
.
Что именно повлияет на выходные данные компонента в том случае, если опции передаются ему в следующем виде?
const App = () => (
Welcome to my bio
)
Те, у кого есть определённый опыт в работе с React (или, скорее, c JavaScript), уже могут предположить, что опция title
перекроет опцию subheading
из-за особенностей работы системы. Последняя опция перезапишет первую.
Но проблема тут заключается в том, что мы не можем, не глядя в код, точно сказать о том, до каких пределов будет применена опция title
или опция subheading
.
Например:
.title {
font-size: 1.2rem;
font-weight: 500;
text-transform: uppercase;
}
.subheading {
font-size: 1.1rem;
font-weight: 400;
text-transform: none !important;
}
Даже хотя title
и выигрывает, CSS-правило text-transform: uppercase
применяться не будет. Происходит это из-за более высокой специфичности правила text-transform: none !important
, которое имеется в subheading
. Если не проявлять в таких ситуациях осторожность — отладка подобных ошибок в стилях может стать чрезвычайно сложным занятием. Особенно — в тех случаях, когда код не выводит в консоль неких предупреждений или сообщений об ошибках. Это может усложнить сигнатуру компонента.
Вот один из возможных вариантов решения этой проблемы — применение более чистого варианта компонента Typography
:
const App = () => Welcome to my bio
Вот код компонента Typography
:
import React from 'react'
import cx from 'classnames'
import styles from './styles.css'
const Typography = ({
children,
color = '#333',
align = 'left',
variant,
...otherProps
}) => {
return (
{children}
)
}
Теперь, когда в компоненте App
мы передаём компоненту Typography
variant="title"
, мы можем быть уверены в том, что на вывод компонента повлияет только title
. Это избавляет нас от необходимости анализа кода компонента, выполняемого для того, чтобы понять, как будет выглядеть то, что этот компонент выведет на экран.
Для работы со свойствами можно применить и простую конструкцию if/else
:
let result
if (variant === 'h1') result = styles.h1
else if (variant === 'h2') result = styles.h2
else if (variant === 'h3') result = styles.h3
else if (variant === 'h4') result = styles.h4
else if (variant === 'h5') result = styles.h5
else if (variant === 'h6') result = styles.h6
else if (variant === 'title') result = styles.title
else if (variant === 'subheading') result = styles.subheading
Но главная сильная сторона подобного подхода заключается в том, что можно просто использовать следующую чистую однострочную конструкцию и поставить на этом точку:
const result = styles[variant]
5. Используйте стрелочные функции
Стрелочные функции представляют собой лаконичный и ясный механизм объявления функций в JavaScript (в данном случае правильнее будет говорить о преимуществе стрелочных функций над функциональными выражениями).
Однако в некоторых случаях разработчики не пользуются стрелочными функциями вместо функциональных выражений. Например — тогда, когда нужно организовать поднятие функций.
В React эти концепции применяются похожим образом. Однако если программиста подъём функций не интересует, то, на мой взгляд, ему имеет смысл воспользоваться синтаксисом стрелочных функций:
// Версия с объявлением функции
function Gallery({ title, images = [], ...otherProps }) {
return (
{images.map((src, index) => (
))}
)
}
// Версия со стрелочной функцией или с функциональным выражением
const Gallery = ({ title, images = [], ...otherProps }) => (
{images.map((src, index) => (
))}
)
Надо отметить, что, анализируя этот пример, сложно увидеть сильные стороны стрелочных функций. Их красота в полной мере проявляется тогда, когда речь идёт о простых однострочных конструкциях:
// Версия с объявлением функции
function GalleryPage(props) {
return
}
// Версия со стрелочной функцией или с функциональным выражением
const GalleryPage = (props) =>
Уверен, что подобные однострочные конструкции всем придутся по душе.
6. Размещайте независимые функции за пределами собственных хуков
Мне доводилось видеть то, как некоторые программисты объявляют функции внутри собственных хуков, но при этом данные хуки не особенно нуждаются в таких функциях. Подобное слегка «раздувает» код хуков и усложняет его чтение. Сложности в чтении кода возникают из-за того, что его читатели могут начать задаваться вопросами о том, действительно ли хук зависит от функции, которая находится у него внутри. Если это не так — лучше переместить функцию за пределы хука. Это даст читателю кода чёткое понимание того, от чего хук зависит, а от чего — нет.
Вот пример:
import React from 'react'
const initialState = {
initiated: false,
images: [],
}
const reducer = (state, action) => {
switch (action.type) {
case 'initiated':
return { ...state, initiated: true }
case 'set-images':
return { ...state, images: action.images }
default:
return state
}
}
const usePhotosList = ({ imagesList = [] }) => {
const [state, dispatch] = React.useReducer(reducer, initialState)
const removeFalseyImages = (images = []) =>
images.reduce((acc, img) => (img ? [...acc, img] : acc), [])
React.useEffect(() => {
const images = removeFalseyImages(imagesList)
dispatch({ type: 'initiated' })
dispatch({ type: 'set-images', images })
}, [])
return {
...state,
}
}
export default usePhotosList
Если проанализировать этот код — можно понять, что функции removeFalseyImages
, на самом деле, необязательно присутствовать внутри хука.Она не взаимодействует с его состоянием, а значит — её вполне можно разместить за его пределами и без проблем вызывать из хука.
7. Будьте последовательны при написании кода
Последовательный подход к написанию кода — это то, что часто рекомендуют тем, кто программирует на JavaScript.
В случае с React стоит обратить внимание на последовательный подход к применению следующих конструкций:
- Команды импорта и экспорта.
- Именование компонентов, хуков, компонентов высшего порядка, классов.
Я, импортируя и экспортируя компоненты, иногда использую нечто подобное следующему:
import App from './App'
export { default as Breadcrumb } from './Breadcrumb'
export default App
Но мне нравится и такой синтаксис:
export { default } from './App'
export { default as Breadcrumb } from './Breadcrumb'
Что бы ни выбрал программист, ему стоит последовательно использовать это в каждом создаваемом им проекте. Это упрощает и работу этого программиста, и чтение его кода другими людьми.
Очень важно придерживаться и соглашений по именованию сущностей.
Например, если некто дал хуку имя useApp
, важно, чтобы и имена других хуков строились бы по похожей схеме — с использованием префикса use
. Например, имя ещё какого-нибудь хука при таком подходе может выглядеть как useController
.
Если не придерживаться этого правила, то код некоего проекта, в итоге, может оказаться примерно таким:
// хук #1
const useApp = ({ data: dataProp = null }) => {
const [data, setData] = React.useState(dataProp)
React.useEffect(() => {
setData(data)
}, [])
return {
data,
}
}
// хук #2
const basicController = ({ device: deviceProp }) => {
const [device, setDevice] = React.useState(deviceProp)
React.useEffect(() => {
if (!device && deviceProp) {
setDevice(deviceProp === 'mobile' ? 'mobile' : 'desktop')
}
}, [deviceProp])
return {
device,
}
}
Вот как выглядит импорт этих хуков:
import React from 'react'
import useApp from './useApp'
import basicController from './basicController'
const App = () => {
const app = useApp()
const controller = basicController()
return (
{controller.errors.map((errorMsg) => (
{errorMsg}
))}
)
}
export default App
С первого взгляда совершенно неочевидно то, что basicController
— это хук, такой же, как и useApp
. Это принуждает разработчика к чтению кода реализации того, что он импортирует. Делается это только для того, чтобы понять, с чем именно разработчик имеет дело. Если же последовательно придерживаться одной и той же стратегии именования сущностей, то подобной ситуации не возникнет. Всё окажется понятным с первого взгляда:
const app = useApp()
const controller = useBasicController()
Продолжение следует…
Уважаемые читатели! Как вы подходите к именованию сущностей в своих React-проектах?