Пожаловаться

8 плохих практик JavaScript, которые 100% приведут к сбоям

6962
Пожаловаться

Разобрали восемь практик JavaScript, в рамках которых нужно быть максимально внимательным, ведь они могут вызвать сбой в вашем приложении.

Позаботьтесь о пользователях, и они позаботятся о вас

К некоторым реализациям кода нужно относиться осторожно. Рассмотрим 8 плохих практик JavaScript, которые могут привести к серьёзным сбоям.

1. Сбой из-за 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 имеет значение falsey, то при инициализации компонент будет иметь значение null.

Если items имеет значение falsey, то нужно инициализировать items в пустой массив по умолчанию. Но при dates со значением falsey происходит сбой именно из-за items со значением null.

Что делать?

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

В нашем случае null является значением, несмотря на falsey.

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

2. Заключение свойств в квадратные скобки

Наверное, самая не очевидная из всех плохих практик JavaScript.

Даже выбор скобок может вызвать сбой. Вот пример поиска объекта с квадратными скобками:

const someFunction = function() {
  const store = {
    people: {
      joe: {
        age: 16,
        gender: 'boy',
      },
      bob: {
        age: 14,
        gender: 'transgender',
      }
    }
  }
  return {
    getPersonsProfile(name) {
      return store.people[name]
    },
    foods: ['apple', 'pineapple'],
  }
}

const obj = someFunction()
const joesProfile = obj.getPersonsProfile('joe')

console.log(joesProfile)
/*
  result: 
      {
        age: 16,
        gender: boy,
      }
*/

В этом примере всё сделано верно. Минус в том, что такой код выполняется медленнее, чем поиск ключа объекта.

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

const someFunction = function () {
  const store = {
    people: {
      joe: {
        age: 16,
        gender: 'boy',
      },
      bob: {
        age: 14, 
      gender: 'transgender',
      }
    }
  }
  return {
    getPersonsProfile(name) {
      return store.people[name]
    },
    foods: ['apple', 'pineapple'],
  }
}

const obj = someFunction()
const joesProfile = obj.getPersonsProfile('Joe')
const joesAge = joesProfile.age

console.log(joesAge)

Если при редактировании кода вы или ваш коллега допустили случайную ошибку — написали Joe с большой буквы вместо маленькой — JavaScript тут же возвращает undefined:

"TypeError: Cannot read property 'age' of undefined
    at tibeweragi.js:24:29
    at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:13924
    at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:10866"

Приложение вылетит в тот момент, когда начнётся поиск свойства.

Так, joesProfile (который затем окажется undefined) находится в коде, и нет никакой возможности узнать об этом. Всё выяснится только при сбое, который произойдёт во время поиска какого-либо свойства. Например, joesProfile.age.

Некоторые разработчики инициализируют верное возвращаемое значение по умолчанию при неудачном поиске:

const store = {
    people: {
      joe: {
        age: 16,
        gender: 'boy',
      },
      bob: {
        age: 14, 
      gender: 'transgender',
      }
    }
  }
  return {
    getPersonsProfile(name) {
      return store.people[name] || {}
    },
    foods: ['apple', 'pineapple'],
  }
}

По крайней мере, так не произойдёт краша. Если используете поиск в квадратных скобках, продумайте, что должно произойти при неудачном поиске.

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

import { createSelector } from 'reselect'

// supports passing in the whole obj or just the string to correct the video type
const fixVideoTypeNaming = (videoType) => {
  let video = videoType
  
  // If video is a video object
  if (video && typeof video === 'object') {
    const media = { ...video }
    video = media.videoType
  }
  
  // If video is the actual videoType string
  if (typeof video === 'string') {
    // fix the typo because brian is an idiot
    if (video === 'mp3') {
      video = 'mp4'
    }
  }
  
  return video
}

/* -------------------------------------------------------
  ---- Pre-selectors
-------------------------------------------------------- */

export const getOverallSelector = (state) =>
  state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total
    .overall
    
export const getSpecificWeekSelector = (state, props) =>
  state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.weekly[
    props.date
  ]
  
/* -------------------------------------------------------
  ---- Selectors
-------------------------------------------------------- */

export const getWeeklyCycleSelector = createSelector(
  getSpecificWeekSelector,
  (weekCycle) => weekCycle || null,
)

