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

Автор оригинала: jsmanifest
  • Перевод
Сегодня мы публикуем вторую часть материала о написании чистого кода при разработке 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/'

Глядя на этот код кто-то может подумать, что особого порядка тут не наблюдается. Ведь импортированные сущности даже не отсортированы по алфавиту. Но расстановка чего-либо в алфавитном порядке — лишь часть той схемы упорядочения команд, которой я пользуюсь.

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

  1. Импорт React.
  2. Импорт библиотек (в алфавитном порядке).
  3. Абсолютные команды импорта сущностей из проекта (в алфавитном порядке).
  4. Относительные команды импорта (в алфавитном порядке).
  5. Команды вида import * as.
  6. Команды вида import './<some file>.<some ext>'.

А вот как я предпочитаю организовывать, например, переменные. Скажем — свойства объектов:

const character = (function() {
  return {
    cry() {
      //
    },
    eat() {
      //
    },
    hop() {
      //
    },
    jump() {
      //
    },
    punch() {
      //
    },
    run() {
      //
    },
    scratch() {
      //
    },
    scream() {
      //
    },
    sleep() {
      //
    },
    walk() {
      //
    },
    yawn() {
      //
    },
  }
})()

Если следовать неким правилам упорядочения сущностей при написании кода — это благотворно скажется на его чистоте.

Итоги


Мы представили вашему вниманию советы по написанию чистого кода React-приложений. Надеемся, вы нашли среди них что-то такое, что вам пригодится.

Уважаемые читатели! Какими рекомендациями вы дополнили бы советы, представленные в этом материале?

  • +26
  • 8,4k
  • 5
RUVDS.com
1 100,25
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Комментарии 5

    +7
    Пожалуйста, не делайте так:

    8.
    const SomeComponent = ({ classes, xs = 12, sm = 6, md, lg }) => {
      const BodySection = ({ header, src }) => {
        return (
          <Section height={500}>
            ...
          </Section>
        )
      }
    
      return (
        <Body noBottom>
          <Background grey>
            <BodySection header="Groups" src={photos.groups} />
          </Background>
          <div>
            <BodySection header="Labs" src={photos.labs} />
          </div>
        </Body>
      )
    }

    Не объявляйте компоненты внутри компонента.
    Особенно внимательно отнеситесь к компонентам, которые принимают в одном из пропсов имя компонента, как например
    <Route path="..." component={SomeComponent} />

    Туда также не нужно передавать компонент, объявленный там же, например:
    <Route path="..." component={props => <..../>} />

    Все компоненты нужно объявлять на верхнем уровне.

    В противном случае вы сломаете рендер реакта в этом месте и реакт будет всегда перестараивать дерево DOM, на каждый рендер. Если у такого компонента будут дочерние компоненты (children), они будут рендерится с чистым стейтом каждый раз, на каждый проход рендера. Фактически, станут stateless.

    Т.е. например input внутри такого BodySection вы уже использовать не сможете.

    Если вам наоборот, необходимо уничтожать стейт, воспользуйтесь явным способом, например указывая key для компонента. Как только key изменится, DOM дерево для этого компонента будет разрушено и создано заново.
      0
      Поддерживаю, вот хуки добавили как более «упрощенный» (добавил в кавычки потому что я в это не верю) вариант создания компонентов, на этом-же примере можно увидеть как можно легко не заметить создания на лету новых компонентов и убить производительность, все это из за того что поощряться создание функции внутри других функции. Конечно маловероятно что опытные разработчики могут так ошибиться, но вот для начинающих это вполне обычная ошибка, по моему (скромному) мнению хуки трудней объяснить новичкам чем те-же классы с их стейтом, но почему-то их впихивают везде и всюду, как панацея от всех проблем, чем они не являются.
        0
        Гхм. Я дико извиняюсь, я так-то фанат хуков.
        Я согласен что они недоделанные, но всё же мне они импонируют много больше классов.
          0
          Я тоже извиняюсь, не знаю что меня заставило писать этот комментарии, наверное из за аллергии на хуки или просто искал повод по-говорить о наболевшей теме. Просто я преподаю Реакт разработку и вижу как новички воспринимают все что связанно с Реактом, становится довольно трудно аргументированно объяснить что лучше когда встречается такой дуализм как подход с хуками и подход с классами (особенно когда тебе одно из двух не очень нравится :D).
          0

          Хуки конечно имеют как свои плюсы, так свои и минусы.
          И было бы отлично, и интересно посмотреть, на неправильное использование функциональных компонент, с небольшими примерами. А если будет объяснение, "почему именно так делать плохо", то сообщество пополнится правильными знаниями.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое