Часть вторая

Обзор

Это вторая часть серии по изучению React Hook s и Context API. В первой части мы изучили основы useState и построили первые части приложения Recipe Search. Полный код можно найти на GitHub.

В этой части мы еще немного поработаем со состояниями, чтобы полностью понять концепцию. Мы объясним useEffect. Мы создадим компонент RecipeSearch . Затем мы реорганизуем нашу стратегию управления состоянием, чтобы использовать Context API useContext. Интересно, правда?

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

Из предыдущей части я извлек RecipeDetails в отдельный компонент. Это по двум причинам. Во-первых, это правильно. Это суть компонентно-ориентированных фреймворков, позволяющих повторно использовать код. Во-вторых, это дает нам широкую возможность увидеть проблему, которая возникает при передаче пропсов (бурение реквизитов), и то, как Context API может нам помочь.

Подробнее об этом позже! Сначала нанесем удар по useEffect .

Краткое руководство по useEffect H ook

В первой части мы упомянули и использовали ловушку useEffect , но не дали подробных объяснений. Я уверен, что лишь немногие из нас осознают проблему того, как мы использовали useEffect Hook в первой части.

Помните, как нам приходилось создавать собственный отдельный ответ JSON, чтобы не делать слишком много вызовов API? Мы делаем что-то ужасно неправильно, просто используя useEffect, как раньше. Я немного углублюсь в это.

Но сначала, что это за useEffect Крюк? Как следует из названия, это должно иметь какое-то отношение к эффектам, верно? Если вы так и догадались, вы абсолютно правы. Но что за эффекты?

Согласно официальной документации React, эффекты - это действия, связанные с получением данных, настройкой подписки и ручным изменением DOM в компонентах React. (Некоторые называют это побочными эффектами. Другие называют это просто эффектами. Мы имеем в виду то же самое.) Если вы знакомы с методами жизненного цикла класса React, вы можете думайте о useEffect Hook как о componentDidMount, componentDidUpdate и componentWillUnmount вместе взятых.

Правильное использование useEffect H ook

Давайте свяжем эту информацию с нашим приложением. В app.js мы получили данные из функции API Food2Fork, а затем вызвали функцию в функции useEffect . Это эквивалентно вызову внутри функции componentDidMount . Давайте посмотрим поближе.

Но подумайте об этом на секунду. Для чего предназначен componentDidMount? Ответ в названии! Вы хотите запускать любую функцию внутри этой функции только тогда, когда компонент смонтирован. Давайте медленно пройдемся по этой информации. При монтировании компонент создается (вашим кодом и внутренними компонентами React), а затем вставляется в DOM.

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

Как же тогда useEffect удается заменить componentDidMount и componentWillUnmount в одной функции? Давайте вернемся к нашему неправильному использованию useEffect, чтобы лучше понять это. Когда мы вызываемuseEffect, как показано ниже, компонент постоянно перерисовывает, потому что не знает, как размонтировать. Это ужасное узкое место в производительности.

Давайте сделаем вскрытие, чтобы увидеть проблему в действии. Внутри fetchRecipe() в app.js , попробуйте записать что-нибудь в консоль, чтобы указать, когда компонент визуализируется. Мы также могли просто проверить вкладку «Сеть» в браузере. Но иногда консольное ведение журнала и наблюдение за ним в действии быстрее доводят дело до конца.

В 3/hook-in-app.js ветке репозитория Edit app.js , добавьте console.log и откройте консоль. Для этого небольшого эксперимента у вас должно быть что-то похожее на следующее в app.js .

Ужас вы увидите ниже. Что, черт возьми, происходит? Это недопустимое поведение. Нам это сошло с рук из-за созданного мной ответа JSON. Мы могли совершать неограниченное количество вызовов API без проблем. Очевидно, что это не может быть правильным способом делать что-то в производстве.

Так в чем проблема? И решение?

Что сразу ясно, так это то, что fetchRecipe постоянно пытается получить ответ от API и каждый раз заново обрабатывается useEffect , хотя ничего не изменилось. Естественно, нам пришлось бы очистить это в компоненте класса, отказавшись от подписки на вызов API в другом componentWillUnmount .

Давайте посмотрим, как useEffect решает эту проблему. Теперь, когда мы оценили проблему, давайте подумаем о ее решении. Сделайте паузу на мгновение. Чего мы намерены достичь? В нашем случае мы хотим, чтобы функция fetchRecipe запускалась только один раз при монтировании компонента, то есть один раз получить данные и отобразить их, или когда что-либо влияет на результат функции. На данный момент на результат ничего не влияет. Как же тогда преодолеть эту проблему? Оказывается, все очень просто.

Внутри функции useEffect мы просто передаем пустой массив в качестве второго параметра. Следовательно, мы указываем useEffect не обновлять, кроме случаев, когда компонент впервые смонтирован, потому что мы не ожидаем, что ничего не вызовет обновление fetchRecipe (см. Ниже).

Если вы вернетесь в app.js и создадите console.log, вы поймете, что fetchRecipe запускается только один раз.

Это замечательно и хорошо работает в нашем случае. Но что, если мы захотим обновить и повторно отрендерить после того, как что-то заставит fetchRecipe измениться? Вы можете спросить, что это могло быть. Допустим, у нас есть состояние, которое изменяет URL для получения данных. Это означает, что данные из fetchRecipe зависят от URL-адреса . Мы просто передаем этот параметр внутри массива, переданного в useEffect. Мы можем передать в массив столько параметров, сколько необходимо.

На человеческом языке вы говорите useEffect: обновлять только при изменении параметра URL или параметра запроса.

Я рекомендую руководство RobinWieruch для получения дополнительных примеров, если вы все еще не уверены.

Давайте создадим RecipeSearch компонент

Зная, как безопасно получать данные, мы теперь перейдем к использованию ключа API от Food2Fork. Мы проведем некоторый рефакторинг в app.js. Мы также познакомим вас с передовой практикой использования блока try catch внутри функции async для обнаружения любых ошибок.

Чтобы продолжить, клонируйте 4/feature/implemented-search ветку репо.

Сначала давайте подумаем, чего мы хотим достичь с помощью компонента поиска, а затем подумаем, как этого добиться.

Мы хотим ввести поиск, а затем, когда мы нажимаем «Отправить» или нажимаем клавишу «Ввод», мы хотим изменить содержимое страницы на новый результат поиска. И мы также хотим иметь возможность вернуться к 30 лучшим рецептам, очистив, таким образом, поиск (или вернуться к 30 лучшим результатам, в зависимости от того, как вы об этом думаете). Вы можете увидеть, что я имею в виду, ниже.

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

Сразу становится очевидно, что нам определенно нужны какие-то состояния для управления всеми этими изменениями. Сначала мы получаем еще один набор из 30 рецептов, связанных с нашим поисковым запросом. Это одно состояние. Мы также должны решить, когда кнопка «Вернуться домой» должна появиться или исчезнуть. Понятно: это другое состояние. Последнее и, возможно, самое важное, это состояние, в котором находится наш параметр поиска.

Как и раньше, давайте посмотрим, как выглядит код в app.js, а затем объясним его построчно. Вы можете найти полный код функции поиска на GitHub.

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

Пойдем построчно.

Мы установили переменную для URL-адреса. Мы знаем, как выглядит конечная точка URL, из документации Food2Fork. Затем мы настраиваем состояние для showHomeButton, чтобы управлять видимостью кнопки Домой. Мы устанавливаем начальное значение на false. Это потому, что изначально, когда мы находимся на главной странице, нет смысла отображать кнопку Домой. Следующие два состояния уже знакомы. Затем у нас есть состояние поиска, и мы устанавливаем исходное значение в пустую строку. Это потому, что мы еще ничего не ищем, когда впервые заходим на главную страницу.

Затем нам нужно управлять тем, как мы реагируем на ввод пользователя в поисковый запрос. Для этого мы создали функцию handleSearchChange. У нас есть кнопка handleSubmit, которая заботится об обновлении данных в списке, чтобы они соответствовали результатам поиска. И, наконец, у нас есть функция handleReturnHome , которая, как вы угадали, помогает нам безопасно вернуться домой, приготовив всего 30 лучших рецептов.

Давайте посмотрим, как каждая функция выполняет свои обязанности.

Сначала посмотрим на handleSearchChange. Его цель - захватить ввод пользователя, а затем обновить состояние поиска, чтобы оно было равным вводу пользователя. Это довольно просто: тело функции - это всего лишь одна строка.

Вот как это работает. Поскольку мы реагируем на ввод данных пользователем, у нас есть доступ к свойству onChange из объекта события. Мы просто взяли значение из event.target.value и обновили состояние поиска этим значением с помощью ловушки setSearch. Затем нам нужно передать их в качестве свойств вплоть до компонента RecipeSearch, как показано ниже. Это одна из тех вещей, которые нужно решить с помощью Context API.

Теперь давайте рассмотрим самую интересную функцию в App.js, handleSubmit. Что оно делает? Давайте сначала посмотрим на код, а затем объясним.

Поскольку это будет запускаться при отправке, мы должны получить доступ к объекту события . Сначала нам нужно предотвратить поведение onSubmit по умолчанию, которое заключается в перезагрузке страницы. Следовательно, у нас есть e.preventDefault(). Мы устанавливаем состояние загрузки на true, где setLoading(true) указывает, что мы все еще получаем данные.

Затем мы берем текущее состояние поиска, которое теперь равно вводу пользователя. Мы используем это для создания новой точки API на основе документации от Food2Fork. Затем извлекаются новые данные на основе этого поискового запроса. Затем он обновляет текущее состояние рецепта с помощью setRecipe , чтобы соответствовать новым данным из поискового запроса. Теперь, когда у нас есть данные, мы устанавливаем для состояния загрузки значение false с помощью setLoading(false).

Теперь, когда новый список рецептов основан на поисковом запросе, нам нужен способ вернуться домой. Поэтому мы устанавливаем для showHomeButton значение true, используя setShowHomeButton.

