10 ошибок при разработке React приложений

<p>React заслуженно считается одним из лидеров в области фронтенда. Не важно, знаете вы эту библиотеку от и до, или только начинаете, статья обязательна к прочтению. Наверняка вы найдете, что исправить в вашем коде.</p>

05 / 07 / 19

1. Не зацикливайтесь только на своей работе


Если все время уходит на написание кода, можно забыть о сообществе разработчиков. Вдруг процесс программирования неэффективен, и вы об этом даже не знаете? Этого легко избежать, общаясь с другими разработчиками.

Когда React выпустил хуки, я создал много минипроектов, чтобы попробовать их в деле и понять, почему они так популярны. Со временем я стал вставлять их и в рабочие проекты. Код был полон useState и useEffect. Но потом я наткнулся на твит об удобстве useReducer. После ресерча я понял, что придется потратить кучу времени на рефакторинг.

React

2. Не используйте .bind вместо конструкторов компонентов класса


Большинство разработчиков считают, что нужно использовать .bind с методами класса. Это хорошо работает со стрелочными функциями. С inline функциями возникают проблемы. Они определяются методом рендеринга и являются пропсом для дочернего элемента. Когда это происходит, React назначает новый экземпляр функции с каждым повторным рендерингом. Это значительно снижает производительность.

Например:

const ShowMeTheMoney = () => {
  const [money, setMoney] = useState(0)

  const showThemTheMoney = (money) => {
    setMoney(money)
  }

  const hideTheMoney = () => {
    setMoney(null)
  }

  const sayWhereTheMoneyIs = (msg) => {
    console.log(msg)
  }

  return (
    <div>
      <h4>Where is the money?</h4>
      <hr />
      <div style={{ display: 'flex', alignItems: 'center' }}>
        <SomeCustomButton
          type="button"
          onClick={() => sayWhereTheMoneyIs("I don't know")}
        >
          I'll tell you
        </SomeCustomButton>{' '}
        <SomeCustomButton type="button" onClick={() => showThemTheMoney(0.05)}>
          I'll show you
        </SomeCustomButton>
      </div>
    </div>
  )
}

Известно, что onClick={() => sayWhereTheMoneyIs("I don't know")} и onClick={() => showThemTheMoney(0.05)} это inline функции.

Многие туториалы рекомендуют делать следующее:

return (
  <div>
    <h4>Where is the money?</h4>
    <hr />
    <div style={{ display: 'flex', alignItems: 'center' }}>
      <SomeCustomButton
        type="button"
        onClick={sayWhereTheMoneyIs.bind(null, "I don't know")}
      >
        I'll tell you
      </SomeCustomButton>{' '}
      <SomeCustomButton
        type="button"
        onClick={showThemTheMoney.bind(null, 0.05)}
      >
        I'll show you
      </SomeCustomButton>
    </div>
  </div>
)

На первый взгляд кажется, что этот код кеширует ссылку. В действительности он создает новые функции на каждом этапе рендеринга. Этого антипаттерна следует избегать.

3. Не передавайте динамические значения как ключи для дочерних элементов


Иногда кажется, что давать уникальные ключи для дочерних элементов не обязательно.

На деле они очень полезны:

