Привет хабр!
В React на смену эпохи классов, пришла эпоха функциональных компонентов. И нам показали хуки, как замена методам жизненного цикла. Но многие так и не задумывались, а равнозначный ли обмен componentDidMount на useEffect. Эта статья направлена как раз на таких людей, чтобы закрыть ваш пробел, в том как работают componentDidMount, useEffect и useLayoutEffect. (данная статья является расшифровкой видео)
Викторина
Для того чтобы разобраться в этом вопросе, проведем небольшую викторину!
Постановка задачи
Допустим у нас есть красный блок с некоторой высотой и шириной. И у нас есть задача вывести ширину этого блока на экран:
На классах код будет выглядеть примерно следующим образом:
const App extends Component { state = { width: 0 }; ref = React.createRef(); componentDidMount() { this.setState({ width: this.ref.current.clientWidth }); } render() { return ( <div className="app"> <div className="block" ref={this.ref} /> <span className="result"> width: <b>{this.state.width}</b> </span> </div> ) }}
Интерес данной ситуации состоит в том, что изначально у нас в
state
хранится ширина равная нолю (state = {
width: 0 };
). Потом происходит render
. И уже
только после рендера в componentDidMount
вызывается
this.setState({ ... })
с реальным значением ширины
блока. И снова происходит render
с уже обновленным
значением this.state.width
Вопрос
Что увидит пользователь в браузере? Сначала ширина будет ноль, а потом быстро изменится на реальное значение? Или сразу увидим реальное значение ширины блока?
Как всегда даем минутку подумать .
Ответ
И правильный ответ: width сразу отобразит цифру 220, без промежуточного значения 0. Результат достаточно интересный, чтобы лучше разобраться в текущей ситуации проведем еще один тест.
Анализ происходящего
Давайте создадим функцию sleep. Только не асинхронную с помощью
setTimeout
, а наоборот синхронную, чтобы блокировала
поток, для этого зациклим while
на присланное
количество миллисекунд:
sleep(duration) { const start = new Date().getTime(); let end = start; while(end < start + duration) { end = new Date();getTime(); }}
И теперь добавим sleep
с 3000 миллисекунд до
присвоения значения width
в state
componentDidMount() { this.sleep(3000); this.setState({ width: this.ref.current.clientWidth });}
Как думаете что теперь пользователь увидит?
Результат снова немного неожиданный, у нас страница просто фризится на 3 секунды и только после этого браузер отрисует красный квадрат и сразу же выдаст цифру с шириной квадрата. Из этого уже можно сделать какие то выводы
Выводы
Получается на основе render
создается виртуальное
дерево, но перед тем как отдать виртуальное дерево на отрисовку в
браузер, вызывается componentDidMount
и даже более
того блокирует отрисовку в браузере, в нашем случае на 3 секунды.
Три, два, один и setState
заново перестраивает
виртуальное дерево, и только после всего этого браузер рисует
страницу. И даже если указать задержку не 3 секунды, а 30 секунд,
результат не изменится мы увидим как страница зависнет на 30
секунд.
useEffect
Давайте теперь сравним с тем как работает
useEffect
. Напишем такой же код на функциональном
компоненте:
const App = () => { const [width, setWidth] = useState(0); const ref = useRef(); useEffect(() => { let start = new Date().getTime(); let end = start; while (end < start + 3000) { end = new Date().getTime(); } setWidth(ref.current.clientWidth); }, []); return ( <div className="app"> <div className="block" ref={ref} /> <span className="result"> width: <b>{width}</b> </span> </div> )}
Результат как вы уже догадались будет отличаться. Мы увидим
сначала значение ноль, а только через 3 секунды цифра обновится до
ширины блока. Таким образом можно предположить что
useEffect
работает по следующему сценарию:
Сначала на основе return
создается виртуальное
дерево, далее оно отдается на отрисовку в браузер и только после
этого вызывается функция переданная в useEffect
, в
которой уже идет блокировка потока на 3 секунды, три, два, один и
после спячки виртуальное дерево заново строится с новым значением в
state
и это дерево передается на отрисовку в
браузер
Документация
Теперь мы точно знаем, что componentDidMount
отличается от useEffect
. Чтобы понять, это отличие
вызвано просто особенностями реализации хуков или же умышленно,
давайте как всегда обратимся к документации.
Из этого можно сделать вывод, что React разработчики умышленно
дали нам, абсолютно новый API, позволяющий отрисовать контент в
браузере до того как запуститься функция переданная в
useEffect
. Этот API просто не существует при написании
компонента на классах. Его можно сымитировать разве что использовав
setTimeout
внутри componentDidMount
.
componentDidMount() { setTimeout(() => { ... }, 0);}
Но это скорее выглядит как костыль чем решение.
useLayoutEffect
А с другой стороны, если нам нужно выполнить какой то код, до
отрисовки в браузере, на предоставили хук
useLayoutEffect
, интерфейс которого полностью
совпадает с useEffect
, но по очередности выполнения
полностью совпадает с componentDidMount
.
Мысли в слух
На что хотелось бы обратить внимание. Несмотря на то что React
разработчики не собираются удалять поддержку классов, мне кажется и
развивать их они так же не собираются. Поэтому вряд ли мы увидим
аналог useEffect
на классах, но с другой стороны,
точно увидим недостающие методы жизненного цикла существующие на
классах и пока не существующие на хуках.
Надеюсь данная статья помогла некоторым из вас восполнить пробел, на который вечно не хватает времени ;-)