Почему появляются React Hooks?

внешний интерфейс React.js
Почему появляются React Hooks?

оригинал:Dev.to/Tyler Wagon Джин Н…
Переводчик: фронтенд технический брат

Когда вы собираетесь узнать что-то новое, первое, что вы должны сделать, это задать себе два вопроса.

  • 1. Почему эта штука существует?
  • 2. Какую проблему может решить эта штука?

Если вы никогда не дадите убедительного ответа на оба этих вопроса, у вас не будет достаточно прочной основы, чтобы углубиться в конкретные вопросы. Над этими вопросами стоит задуматься, когда дело доходит до React Hooks. Когда были выпущены Hooks, React был самым популярным и востребованным интерфейсным фреймворком в экосистеме JavaScript. Хотя React получил высокую оценку, команда React по-прежнему чувствует необходимость создавать и выпускать хуки. В различных сообщениях на Medium и в блогах обсуждалось (1) почему и для чего, несмотря на высокую оценку и популярность, команда React решила потратить ценные ресурсы на создание и публикацию хуков и (2) их преимущества. Чтобы лучше понять ответы на эти два вопроса, нам сначала нужно глубже взглянуть на то, как мы писали приложения React в прошлом.

createClass

Если вы используете React достаточно долго, вы помните React.createClassAPI. Именно так мы изначально создавали компоненты React. Вся информация, используемая для описания компонента, будет передана в createClass как объект.

const ReposGrid = React.createClass({
  getInitialState () {
    return {
      repos: [],
      loading: true
    }
  },
  componentDidMount () {
    this.updateRepos(this.props.id)
  },
  componentDidUpdate (prevProps) {
    if (prevProps.id !== this.props.id) {
      this.updateRepos(this.props.id)
    }
  },
  updateRepos (id) {
    this.setState({ loading: true })

    fetchRepos(id)
      .then((repos) => this.setState({
        repos,
        loading: false
      }))
  },
  render() {
    const { loading, repos } = this.state

    if (loading === true) {
      return <Loading />
    }

    return (
      <ul>
        {repos.map(({ name, handle, stars, url }) => (
          <li key={name}>
            <ul>
              <li><a href={url}>{name}</a></li>
              <li>@{handle}</li>
              <li>{stars} stars</li>
            </ul>
          </li>
        ))}
      </ul>
    )
  }
})

createClassэто простой и эффективный способ создания компонентов реагирования. Реагировать изначально использовалиcreateClassAPIПричина в том, что в то время в JavaScript не было встроенной системы классов. Конечно, со временем это изменилось. В ES6 JavaScript представил ключевое слово class и использовал его для создания классов в JavaScript нативным способом. Это ставит React перед дилеммой. Либо продолжайте использовать createClass и бороться с эволюцией JavaScript, либо отправьте и включите класс в соответствии со стандартом EcmaScript. История показывает, что они выбрали второе.

React.Component

Мы не думаем, что занимаемся разработкой классовых систем. Мы просто хотим создавать классы любым идиоматическим способом JavaScript. -React v0.13.0 выпущен Представлен Reactiv0.13.0React.ComponentAPI, который позволяет создавать компоненты React из (теперь) нативных классов JavaScript. Это огромная победа, поскольку она лучше соответствует стандарту ECMAScript.

class ReposGrid extends React.Component {
  constructor (props) {
    super(props)

    this.state = {
      repos: [],
      loading: true
    }

    this.updateRepos = this.updateRepos.bind(this)
  }
  componentDidMount () {
    this.updateRepos(this.props.id)
  }
  componentDidUpdate (prevProps) {
    if (prevProps.id !== this.props.id) {
      this.updateRepos(this.props.id)
    }
  }
  updateRepos (id) {
    this.setState({ loading: true })

    fetchRepos(id)
      .then((repos) => this.setState({
        repos,
        loading: false
      }))
  }
  render() {
    if (this.state.loading === true) {
      return <Loading />
    }

    return (
      <ul>
        {this.state.repos.map(({ name, handle, stars, url }) => (
          <li key={name}>
            <ul>
              <li><a href={url}>{name}</a></li>
              <li>@{handle}</li>
              <li>{stars} stars</li>
            </ul>
          </li>
        ))}
      </ul>
    )
  }
}

