Зачем нужен prevState в ReactJS?

Олег Кусов07.10.2021
ReactJS

Это понятное описание prevState в React. Но давайте посмотрим на какой-нибудь более интересный пример. Представим, что у нас есть контекст, в котором хранится результат useState.

Мы создали компонент List, который выглядит следующим образом:

Здесь мы в useEffect заносим в наш стейт массив [“1”, “2”, “3”] в свойство list. У нас также есть свойство anotherList, куда мы занесем значения в useEffect компонента :

Некоторые могли подумать, что всё хорошо и стейт поменяется корректно, но это не так. Этот пример интересен тем, что здесь не так очевидно то, что наш стейт меняется в одном ререндере. На первый взгляд, мы сначала меняем стейт в родительском <List /> компоненте, в котором отрабатывает наш useEffect, а уже затем меняем стейт в компоненте <AnotherList /> с помощью useEffect. В действительности же всё происходит в одном ререндере и на экране мы получим лишь “1,2,3”. То есть в момент задания стейта в <AnotherList /> наш store.state будет иметь исходное значение.

Всё дело в том, как работает React. Каждый ререндер сохраняет внутри себя состояние и представляет собой снимок компонента и его стейта, то есть мы имеем дело с замыканием. Когда мы пытаемся изменить стейт через store.setState и копируем наш прежний стейт, он - стейт - на самом деле, в этот момент бывает исходным, потому что мы имеем дело с одним ререндером, т.к. наши компоненты рендерятся внутри компонента <App />, в котором хранится наш стейт.

Что же происходит на самом деле?

  • Рендерится List, стейт пока исходный { list: [], anotherList: [] }
  • Рендерится AnotherList, стейт исходный { list: [], anotherList: [] }
  • Вызываются useEffect поочередно в любой последовательности:
    • Вызывается useEffect в AnotherList, задается значение в стейт: { list: [], anotherList: [“5”, “6”, “7”] }
    • Вызывается useEffect в List, задается значение в стейт: { list: [“1”, “2”, “3”], anotherList: [] }

Вы заметили, что в <List /> стейт будет старым -то есть исходным - и не будет включать в себя изменения, которые были внесены в useEffect из <AnotherList />. А всё потому, что мы имеем дело с одним рендером (снимком) и в этом снимке нашего состояния оно всё еще старое и равно { list: [], anotherList: [] }, ведь компонент <List /> и все его дети еще не успели перерендириться и не закоммитили изменения стейта.

Рендерится приложение с выводом на экран “1,2,3”. Вывод “5,6,7” был потерян.

Главное правило контекста как замены Redux - Know that the value has been updated because the component re-rendered (Знайте, что значение обновляется потому, что компонент перерисовывается).

prevState или Redux - решение проблемы

В таких случаях на помощь приходит prevState в setState, которому передается актуальное состояние стейта через коллбэк- без привязки к ререндерам компонентов. Поэтому, если в useEffect внести следующие изменения, всё будет работать:

Еще одним решением станет отказ от useState в пользу Redux, который не зависит от ререндеров и использует собственный объект стора со своим замыканием. Уведомляет Redux подписчиков об изменении стейта, реализуя паттерн Publisher-Subscriber.

Делитесь мнением в комментариях и подписывайтесь на меня в Twitter.