14 советов по написанию чистого React-кода. Часть 1

в 9:30, , рубрики: javascript, React, ReactJS, Блог компании RUVDS.com, разработка, Разработка веб-сайтов

Написание чистого кода — это навык, который становится обязательным на определённом этапе карьеры программиста. Особенно этот навык важен тогда, когда программист пытается найти свою первую работу. Это, по существу, то, что делает разработчика командным игроком, и то, что способно либо «завалить» собеседование, либо помочь его успешно пройти. Работодатели, принимая кадровые решения, смотрят на код, написанный их потенциальными сотрудниками. Код, который пишет программист, должен быть понятен не только машинам, но и людям.

14 советов по написанию чистого React-кода. Часть 1 - 1

В материале, первую часть перевода которого мы публикуем сегодня, представлены советы по написанию чистого кода React-приложений. Актуальность этих советов тем выше, чем больше размер проекта, в котором применяются изложенные в них принципы. В маленьких проектах, вероятно, можно обойтись и без применения этих принципов. Принимая решение о том, что нужно в каждой конкретной ситуации, стоит руководствоваться здравым смыслом.

1. Деструктурируйте свойства

Деструктурирование свойств (в англоязычной терминологии React их называют «props») — это хороший способ сделать код чище и улучшить возможности по его поддержке. Дело в том, что это позволяет чётко выражать или объявлять то, что использует некая сущность (вроде компонента React). При этом такой подход не принуждает разработчиков вчитываться в реализацию компонента для того, чтобы выяснить состав свойств, связанных с ним.

Деструктурирование свойств, кроме того, даёт программисту возможность задавать их значения по умолчанию. Подобное встречается довольно-таки часто:

import React from 'react'
import Button from 'components/Button'

const MyComponent = ({ placeholder = '', style, ...otherProps }) => {
  return (
    <Button
      type="button"
      style={{
        border: `1px solid ${placeholder ? 'salmon' : '#333'}`,
        ...style,
      }}
      {...otherProps}
    >
      Click Me
    </Button>
  )
}

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) => (
  <WrappedComponent BreadcrumbHotdog={BreadcrumbHotdog} {...props} />
)

const WorldOfBreadcrumbs = ({
  BreadcrumbHotdog: BreadcrumbHotdogComponent,
}) => {
  const [hasFishes, setHasFishes] = React.useState(false)

  return (
    <BreadcrumbDragon0814
      hasFishes={hasFishes}
      render={(results) => (
        <BreadcrumbFishes>
          {({ breadcrumbFishes }) => (
            <BreadcrumbLeftOvers.Provider>
              <BreadcrumbHotdogComponent>
                <Expander>
                  <BreadcrumbText>
                    <BreadcrumbAddict>
                      <pre>
                        <code>{JSON.stringify(results, null, 2)}</code>
                      </pre>
                    </BreadcrumbAddict>
                  </BreadcrumbText>
                </Expander>
                {hasFishes
                  ? breadcrumbFishes.map((fish) => (
                      <>
                        {fish}
                        <CollapsedSeparator />
                      </>
                    ))
                  : null}
              </BreadcrumbHotdogComponent>
            </BreadcrumbLeftOvers.Provider>
          )}
        </BreadcrumbFishes>
      )}
    />
  )
}

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 (
        <WrappedComponent
          additionalMoney={[
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
          ]}
          {...this.props}
        />
      )
    }
  }

  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 (
        <WrappedComponent
          additionalMoney={[
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
            this.giveFreeMoney(),
          ]}
          {...this.props}
        />
      )
    }
  }

  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 = () => (
  <Typography color="primary" align="center" subheading title>
    Welcome to my bio
  </Typography>
)

Те, у кого есть определённый опыт в работе с 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 = () => <Typography variant="title">Welcome to my bio</Typography>

Вот код компонента Typography:

import React from 'react'
import cx from 'classnames'
import styles from './styles.css'

const Typography = ({
  children,
  color = '#333',
  align = 'left',
  variant,
  ...otherProps
}) => {
  return (
    <div
      className={cx({
        [styles.h1]: variant === 'h1',
        [styles.h2]: variant === 'h2',
        [styles.h3]: variant === 'h3',
        [styles.h4]: variant === 'h4',
        [styles.h5]: variant === 'h5',
        [styles.h6]: variant === 'h6',
        [styles.title]: variant === 'title',
        [styles.subheading]: variant === 'subheading',
      })}
    >
      {children}
    </div>
  )
}

Теперь, когда в компоненте 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 (
    <CarouselContext.Provider>
      <Carousel>
        {images.map((src, index) => (
          <img align="center" src={src} key={`img_${index}`} />
        ))}
      </Carousel>
    </CarouselContext.Provider>
  )
}

// Версия со стрелочной функцией или с функциональным выражением
const Gallery = ({ title, images = [], ...otherProps }) => (
  <CarouselContext.Provider>
    <Carousel>
      {images.map((src, index) => (
        <img align="center" src={src} key={`img_${index}`} />
      ))}
    </Carousel>
  </CarouselContext.Provider>
)

Надо отметить, что, анализируя этот пример, сложно увидеть сильные стороны стрелочных функций. Их красота в полной мере проявляется тогда, когда речь идёт о простых однострочных конструкциях:

// Версия с объявлением функции
function GalleryPage(props) {
  return <Gallery {...props} />
}

// Версия со стрелочной функцией или с функциональным выражением
const GalleryPage = (props) => <Gallery {...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 стоит обратить внимание на последовательный подход к применению следующих конструкций:

  1. Команды импорта и экспорта.
  2. Именование компонентов, хуков, компонентов высшего порядка, классов.

Я, импортируя и экспортируя компоненты, иногда использую нечто подобное следующему:

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 (
    <div>
      {controller.errors.map((errorMsg) => (
        <div>{errorMsg}</div>
      ))}
    </div>
  )
}

export default App

С первого взгляда совершенно неочевидно то, что basicController — это хук, такой же, как и useApp. Это принуждает разработчика к чтению кода реализации того, что он импортирует. Делается это только для того, чтобы понять, с чем именно разработчик имеет дело. Если же последовательно придерживаться одной и той же стратегии именования сущностей, то подобной ситуации не возникнет. Всё окажется понятным с первого взгляда:

const app = useApp()
const controller = useBasicController()

Продолжение следует…

Уважаемые читатели! Как вы подходите к именованию сущностей в своих React-проектах?

14 советов по написанию чистого React-кода. Часть 1 - 2

Автор: ru_vds

Источник


* - обязательные к заполнению поля