Хотя это явный шаг в правильном направлении, React.Component не лишен недостатков.

Конструктор

Используя компоненты класса, мы можемconstructorМетод инициализирует состояние компонента свойством состояния экземпляра (this). Однако, согласно спецификации ECMAScript, если вы хотите расширить подкласс (здесь мы говорим о React.Component), вы должны вызвать super, прежде чем сможете использовать это. В частности, при использовании React мы также должны помнить о передаче props в super.

constructor (props) {
    super(props) // 🤮

    ...
  }

автоматическая привязка

когда используешьcreateClass, React автоматически привяжет все методы к экземпляру компонента, т.е.this. имеютReact.Component, ситуация другая. Вскоре разработчики React повсюду поняли, что не знают, как использовать ключевое слово «this». Мы должны помнить, что на урокеconstructorсередина.bindвместо вызова метода, который просто работает. Если вы этого не сделаете, вы получите повсеместную ошибку «Невозможно прочитать неопределенноеsetStateошибка "свойство".

constructor (props) {
    ...
    this.updateRepos = this.updateRepos.bind(this) // 😭
}

Теперь я думаю, вы могли бы подумать. Во-первых, эти вопросы довольно поверхностны. Конечно, вызвать super(props) и запомнить метод привязки сложно, но в этом нет ничего принципиально неправильного. Во-вторых, эти проблемы React не так серьезны, как то, как спроектированы классы JavaScript. Конечно, оба эти пункта бесспорны. Тем не менее, мы разработчики. Даже самые тривиальные проблемы могут стать надоедливыми, когда приходится решать их более 20 раз в день. К счастью, предложение о полях классов появилось вскоре после перехода с createClass на React.Component.

поле класса

Поля класса позволяют нам напрямую добавлять свойства экземпляра как свойства класса без использования конструктора. Для нас это означает, что в области классов будут решены обе «маленькие» проблемы, которые мы обсуждали ранее. Нам больше не нужно использовать конструктор для установки начального состояния компонента, а также нам не нужно использовать .bind в конструкторе, потому что мы можем использовать стрелочные функции.

class ReposGrid extends React.Component {
  state = {
    repos: [],
    loading: true
  }
  componentDidMount () {
    this.updateRepos(this.props.id)
  }
  componentDidUpdate (prevProps) {
    if (prevProps.id !== this.props.id) {
      this.updateRepos(this.props.id)
    }
  }
  updateRepos = (id) => {
    this.setState({ loading: true })

    fetchRepos(id)
      .then((repos) => this.setState({
        repos,
        loading: false
      }))
  }
  render() {
    const { loading, repos } = this.state

    if (loading === true) {
      return <Loading />
    }

    return (
      <ul>
        {repos.map(({ name, handle, stars, url }) => (
          <li key={name}>
            <ul>
              <li><a href={url}>{name}</a></li>
              <li>@{handle}</li>
              <li>{stars} stars</li>
            </ul>
          </li>
        ))}
      </ul>
    )
  }
}

Так что теперь у нас нет проблем, верно? Однако нет. от createClass кReact.ComponentВ процессе миграции есть некоторые компромиссы, но, как мы видим, поле класса решает некоторые проблемы. К сожалению, у нас все еще есть некоторые более глубокие (но менее упоминаемые) проблемы, которые мы наблюдали во всех предыдущих версиях. Вся концепция React заключается в том, что вы можете лучше управлять сложностью своего приложения, разбивая его на отдельные компоненты, а затем объединяя их вместе. Эта компонентная модель делает React таким тонким и уникальным. Однако проблема не в компонентной модели, а в том, как установить компонентную модель.

