React.js для продолжающих

Итак, вы изучили основы React.js и теперь не знаете, что делать дальше? В статье рассмотрены 5 интересных вещей, которые поднимут ваши навыки и знания React.js на новый уровень.

Жизненный цикл компонента

Самой важной для понимания концепцией в этой статье является жизненный цикл компонента. Её суть ясна из названия — она описывает жизнь компонента. Как и мы, компоненты рождаются, что-то делают во время своего пребывания на бренной земле (сервере), а потом умирают.

Однако жизненные этапы компонентов немного отличаются от наших. Вот как они выглядят:

Каждый раскрашенный горизонтальный прямоугольник отражает метод жизненного цикла (за исключением «React обновляет DOM и ссылки»). Столбцы отражают разные этапы жизни компонента.

Компонент может находиться только на одном этапе. Всё начинается с монтирования и продолжается обновлением. Компонент постоянно обновляется, пока не будет удалён из виртуального DOM. Затем компонент переходит на этап размонтирования и удаляется из DOM.

Методы жизненного цикла позволяют запускать код в определённые моменты жизни компонента или в ответ на какие-то изменения в ней.

Пройдёмся по каждому этапу жизни компонента и связанным методам.

Монтирование

Так как компоненты на основе классов являются классами (отсюда и название), первый запускаемый метод — это constructor(). Как правило, в constructor() мы инициализируем состояние компонента.

Затем компонент запускает getDerivedStateFromProps(). Опустим этот метод, так как он не очень полезный.

После этого наступает очередь метода render(), который возвращает JSX. Теперь React «монтируется» в DOM.

Наконец, запускается метод componentDidMount(). Здесь выполняются все асинхронные вызовы к базам данных или напрямую управляется DOM. Компонент рождён.

Обновление

Этот этап запускается при каждом изменении состояния или свойств. Как и при монтировании, вызывается метод getDerivedStateFromProps(), но в этот раз без constructor().

Затем запускается shouldComponentUpdate(). Здесь можно сравнивать старые свойства или состояния с новым набором свойств или состояний. Можно указать, нужно ли заново отображать компонент, вернув true или false. Это позволит сделать приложение более эффективным за счёт уменьшения количества лишних отображений. Если shouldComponentUpdate() возвращает false, на этом этап обновлений завершается.

В противном случае React заново отобразится, а затем запускается getSnapshotBeforeUpdate(). Этот метод тоже не очень полезен. Далее React запускает componentDidUpdate(). Как и componentDidMount(), его можно использовать для асинхронных вызовов или управления DOM.

Размонтирование

Компонент прожил хорошую жизнь, однако всё хорошее рано или поздно кончается. Размонтирование — последний этап жизни компонента. При удалении компонента из DOM React запускает componentWillUnmount() прямо перед удалением. Этот метод используется для закрытия всех открытых соединений вроде веб-сокетов или тайм-аутов.

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

Прежде чем мы перейдём к другой теме, давайте вскользь упомянем forceUpdate() и getDerivedStateFromError().

forceUpdate() — метод, который напрямую вызывает повторное отображение. Хотя у него и есть несколько применений, обычно его следует избегать.

С другой стороны, getDerivedStateFromError() — метод жизненного цикла, который не совсем является его частью. Если в компоненте возникает ошибка, запускается getDerivedStateFromError() и вы получаете возможность обновить состояние, чтобы показать, что произошла ошибка. Используйте этот метод почаще.

Следующий фрагмент кода с CodePen показывает этапы стадии монтирования:

class App extends React.Component {
  constructor(props) {
    super(props)
    console.log('Hello from constructor')
  }