Наконец, у нас есть функция handleReturnHome, задача которой - вернуть нас домой с 30 лучшими рецептами. Мы просто получаем рецепт, как и изначально. Это вернет состояние рецепта в прежнее состояние. Затем мы снова устанавливаем для showHomeButton значение false, заставляя кнопку исчезать, когда мы находимся на домашней странице. Затем мы просто передаем все необходимые состояния в качестве свойств дочерним компонентам, где и будем их использовать.

Контекстный API

React Context API - это, по сути, способ создания глобальных состояний, которые можно использовать в любом месте приложения. Это альтернатива передаче реквизита от бабушек и дедушек детям и так далее. Его рекламировали как более легкую альтернативу Redux. По моему опыту, я скажу, что это больше похоже на VueX Vue, где у вас есть единственный источник истины и вы можете получить доступ к этому состоянию глобально.

Давайте рассмотрим пример бурения опор в нашем приложении. Вы можете представить себе в таком простом приложении, что нам уже нужно передавать реквизиты от app.js до RecipeList, а затем в RecipeSearch. Несмотря на то, что компоненту RecipeList не нужны реквизиты handlesSubmit и handlesSearchChange, нам все равно нужно передать их, потому что это родительский компонент для RecipeSearch .

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

useContext Привет на помощь

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

С Context API нужно понимать несколько вещей.

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

Давайте обсудим API, которые дает нам Context.

Сначала у нас есть React.createContex, который создает объект Context. Когда React визуализирует компонент, который подписывается на этот объект Context, он считывает текущее значение Context из ближайшего соответствующего Provider над ним в дереве.

Далее идет Context.Provide. Каждый объект Context поставляется с компонентом Provider React, который позволяет потребляющим компонентам подписываться на изменения контекста.

Наконец, есть Context.Consumer, компонент React, который подписывается на изменения контекста. Это позволяет вам подписаться на контекст внутри функционального компонента.

Как работает перехватчик useContext в нашем примере

Во-первых, давайте создадим папку Context или просто файл с index.js файлом в нем. Внутри index.js нам нужно создать Provider, который принимает и предоставляет данные всем дочерним компонентам в нем. Сначала давайте переместим всю нашу логику получения данных из app.js в файл контекста. У вас должен остаться почти пустой app.js , как показано ниже.

И index.js

Давайте попробуем понять приведенный выше код, внимательно его разобрав.

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

Теперь, что самое интересное, строка 2. Мы создали RecipeContext из метода React.createContext() . (RecipeContext - это просто переменная. Имя не имеет значения. Это просто хорошая практика - дать ей имя, близкое к тому, что она делает.)

Когда мы createdRecipeContext,, он предоставил нам доступ к двум другим свойствам, а именно к Provider и Consumer. Provider дает нам возможность передавать все данные и состояния в качестве свойств из самой верхней части дерева React туда, где мы хотели бы их использовать. Поэтому мы создали RecipeContext.Provider и передали все состояния и данные как свойство с именем value. Затем мы экспортируем эти значения для использования, как показано ниже. Это станет понятнее, когда мы начнем их использовать.

Затем мы должны найти самое верхнее дерево в нашей иерархии компонентов, чтобы передать ему свойство value . Только так он может быть передан всем своим потомкам. И это будет index.js в нашем корне. Здесь находится App компонент. Компонент App может передавать любые данные или состояние в качестве свойств туда, где они необходимы. В index.js в корне вашего приложения вы должны обернутьRecipeProvider вокруг App, как показано ниже.

Отсюда все свойства передаются в наш файл контекста и доступны всем дочерним элементам компонента App, который, по сути, является каждым компонентом.

Самое интересное в том, как мы будем это использовать. Мы сделаем пример с компонентами RecipeList и RecipeSearch. Ваш RecipeList компонент должен выглядеть, как показано ниже.

Теперь мы импортировали RecipeContext из нашего файла и импортировали useContext из React. Внутри нашей функции мы создали переменную для хранения значения RecipeContext. Затем мы просто берем только те значения, которые нам нужны внутри RecipeList .

Это чисто по двум причинам. Сначала мы просто перешли к RecipeList прямо оттуда, где нам были нужны эти состояния и данные. Нам не нужно было передавать реквизиты от родителей к детям. Во-вторых, мы извлекли из компонента только необходимые свойства. Нам не нужно было давать RecipeList какие-то реквизиты, в которых он не нуждался ни для чего, кроме как для передачи его дочерним элементам. Мы просто переходим к этому дочернему компоненту напрямую, если ему нужны данные.

Давайте рассмотрим компонент RecipeSearch. Еще раз, давайте посмотрим код, а затем объясним его.

Как и в RecipeList, мы импортировали useContext и RecipeContext. Мы создали переменные. И посмотрите, насколько это чисто и лаконично. Нам не нужно было получать handleSubmit и handleSearchChange от RecipeList. Мы просто взяли их из контекста здесь.

Вот и все. Мы рассмотрели три основных типа хуков: useState, useEffect и useContext . Я считаю, что это основы, необходимые для понимания более продвинутых и лучших способов работы с React Hooks. Я считаю, что они чище и легче усваиваются.

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

Спасибо! Полный код можно найти на GitHub.