повторяющаяся логика

В прошлом то, как мы создавали компоненты React, было связано с жизненным циклом компонента. Этот разрыв логически заставляет связанную логику быть разбросанной по всему компоненту. Мы можем ясно видеть это в нашем примере с ReposGrid. Нам нужны три отдельных метода (componentDidMount, componentDidUpdate и updateRepos) для выполнения одной и той же задачи — синхронизации репозиториев с любым props.id.

componentDidMount () {
    this.updateRepos(this.props.id)
 }
 componentDidUpdate (prevProps) {
    if (prevProps.id !== this.props.id) {
      this.updateRepos(this.props.id)
    }
 }
 updateRepos = (id) => {
    this.setState({ loading: true })
    fetchRepos(id)
      .then((repos) => this.setState({
        repos,
        loading: false
      }))
  }

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

Общая невизуальная логика

Когда вы думаете о композиции в React, вы, скорее всего, думаете о композиции пользовательского интерфейса. Это естественно, потому что именно в этом React преуспевает.

view = fn(state)

На самом деле создание приложения — это нечто большее, чем просто создание слоя пользовательского интерфейса. Нередко возникает необходимость комбинировать и повторно использовать невизуальную логику. Однако, поскольку React связывает пользовательский интерфейс с компонентами, это сложнее. Пока что React не дал хорошего решения. Продолжая наш пример, предположим, что нам нужно создать еще один компонент, которому также требуется состояние репозитория. Теперь в компоненте ReposGrid у нас есть это состояние и логика для его обработки. Как это сделать?Один из самых простых способов — скопировать всю логику получения и обработки репозиториев и вставить в новый компонент. Звучит здорово, но нет. Более аккуратный подход — создать компонент более высокого порядка, который обертывает всю общую логику и передает загрузку и репозитории как свойство любому компоненту, который в этом нуждается.

function withRepos (Component) {
  return class WithRepos extends React.Component {
    state = {
      repos: [],
      loading: true
    }
    componentDidMount () {
      this.updateRepos(this.props.id)
    }
    componentDidUpdate (prevProps) {
      if (prevProps.id !== this.props.id) {
        this.updateRepos(this.props.id)
      }
    }
    updateRepos = (id) => {
      this.setState({ loading: true })

      fetchRepos(id)
        .then((repos) => this.setState({
          repos,
          loading: false
        }))
    }
    render () {
      return (
        <Component
          {...this.props}
          {...this.state}
        />
      )
    }
  }
}

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

// ReposGrid.js
function ReposGrid ({ loading, repos }) {
  ...
}

export default withRepos(ReposGrid)
// Profile.js
function Profile ({ loading, repos }) {
  ...
}

export default withRepos(Profile)

Это работает, и в прошлом это вместе с Render Props было рекомендуемым решением для совместного использования невизуальной логики. Однако оба режима имеют некоторые недостатки. Во-первых, если вы с ними не знакомы (а даже если и знакомы), вы будете немного сбиты с толку. Когда мы используем расширенный компонент withRepos, у нас есть функция, которая принимает окончательный визуализированный компонент в качестве первого параметра, но возвращает компонент нового класса, в котором и заключается логика. Какой это сложный процесс. Далее, что, если бы мы потребляли несколько высокоуровневых компонентов?Как вы понимаете, это быстро вышло из-под контроля.

export default withHover(
  withTheme(
    withAuth(
      withRepos(Profile)
    )
  )
)

Хуже, чем ^, это то, что у вас получится. Эти расширенные компоненты (и подобные шаблоны) заставляют нас реструктурировать и обертывать компоненты. В конечном итоге это может привести к «аду упаковки», что снова усложняет отслеживание.

<WithHover>
  <WithTheme hovering={false}>
    <WithAuth hovering={false} theme='dark'>
      <WithRepos hovering={false} theme='dark' authed={true}>
        <Profile 
          id='JavaScript'
          loading={true} 
          repos={[]}
          authed={true}
          theme='dark'
          hovering={false}
        />
      </WithRepos>
    </WithAuth>
  <WithTheme>