  static getDerivedStateFromProps() {
    console.log('Hello from before rendering')
  }
  componentDidMount() {
    console.log('Hello from after mounting')
  }
  render() {
    console.log('Hello from render')

    return (
      <div>Hello!</div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);

Вывод:

Hello from constructor
Hello from before rendering
Hello from render
Hello from after mounting

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

Компоненты высшего порядка

Возможно, вы уже использовали компоненты высшего порядка (КВП). Например, функция Redux connection() возвращает КВП. Но что из себя представляют эти компоненты?

Из документации React:

Компонент высшего порядка — это функция, которая принимает компонент и возвращает новый компонент.

Возвращаясь к функции connect(), посмотрим на следующий фрагмент кода:

const hoc = connect(state => state)
const WrappedComponent = hoc(SomeComponent)

При вызове connect() мы получаем КВП, который можно использовать для того, чтобы обернуть компонент. Затем мы просто передаём наш компонент в КВП и начинаем использовать возвращаемый им компонент.

КВП позволяют нам абстрагировать общую для компонентов логику в один главный компонент.

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

Вот как всё это может выглядеть без КВП:

class RegularComponent extends React.Component {
  render() {
    if (this.props.isLoggedIn) {
      return <p>Hi</p>
    }
    return <p>You're not logged in :С</p>
  }
}
// повторяющийся код!
class OtherRegularComponent extends React.Component {
  render() {
    if (this.props.isLoggedIn) {
      return <p>Hi</p>
    }
    return <p>You're not logged in :С</p>
  }
}
// обратите внимание, что для функционального компонента нам нужна другая логика
const FunctionalComponent = ({ isLoggedIn }) => ( isLoggedIn ? <p>Hi There</p> : <p>You're not logged in :С</p> )

Тонна повторяющегося кода и запутанная логика!

И вот как код преобразится с использованием КВП:

function AuthWrapper(WrappedComponent) {
  return class extends React.Component {
    render() {
      if (this.props.isLoggedIn) {
        return <WrappedComponent {...this.props} />
      }
      return <p>You're not logged in :С</p>
    }
  }
}

class RegularComponent extends React.Component {
  render() {
    return <p>hi</p>
  }
}
class OtherRegularComponent extends React.Component {
  render() {
    return <p>Hello</p>
  }
}
const FunctionalComponent = () => (<p>Hi There</p>)

const WrappedOne = AuthWrapper(RegularComponent)
const WrappedTwo = AuthWrapper(OtherRegularComponent)
const WrappedThree = AuthWrapper(FunctionalComponent)

Вот CodePen со всем кодом.

Глядя на этот код можно понять, что компоненты могут оставаться очень простыми, даже, если будут производить аутентификацию. Компонент AuthWrapper содержит всю логику аутентификации. Всё, что он делает, — смотрит на свойство isLoggedIn и возвращает WrappedComponent или тег <p> в зависимости от присвоенного свойству значения.

Как видите, КВП очень полезны, так как позволяют повторно использовать код. Скоро мы к ним вернёмся.

Состояние и setState()

Большинство из вас скорее всего уже использовали состояния React, они были даже в примере с КВП. Однако важно понимать, что при смене состояния React запустит процесс повторного отображения компонента (если не указать иное в shouldComponentUpdate()).

Поговорим о том, как можно изменить состояние. Единственный способ сделать это — использовать метод setState(). Он принимает объект и объединяет его с текущим состоянием. Помимо этого есть ещё несколько вещей, которые следует знать.

Во-первых, setState() асинхронный. Это значит, что состояние не обновится сразу после вызова setState(), что может привести к нежелательному поведению, которого мы сейчас научимся не допускать.

Взглянув на эту картинку можно увидеть, что мы сначала вызываем setState(), а затем console.log(). Новое значение переменной counter должно быть равно 1, однако выводится 0. Что если мы хотим получить доступ к новому состоянию после того, как setState() действительно обновит состояние?

Тут мы плавно переходим к следующей вещи, которую нужно знать о setState() — этот метод может принимать callback-функцию. Исправим наш код:

Отлично, всё работает, на этом можно закончить, да? Не совсем. На самом деле, в данном случае мы не используем setState() корректно. Вместо передачи объекта мы передадим методу функцию. Так обычно делается, когда для установки нового состояния используется текущее. Если это не ваш случай, то смело продолжайте передавать объект в setState(). Исправим код ещё раз:

Вот CodePen для этого примера.

В чём смысл передачи функции вместо объекта? Поскольку setState() асинхронный, полагаться на него при создании нового значения немного рискованно. Например, к моменту запуска одного setState() другой setState() мог уже обновить состояние. Передача функции в setState() даёт два преимущества:

  1. Это позволяет создавать статичную копию состояния, которое никогда не изменится само по себе.
  2. Это поместит вызовы setState() в очередь, поэтому они будут запускаться по порядку.

Посмотрим на следующий пример, в котором попытаемся увеличить счётчик на 2, используя два последовательных вызова setState():

Сравните с кодом ниже:

CodePen для этого кода.

На первом изображении обе функции setState() напрямую используют this.state.counter, который, как мы узнали ранее, останется со значением 0 после вызова первого setState(). Таким образом, конечное значение равно 1, а не 2, так обе функции setState() устанавливают значение счётчика равным 1.

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

Вот и всё, что вам нужно знать о состояниях React.

Контекст

Контекст React — это глобальное состояние для компонентов.

API контекста React даёт возможность создавать глобальные объекты компонентов, которые будут доступны любому компоненту. Это позволяет обмениваться данными без необходимости передавать свойства по всему дереву DOM.

Как использовать контекст?

Сначала создадим объект контекста:

const ContextObject = React.createContext({ foo: "bar" })

В документации React установка контекста в компоненте выглядит так:

MyClass.contextType = MyContext;

Однако в CodePen (React 16.4.2) это не сработало. Вместо этого мы воспользуемся КВП, чтобы использовать контекст образом, похожим на тот, что рекомендует Дэн Абрамов.

function contextWrapper(WrappedComponent, Context) {
  return class extends React.Component {
    render() {
      return (
        <Context.Consumer>
          { context => <WrappedComponent context={context} { ...this.props } /> }
        </Context.Consumer>
      )
    }
  }
}

Здесь мы оборачиваем компонент в компонент Context.Consumer и передаём в контекст в качестве свойства.

Теперь мы можем написать что-то такое:

class Child extends React.Component {
  render() {
    console.log(this.props.context)
    return <div>Child</div>
  }
}
const ChildWithContext = contextWrapper(Child, AppContext)

И у нас будет доступ к foo из объекта контекста в свойствах.

А как сменить контекст? Это уже немного сложнее, однако мы можем ещё раз использовать КВП и получить что-то такое:

function contextProviderWrapper(WrappedComponent, Context, initialContext) {
  return class extends React.Component {
    constructor(props) {
      super(props)
      this.state = { ...initialContext }
    }
    
    // определяем изменения состояния
    changeContext = () => {
      this.setState({ foo: 'baz' })
    }

    render() {
      return (
        <Context.Provider value={{
          ...this.state,
          changeContext: this.changeContext
        }} >
          <WrappedComponent />
        </Context.Provider>
      )
    }
  }
}

Сначала мы берём исходное состояние контекста — объект, переданный в React.createContext() — и используем его в качестве состояния компонента-обёртки. Затем мы определяем все методы, которые будем использовать для смены состояния. Наконец, оборачиваем наш компонент в компонент Context.Provider. Мы передаём состояние и функцию в свойство value. Теперь они будут в контексте у всех наследников, обёрнутых в компонент Context.Consumer.

Собираем всё воедино (КВП опущены для краткости):

const initialContext = { foo: 'bar' }
const AppContext = React.createContext(initialContext);

class Child extends React.Component {
  render() {
    return (
      <div>
        <button onClick={this.props.context.changeContext}>Click</button>
        {this.props.context.foo}
      </div>
     )
  }
}

const ChildWithContext = contextConsumerWrapper(Child, AppContext)
const ChildWithProvide = contextProviderWrapper(ChildWithContext, AppContext, initialContext)

class App extends React.Component {
  render() {
    return (
      <ChildWithProvide />
    );
  }
}

Теперь у дочернего компонента есть доступ к глобальному контексту. У него есть возможность изменить значение атрибута foo в состоянии на baz.

CodePen со всем кодом по теме контекста.

Не пропускайте новостей React!

React активно развивается. Например, в React 16.3 некоторые методы жизненного цикла были упразднены, в React 16.6 появились асинхронные компоненты, а в React 16.7 появились хуки, цель которых — полностью заменить компоненты на основе классов. Следите за развитем React и растите вместе с ним!

Перевод статьи «These are the concepts you should know in React.js (after you learn the basics)»

Подобрали два теста для вас:
— А здесь можно применить блокчейн?
Серверы для котиков: выберите лучшее решение для проекта и проверьте себя.