export const getFetchingTotalStatusSelector = createSelector(
  (state) =>
    state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total
      .fetching,
  (fetching) => fetching,
)

export const getFetchErrorSelector = createSelector(
  (state) =>
    state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total
      .fetchError,
  (fetchError) => fetchError,
)

fixVideoTypeNaming — это функция, которая определяет тип видео на основе значений, переданных в качестве аргументов. Если аргумент — видеофайл, то функция определит тип видео с помощью свойства .videoType. Если аргумент — строка, то вызывающая функция переходит к videoType, мы пропускаем первый шаг. Выяснилось, что videoType .mp4property несколько раз в приложении было написано неправильно. Чтобы быстро исправить эту проблему с опечаткой, в качестве временного решения использовалось fixVideoTypeNaming.

Как вы уже догадались, приложение написано с помощью Redux — отсюда такой синтаксис.

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

const withTotalCount = (WrappedComponent) => {
  class WithTotalCountContainer extends React.Component {
    componentDidMount = () => {
      const { total, dispatch } = this.props
      if (total == null) {
        dispatch(fetchTotalVideoTypeCount())
      }
    }
    
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
  
  WithTotalCountContainer.propTypes = {
    fetching: PropTypes.bool.isRequired,
    total: PropTypes.number,
    fetchError: PropTypes.object,
    dispatch: PropTypes.func.isRequired,
  }
  
  WithTotalCountContainer.displayName = `withTotalCount(${getDisplayName(
    WrappedComponent,
  )})`
  
  return connect((state) => {
    const videoType = fixVideoTypeNaming(state.app.media.video.videoType)
    const { fetching, total, fetchError } = state.app.media.video[
      videoType
    ].options.total
    return { fetching, total, fetchError }
  })(WithTotalCountContainer)
}

Компонент пользовательского интерфейса:

const TotalVideoCount = ({ classes, total, fetching, fetchError }) => {
  if (fetching) return <LoadingSpinner />
  
  const hasResults = !!total
  const noResults = fetched && !total
  const errorOccurred = !!fetchError
  
  return (
    <Typography
      variant="h3"
      className={classes.root}
      error={!!fetched && !!fetchError}
      primary={hasResults}
      soft={noResults || errorOccurred}
      center
    >
      {noResults && 'No Results'}
      {hasResults && `$${formatTotal(total)}`}
      {errorOccurred && 'An error occurred.'}
    </Typography>
  )
}

HOC передаёт все пропсы этому компоненту, он получает их и отображает информацию, адаптируя данных от пропсов. В теории всё должно отлично работать, но так происходит не всегда.

Если мы вернёмся к контейнерам и посмотрим, как селекторы выбирают значения, мы поймём, в чём здесь подвох:

export const getOverallSelector = (state) =>
  state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.total
    .overall
    
export const getSpecificWeekSelector = (state, props) =>
  state.app[fixVideoTypeNaming(state.app.media.video.videoType)].options.weekly[
    props.date
  ]

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

Например, state.app.media.video.videoType представляет собой цепочку из четырёх уровней. Допустим, что другой разработчик вносил правки в код и допустил ошибку —  state.app.media.video стал со значением undefined. В таком случае приложение зависнет, потому что оно не воспринимает videoType со значением undefined.

Если вы допустите ещё одну опечатку в videoType, если не устраните её с помощью fixVideoTypeNaming, если возникнут проблемы с mp3 — во всех этих случаях произойдёт ещё один сбой. Проблема в том, что обнаружить все эти проблемы вы сможете только тогда, когда пользователь с ними столкнётся. К тому времени, может быть уже слишком поздно.

Будьте внимательнее. Не надейтесь, что в вашем приложении никогда таких багов не будет.

3. Краш приложения при проверке пустых объектов при рендеринге

Я тоже так делал, когда при условном рендеринге нужно было проверять, содержат ли данные какие-либо объекты. Для этого я использовал Object.keys. Если в данных были объекты, то компонент продолжал рендеринг, если поставленное условие соблюдалось:

const SomeComponent = ({ children, items = {}, isVisible }) => (
  <div>
    {Object.keys(items).length ? (
      <DataTable items={items} />
    ) : (
      <h2>Data has not been received</h2>
    )}
  </div>
)

Давайте представим, что мы вызвали какой-то API и получили в ответ items в качестве объекта. На первый взгляд кажется, что всё нормально. Ожидаемый тип items — объект, поэтому для работы с ним отлично подходит Object.keys. Затем мы инициализируем items в виде пустого объекта —  на случай бага, который может вернуть items со значением falsey.

Но что, если items в будущем станет массивом? Object.keys(items) не вызовет краш приложения, но вернёт что-то странное, например, ["0", "1", "2"]. Как думаете, как поведут себя компоненты при рендеринге с такими данными?

Проблема сниппета в том, что если в пропсах у items будет значение null, то items не инициализируется с введённым вами дефолтным значением.

Таким образом, приложение вылетит, так что будьте осторожнее:

"TypeError: Cannot convert undefined or null to object
    at Function.keys (<anonymous>)
    at yazeyafabu.js:4:45
    at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:13924
    at https://static.jsbin.com/js/prod/runner-4.1.7.min.js:1:10866"

4. Вылет при проверке наличия массивов перед рендерингом

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

render() {
  const { arr } = this.props
  return (
    <div>
      {arr && arr.map()...}
    </div>
  )
}

Оператор &&amp; воспримет объектный литерал как truthy и применит функцию .map, что приведёт к сбою.

Будьте внимательны — внимательность сохранит вам время и силы, они понадобятся для решения более сложных и важных проблем.

5. Работа без линтера

Линтер — это чрезвычайно полезный инструмент. Я пользуюсь ESLint, популярным линтером для JavaScript: он помогает найти ошибки в коде, даже не выполняя его. Он помогает исправить ошибки в реальном времени, как бы выполняя роль наставника. Он объясняет, что с кодом не так и что нужно сделать, чтобы это исправить.

Приведу пример:

 

8 плохих практик React, которые 100% приведут к сбоям

Ещё одна функция ESLint — если вы не согласны с какими-либо правилом проверки, можете просто отключить его. Тогда ESLint не будет помечать это как ошибку. Удобно, правда?

6. Деструктуризация при рендеринге списков

Такое иногда происходит, но этот баг не всегда легко обнаружить. Допустим, у вас есть список элементов, и вы хотите провести рендеринг группы компонентов для каждого элемента из этого списка. Если значение одного из элементов списка не соответствует ожидаемому, приложение вылетает — оно не знает, что делать с таким типом значения.

Приведу пример:

const api = {
  async getTotalFrogs() {
    return {
      data: {
        result: [
          { name: 'bob the frog', tongueWidth: 50, weight: 8 },
          { name: 'joe the other frog', tongueWidth: 40, weight: 5 },
          { name: 'kelly the last frog', tongueWidth: 20, weight: 2 },
        ],
      },
    }
  },
}

const getData = async ({ withTongues = false }) => {
  try {
    const response = await api.getTotalFrogs({ withTongues })
    return response.data.result
  } catch (err) {
    throw err
  }
}

const DataList = (props) => {
  const [items, setItems] = useState([])
  const [error, setError] = useState(null)
  
  React.useEffect(() => {
    getData({ withTongues: true })
      .then(setItems)
      .catch(setError)
  }, [])
  
  return (
    <div>
      {Array.isArray(items) && (
        <Header size="tiny" inverted>
          {items.map(({ name, tongueWidth, weight }) => (
            <div style={{ margin: '25px 0' }}>
              <div>Name: {name}</div>
              <div>Width of their tongue: {tongueWidth}cm</div>
              <div>Weight: {weight}lbs</div>
            </div>
          ))}
        </Header>
      )}
      {error && <Header>You received an error. Do you need a linter?</Header>}
    </div>
  )
}

 

8 плохих практик React, которые 100% приведут к сбоям

Всё идёт хорошо, код работает. Перейдём к вызову API. И вместо этого…

const api = {
  async getTotalFrogs() {
    return {
      data: {
        result: [
          { name: 'bob the frog', tongueWidth: 50, weight: 8 },
          { name: 'joe the other frog', tongueWidth: 40, weight: 5 },
          { name: 'kelly the last frog', tongueWidth: 20, weight: 2 },
        ],
      },
    }
  },
}

…мы получаем это. Что делать, если в клиенте API возникла проблема с обработкой потока данных, из-за которой код вернул этот массив?

const api = {
  async getTotalFrogs() {
    return {
      data: {
        result: [
          { name: 'bob the frog', tongueWidth: 50, weight: 8 },
          undefined,
          { name: 'kelly the last frog', tongueWidth: 20, weight: 2 },
        ],
      },
    }
  },
}

Приложение не знает, что делать с таким типом значения:

Uncaught TypeError: Cannot read property 'name' of undefined
    at eval (DataList.js? [sm]:65)
    at Array.map (<anonymous>)
    at DataList (DataList.js? [sm]:64)
    at renderWithHooks (react-dom.development.js:12938)
    at updateFunctionComponent (react-dom.development.js:14627)

Чтобы предотвратить сбой, установите объект по умолчанию на каждой итерации:

{
  items.map(({ name, tongueWidth, weight } = {}) => (
    <div style={{ margin: '25px 0' }}>
      <div>Name: {name}</div>
      <div>Width of their tongue: {tongueWidth}cm</div>
      <div>Weight: {weight}lbs</div>
    </div>
  ))
}

Теперь пользователи не будут осуждать вас — всё работает:

8 плохих практик React, которые 100% приведут к сбоям

 

Мы предотвратили сбой, но я бы посоветовал разобраться с пропущенными значениями.  Алгоритм в этом случае похож на решение проблемы с возвращением null для целых элементов, так как в обоих случаях нет никаких данных.

7. Недостаточно подробное исследование перед реализацией

Другой случай из плохих практик JavaScript. Раньше я тоже допускал подобную ошибку — был слишком уверен в своих знаниях, когда реализовывал поисковый запрос.

На самом деле, реализовать компонент поискового запроса было достаточно просто. Проблема заключалась в символах в запросах.

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

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

const hasInvalidChars = /^.*?(?=[\+\^#%&$\*:<>\?/\{\|\}\[\]\\\)\(]).*$/g.test(
  inputValue,
)

Это пример самого распространённого и современного регулярного выражения для поискового API.

Раньше было так:

const hasInvalidChars = /^.*?(?=[\+\^#%&$\*:<>\?/\{\|\}\[\]\)\(]).*$/g.test(
  inputValue,
)

const callApi = async (keywords) => {
  try {
    const url = `https://someapi.com/v1/search/?keywords=${keywords}/`
    return api.searchStuff(url)
  } catch (error) {
    throw error
  }
}

Как вы видите, здесь нет слэша / — это может привести к сбоям приложения. Угадайте, что случится, если отправить этот символ поисковому API.

Если честно, я бы не стал на сто процентов доверять примерам из интернета. Во-первых, большинство из них не были тщательно протестированы. Во-вторых, когда речь идёт о регулярных выражениях, нет никакого универсального решения, которое подойдёт во всех случаях.

8. Неограниченный размер загружаемых файлов

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

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

Покажу на примере, как ограничить размер файла 5 мегабайтами (5,000,000 байт):

import React, { useState, useEffect } from 'react'

const useUploadStuff = () => {
  const [files, setFiles] = useState([])
  
  // Limit the file sizes here
  const onChange = (e) => {
    const arrFiles = Array.from(e.target.files)
    const filesUnder5mb = arrFiles.filter((file) => {
      const bytesLimit = 5000000
      if (file.size > bytesLimit) {
        // optionally process some UX about this file size
      }
      return file.size < bytesLimit
    })
    setFiles(filesUnder5mb)
  }
  
  useEffect(() => {
    if (files.length) {
      // do something with files
    }
  }, [files])
  
  return {
    files,
    onChange,
  }
}

const UploadStuff = () => {
  const { onChange } = useUploadStuff()
  
  return (
    <div>
      <h2 style={{ color: '#fff' }}>Hi</h2>
      <div>
        <input
          style={{ color: '#fff' }}
          onChange={onChange}
          type="file"
          placeholder="Upload Stuff"
          multiple
        />
      </div>
    </div>
  )
}

export default UploadStuff

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

Какие из плохих практик JavaScript затронули бы вы?

6962

Комментарии

Рекомендуем

BUG!