</WithHover>

ситуация

Вот где мы сейчас находимся.

  • Реакт очень популярен.
  • Мы используем классы для компонентов React, потому что в настоящее время это наиболее целесообразно.
  • Вызов super(props) раздражает.
  • Никто не знает, о чем «это».
  • Ну, успокойся. Я знаю, вы знаете, что происходит, но для некоторых это ненужное препятствие.
  • Организация компонентов в рамках жизненного цикла вынуждает нас распространять связанную логику между компонентами.
  • В React нет хороших примитивов для совместного использования невизуальной логики.

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

React Hooks

Начиная с Reactive0.14.0 у нас есть два способа создания компонентов — класс или функция. Разница в том, что если компонент имеет состояние или должен использовать методы жизненного цикла, вы должны использовать класс. В противном случае, если он просто принимает свойства и отображает некоторый пользовательский интерфейс, мы можем использовать функцию. Если не. Что, если вместо использования классов мы всегда использовали функции?

Иногда для безупречной установки требуется только одна функция. Нет метода. Класса нет. Рамка тоже не нужна. Требуется только одна функция. — Джон Кармак, технический директор OculusVR.

Конечно, нам нужно найти способ добавить возможность для функциональных компонентов иметь методы состояния и жизненного цикла, но если мы это сделаем, какие преимущества мы получим? Нам больше не нужно вызывать super(props), больше не нужно думать о методах связывания или ключевом слове this, и больше не нужно использовать поля класса. , все "мелкие" проблемы, которые мы обсуждали ранее, исчезнут.

(ノಥ,_」ಥ)ノ彡 React.Component 🗑

function ヾ(Ő‿Ő✿)

Теперь возникает более сложный вопрос.

  • условие
  • методы жизненного цикла
  • Общая невизуальная логика

условие

Поскольку мы больше не используем классы или это, нам нужен новый способ добавления и управления состоянием внутри компонентов. React v16.8.0 дает нам эту новую возможность через метод useState.
useState — это первый из многих «хуков», которые мы увидим в этом курсе. Пусть следующая часть этой статьи послужит кратким введением. После этого мы углубимся в useState и другие хуки.
UseState принимает только параметр, начальное значение состояния. Он возвращает массив, где первый элемент — это блок статуса, а второй элемент — функция, которая обновляет статус.

const loadingTuple = React.useState(true)
const loading = loadingTuple[0]
const setLoading = loadingTuple[1]

...

loading // true
setLoading(false)
loading // false

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

// const loadingTuple = React.useState(true)
// const loading = loadingTuple[0]
// const setLoading = loadingTuple[1]

const [ loading, setLoading ] = React.useState(true) // 👌

Теперь давайте обновим компонент ReposGrid нашими новыми знаниями о хуке useState.

function ReposGrid ({ id }) {
  const [ repos, setRepos ] = React.useState([])
  const [ loading, setLoading ] = React.useState(true)

  if (loading === true) {
    return <Loading />
  }

  return (
    <ul>
      {repos.map(({ name, handle, stars, url }) => (
        <li key={name}>
          <ul>
            <li><a href={url}>{name}</a></li>
            <li>@{handle}</li>
            <li>{stars} stars</li>
          </ul>
        </li>
      ))}
    </ul>
  )
}
  • Статус ✅
  • методы жизненного цикла
  • Общая невизуальная логика

методы жизненного цикла