const Cereal = ({ items, ...otherProps }) => {
  const indexHalf = Math.floor(items.length / 2)
  const items1 = items.slice(0, indexHalf)
  const items2 = items.slice(indexHalf)
  return (
    <>
      <ul>
        {items1.map(({ to, label }) => (
          <li key={to}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
      <ul>
        {items2.map(({ to, label }) => (
          <li key={to}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
    </>
  )
}

Представьте, что некоторые значения to из items1 и items2 оказались одинаковыми. Когда нужно рефакторить подобный компонент, обычно делают что-то подобное:

import { generateRandomUniqueKey } from 'utils/generating'

const Cereal = ({ items, ...otherProps }) => {
  const indexHalf = Math.floor(items.length / 2)
  const items1 = items.slice(0, indexHalf)
  const items2 = items.slice(indexHalf)
  return (
    <>
      <ul>
        {items1.map(({ to, label }) => (
          <li key={generateRandomUniqueKey()}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
      <ul>
        {items2.map(({ to, label }) => (
          <li key={generateRandomUniqueKey()}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
    </>
  )
}

Это работает. Но тут есть пара ошибок:

  • Из-за того, что сгенерированный ключ всегда разный, React создает все узлы заново при каждом рендеринге.
  • Ключи работают как идентификаторы. А для того, чтобы идентифицировать компонент все ключи должны быть уникальными.

Такой вариант работает лучше:

import { generateRandomUniqueKey } from 'utils/generating'

const Cereal = ({ items, ...otherProps }) => {
  const indexHalf = Math.floor(items.length / 2)
  const items1 = items.slice(0, indexHalf)
  const items2 = items.slice(indexHalf)
  return (
    <>
      <ul>
        {items1.map(({ to, label }) => (
          <li key={`items1_${to}`}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
      <ul>
        {items2.map(({ to, label }) => (
          <li key={`items2_${to}`}>
            <Link to={to}>{label}</Link>
          </li>
        ))}
      </ul>
    </>
  )
}

Теперь каждому компоненту будет присвоено свое значение ключа. Это сохраняет их идентичность.

4. Не объявляйте параметры по умолчанию вместо null


Иногда приходится отлаживать что-то вроде этого:

const SomeComponent = ({ items = [], todaysDate, tomorrowsDate }) => {
  const [someState, setSomeState] = useState(null)

  return (
    <div>
      <h2>Today is {todaysDate}</h2>
      <small>And tomorrow is {tomorrowsDate}</small>
      <hr />
      {items.map((item, index) => (
        <span key={`item_${index}`}>{item.email}</span>
      ))}
    </div>
  )
}

const App = ({ dates, ...otherProps }) => {
  let items
  if (dates) {
    items = dates ? dates.map((d) => new Date(d).toLocaleDateString()) : null
  }

  return (
    <div>
      <SomeComponent {...otherProps} items={items} />
    </div>
  )
}

Согласно этому коду, если dates будет ложно, то оно инициализируется как null.

Логично предположить, что если items ложно, то оно инициализируется в пустой массив по умолчанию. Но приложение вылетает, когда dates ложно, потому что items равно нулю. Звучит сложно.

Разберемся с функциями по умолчанию. Они позволяют названным параметрам инициализироваться со значениями по умолчанию, если они не определены или равны нулю.

В этом примере null ложно, но это все равно значение.

Такие ошибки отлаживаются очень долго.

5. Не игнорируйте повторяющийся код


Часто хочется просто скопировать и вставить повторяющиеся части кода. Особенно когда дедлайн близок, а фикс нужен прямо сейчас.

Возьмем такой пример:

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>
    <Background grey>
      <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>
    </Background>
    <Background grey>
      <Section height={300}>
        <Grid spacing={16} container>
          <Grid xs={12} sm={6} item>
            <div className={classes.partnersHeader}>
              <Header center>Partners</Header>
            </div>
          </Grid>
          <Grid xs={12} sm={6} item>
            <div>
              <img src={photos.partners} alt="" className={classes.img} />
            </div>
          </Grid>
        </Grid>
      </Section>
    </Background>
  </Body>
)

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

Удобнее использовать вместо повторов слегка отличающиеся пропсы.

const SectionContainer = ({
  bgProps,
  height = 500,
  header,
  headerProps,
  imgProps,
}) => (
  <Background {...bgProps}>
    <Section height={height}>
      <Grid spacing={16} container>
        <Grid xs={12} sm={6} item>
          <div {...headerProps}>
            <Header center>{header}</Header>
          </div>
        </Grid>
        <Grid xs={12} sm={6} item>
          <div>
            <img {...imgProps} />
          </div>
        </Grid>
      </Grid>
    </Section>
  </Background>
)

const SomeComponent = () => (
  <Body noBottom>
    <Header center>Title</Header>
    <Divider />
    <SectionContainer
      header="Groups"
      headerProps={{ className: classes.groupsHeader }}
      imgProps={{ src: photos.groups, className: classes.img }}
    />
    <SectionContainer
      bgProps={{ grey: true }}
      header="Labs"
      headerProps={{ className: classes.labsHeader }}
      imgProps={{ src: photos.labs, className: classes.img }}
    />
    <SectionContainer
      height={300}
      header="Partners"
      headerProps={{ className: classes.partnersHeader }}
      imgProps={{ src: photos.partners, className: classes.img }}
    />
  </Body>
)

Теперь если руководитель передумает и захочет сделать все секции 300 пикселей в длину, нужно будет изменить всего одну строку.

Но и у этого варианта есть минусы. Оно подходит только тогда, когда известно, что компоненты будут повторяться только в этой среде. Более динамичным решением, которое можно будет использовать много раз, будет что-то вроде этого:

const SectionContainer = ({
  bgProps,
  sectionProps,
  children,
  gridContainerProps,
  gridColumnLeftProps,
  gridColumnRightProps,
  columnLeft,
  columnRight,
}) => (
  <Background {...bgProps}>
    <Section {...sectionProps}>
      {children || (
        <Grid spacing={16} container {...gridContainerProps}>
          <Grid xs={12} sm={6} item {...gridColumnLeftProps}>
            {columnLeft}
          </Grid>
          <Grid xs={12} sm={6} item {...gridColumnRightProps}>
            {columnRight}
          </Grid>
        </Grid>
      )}
    </Section>
  </Background>
)

Это позволяет расширять любую часть компонента, не меняя при этом изначальную реализацию.

6. Не инициализируйте пропсы в конструкторе


Часто состояние инициализируется в конструкторе:

import React from 'react'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      items: props.items,
    }
  }
}

Результат такого подхода нередко сопровождается багами. Это происходит из-за того, что конструктор вызывается только один раз при создании компонента. Поэтому когда меняются пропсы, состояние остается неизменным.

Чтобы они синхронизировались, лучше использовать подобное решение:

import React from 'react'

class App extends React.Component {
  constructor(props) {
    super(props)
    // Initialize the state on mount
    this.state = {
      items: props.items,
    }
  }

  // Keep the state in sync with props in further updates
  componentDidUpdate = (prevProps) => {
    const items = []
    // after  calculations comparing prevProps with this.propsНе делайте
    if (...) {
      this.setState({ items })
    }
  }
}


7. Помните об опасностях условного рендеринга с &&


Очень распространенная ошибка заключается в использовании оператора && при условном рендеринге.

React будет пытаться рендерить все альтернативные результаты, если условие не будет выполняться.

Например:

const App = ({ items = [] }) => (
  <div>
    <h2>Here are your items:</h2>
    <div>
      {items.length &&
        items.map((item) => <div key={item.label}>{item.label}</div>)}
    </div>
  </div>
)

Когда items.length будет пустым рендериться будет число 0. JavaScript воспринимает его как ложное значение. Поэтому, когда items — пустой массив, оператор && не будет вычислять выражение справа от него и просто вернет первое значение.

В таких случаях можно использовать двойное отрицание:

const App = ({ items = [] }) => (
  <div>
    <h2>Here are your items:</h2>
    <div>
      {!!items.length &&
        items.map((item) => <div key={item.label}>{item.label}</div>)}
    </div>
  </div>
)

Таким образом, если items будет пустым массивом, React не будет ничего рендерить при условии, что результат — булево значение.

8. Не забывайте распространять прошлые состояния


Если реализовать логику обновления состояния небрежно, то непременно возникнут баги.

Например, при реализации хука useReducer возникла следующая проблема:

const something = (state) => {
  let newState = { ...state }
  const indexPanda = newState.items.indexOf('panda')
  if (indexPanda !== -1) {
    newState.items.splice(indexPanda, 1)
  }
  return newState
}

const initialState = {
  items: [],
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'add-item':
      return { ...state, items: [...something(state).items, action.item] }
    case 'clear':
      return { ...initialState }
    default:
      return state
  }
}

Когда функция something запускает и переписывает состояние, свойство items не меняется. Поэтому когда мы используем .splice, state.items мутирует и вызывает баги.

В длинном коде это особенно опасно. О прошлых состояниях легко забыть, но они часто могут вас спасти.

9. Передавайте пропсы дочерним элементам


Этому есть несколько причин:

  1. Более простой процесс отладки.
  2. Понимание, для чего именно нужен каждый элемент.
  3. Меньше пропсов необходимо для рендеринга.

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

const Parent = (props) => {
  if (props.user && props.user.email) {
    // Fire some redux action to update something globally that another
    //    component might need to know about
  }

  // Continue on with the app
  return <Child {...props} />
}

Главное, чтобы не случилось такого:

<ModalComponent
  open={aFormIsOpened}
  onClose={() => closeModal(formName)}
  arial-labelledby={`${formName}-modal`}
  arial-describedby={`${formName}-modal`}
  classes={{
    root: cx(classes.modal, { [classes.dialog]: shouldUseDialog }),
    ...additionalDialogClasses,
  }}
  disableAutoFocus
>
  <div>
    {!dialog.opened && (
      <ModalFormRoot
        animieId={animieId}
        alreadySubmitted={alreadySubmitted}
        academy={academy}
        user={user}
        clearSignature={clearSignature}
        closeModal={closeModal}
        closeImageViewer={closeImageViewer}
        dialog={dialog}
        fetchAcademyMember={fetchAcademyMember}
        formName={formName}
        formId={formId}
        getCurrentValues={getCurrentValues}
        header={header}
        hideActions={formName === 'signup'}
        hideClear={formName === 'review'}
        movieId={movie}
        tvId={tvId}
        openPdfViewer={openPdfViewer}
        onSubmit={onSubmit}
        onTogglerClick={onToggle}
        seniorMember={seniorMember}
        seniorMemberId={seniorMemberId}
        pdfViewer={pdfViewer}
        screenViewRef={screenViewRef}
        screenRef={screenRef}
        screenInputRef={screenInputRef}
        updateSignupFormValues={updateSignupFormValues}
        updateSigninFormValues={updateSigninFormValues}
        updateCommentFormValues={updateCommentFormValues}
        updateReplyFormValues={updateReplyFormValues}
        validateFormId={validateFormId}
        waitingForPreviousForm={waitingForPreviousForm}
        initialValues={getCurrentValues(formName)}
        uploadStatus={uploadStatus}
        uploadError={uploadError}
        setUploadError={setUploadError}
        filterRolesFalseys={filterRolesFalseys}
      />
    )}
  </div>
</ModalComponent>

А если такое все же случается, попробуйте разделить один компонент на несколько.

10. Prop Drilling


Prop drilling это когда родительский компонент передает пропсы сразу нескольким дочерним на разных уровнях дерева. Проблема не в родительском, не в дочернем, а в том что между ними. 

При повторном рендеринге первого, также по очереди рендерятся и все дочерние. Чтобы этого избежать можно использовать context либо redux.


10 Things NOT To Do When Building React Applications

4.5

Оставьте свой e-mail и получайте свежие статьи первыми.

instagramlinkedinFBtwitter