Есть что-то, что может вас огорчить (или порадовать?). При использовании ReactHooks нам нужно забыть все, что мы знаем о неудачном подходе к жизненному циклу React и таком образе мышления. Мы уже видели проблему, возникающую при размышлении о жизненном цикле компонента: «Это (имея в виду жизненный цикл) логически вынуждает связанную логику быть разбросанной по всему компоненту». Подумайте о времени, когда мы использовали события жизненного цикла. Будь то установка начального состояния компонента, выборка данных, обновление DOM и т. д., конечной целью всегда является синхронизация. В общем, синхронизируйте вещи за пределами Reactland (запросы API, DOM и т. д.) с вещами внутри Reactland (состояние компонентов) и наоборот. Когда мы думаем о синхронизации, а не о событиях жизненного цикла, это позволяет нам сгруппировать связанные части логики вместе. Для этого React дает нам еще один хук под названием useEffect.
Можно с уверенностью сказать, что useEffect позволяет нам выполнять побочные эффекты в функциональных компонентах. Он принимает два параметра, функцию и необязательный массив. Функция определяет запускаемый побочный эффект, а (необязательный) массив определяет, когда «повторно синхронизировать» (или повторно запускать) эффект.

React.useEffect(() => {
  document.title = `Hello, ${username}`
}, [username])

В приведенном выше коде функция, переданная в useEffect, будет выполняться при изменении имени пользователя. Поэтому синхронизируйте заголовок документа с тем, что разбирает Hello, ${username}. Теперь, как мы можем использовать хук useEffect в коде для синхронизации запросов API репозиториев и fetchRepos?

function ReposGrid ({ id }) {
  const [ repos, setRepos ] = React.useState([])
  const [ loading, setLoading ] = React.useState(true)

  React.useEffect(() => {
    setLoading(true)

    fetchRepos(id)
      .then((repos) => {
        setRepos(repos)
        setLoading(false)
      })
  }, [id])

  if (loading === true) {
    return <Loading />
  }

  return (
    <ul>
      {repos.map(({ name, handle, stars, url }) => (
        <li key={name}>
          <ul>
            <li><a href={url}>{name}</a></li>
            <li>@{handle}</li>
            <li>{stars} stars</li>
          </ul>
        </li>
      ))}
    </ul>
  )
}

Довольно аккуратно, правда? нам удалось избавиться отReact.Component, constructor, super, this, и, что более важно, мы больше не распространяем (и не дублируем) логику эффектов по всему компоненту.

  • Статус ✅
  • Методы жизненного цикла ✅
  • Общая невизуальная логика

Общая невизуальная логика

Ранее мы упоминали, что у React нет отличного решения для совместного использования невизуальной логики, потому что «React связывает пользовательский интерфейс с компонентами». Это приводит к чрезмерно сложным паттернам, таким как компоненты более высокого порядка или реквизиты рендеринга. Теперь, как вы могли догадаться, у Hooks есть ответ и на этот вопрос. Однако это может быть не то, что вы думаете. На самом деле нет встроенных хуков для совместного использования невизуальной логики, вместо этого мы можем создавать собственные хуки, которые не связаны с любым пользовательским интерфейсом.
Мы можем увидеть это, создав собственный хук useRepos. Это возьмет идентификатор репозитория, который мы хотим получить, и (сохраняя аналогичный API) вернет массив, где первый элемент — это состояние загрузки, а второй — состояние репозитория.

function useRepos (id) {
  const [ repos, setRepos ] = React.useState([])
  const [ loading, setLoading ] = React.useState(true)

  React.useEffect(() => {
    setLoading(true)

    fetchRepos(id)
      .then((repos) => {
        setRepos(repos)
        setLoading(false)
      })
  }, [id])

  return [ loading, repos ]
}

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

function ReposGrid ({ id }) {
  const [ loading, repos ] = useRepos(id)
  ...
}
function Profile ({ user }) {
  const [ loading, repos ] = useRepos(user.id)
  ...
}
  • Статус ✅
  • Методы жизненного цикла ✅
  • Поделитесь невизуальной логикой ✅

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

❤️ после просмотра

  • Ставьте лайк, чтобы больше людей увидело этот контент
  • Обратите внимание на публичный аккаунт «Новое фронтенд-сообщество» и наслаждайтесь первыми статьями! Сосредоточьтесь на преодолении технической трудности переднего плана каждую неделю.