Когда вы пишете Эффект, линтер проверяет, включили ли вы все реактивные значения (такие как props и state), которые Эффект считывает, в список зависимостей вашего Эффекта. Это гарантирует, что ваш Эффект будет синхронизирован с последними реквизитами и состоянием вашего компонента. Ненужные зависимости могут привести к тому, что ваш Эффект будет запускаться слишком часто или даже создаст бесконечный цикл. Следуйте этому руководству, чтобы просмотреть и удалить ненужные зависимости из ваших Эффектов.
Как исправить бесконечные циклы зависимостей Эффектов
Что делать, если вы хотите удалить зависимость
Как считать значение из вашего Эффекта, не "реагируя" на него
Как и почему следует избегать зависимостей от объектов и функций
Почему подавление линтера зависимостей опасно, и что делать вместо этого.
Зависимости должны соответствовать коду {/dependencies-should-match-the-code/}¶
Когда вы пишете эффект, вы сначала указываете, как запускать и останавливать то, что вы хотите, чтобы делал ваш эффект:
import{useState,useEffect}from'react';import{createConnection}from'./chat.js';constserverUrl='https://localhost:1234';functionChatRoom({roomId}){useEffect(()=>{constconnection=createConnection(serverUrl,roomId);connection.connect();return()=>connection.disconnect();},[]);// <-- Fix the mistake here!return<h1>Welcometothe{roomId}room!</h1>;}exportdefaultfunctionApp(){const[roomId,setRoomId]=useState('general');return(<><label>Choosethechatroom:{' '}<selectvalue={roomId}onChange={(e)=>setRoomId(e.target.value)}><optionvalue="general">general</option><optionvalue="travel">travel</option><optionvalue="music">music</option></select></label><hr/><ChatRoomroomId={roomId}/></>);}
1 2 3 4 5 6 7 8 910111213141516171819202122
exportfunctioncreateConnection(serverUrl,roomId){// A real implementation would actually connect to the serverreturn{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');},disconnect(){console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl);},};}
Заполните их в соответствии с указаниями на вкладыше:
1 2 3 4 5 6 7 8 91011
functionChatRoom({roomId}){useEffect(()=>{constconnection=createConnection(serverUrl,roomId);connection.connect();return()=>connection.disconnect();},[roomId]);// ✅ All dependencies declared// ...}
Эффекты "реагируют" на реактивные значения. Поскольку roomId является реактивным значением (оно может измениться в результате повторного рендеринга), линтер проверяет, указали ли вы его в качестве зависимости. Если roomId получит другое значение, React пересинхронизирует ваш Effect. Это гарантирует, что чат остается подключенным к выбранной комнате и "реагирует" на выпадающий список:
exportfunctioncreateConnection(serverUrl,roomId){// A real implementation would actually connect to the serverreturn{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');},disconnect(){console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl);},};}
Чтобы удалить зависимость, докажите, что она не является зависимостью {/to-remove-a-dependency-prove-that-its-not-a-dependency/}¶
Обратите внимание, что вы не можете "выбрать" зависимости вашего Эффекта. Каждый \<CodeStep step={2}>реактивное значение\</CodeStep>, используемый кодом вашего Эффекта, должен быть объявлен в списке зависимостей. Список зависимостей определяется окружающим кодом:
1 2 3 4 5 6 7 8 91011121314
constserverUrl='https://localhost:1234';functionChatRoom({roomId}){// This is a reactive valueuseEffect(()=>{constconnection=createConnection(serverUrl,roomId);// This Effect reads that reactive valueconnection.connect();return()=>connection.disconnect();},[roomId]);// ✅ So you must specify that reactive value as a dependency of your Effect// ...}
Reactive values включают props и все переменные и функции, объявленные непосредственно внутри вашего компонента. Поскольку roomId является реактивным значением, вы не можете удалить его из списка зависимостей. Линтер не позволит этого сделать:
1 2 3 4 5 6 7 8 910111213
constserverUrl='https://localhost:1234';functionChatRoom({roomId}){useEffect(()=>{constconnection=createConnection(serverUrl,roomId);connection.connect();return()=>connection.disconnect();},[]);// 🔴 React Hook useEffect has a missing dependency: 'roomId'// ...}
И линтер будет прав! Поскольку roomId может меняться со временем, это внесет ошибку в ваш код.
Чтобы удалить зависимость, "докажите" линтеру, что она не должна быть зависимостью. Например, вы можете убрать roomId из вашего компонента, чтобы доказать, что он не реактивный и не будет меняться при повторных рендерингах:
1 2 3 4 5 6 7 8 91011121314
constserverUrl='https://localhost:1234';constroomId='music';// Not a reactive value anymorefunctionChatRoom(){useEffect(()=>{constconnection=createConnection(serverUrl,roomId);connection.connect();return()=>connection.disconnect();},[]);// ✅ All dependencies declared// ...}
Теперь, когда roomId не является реактивным значением (и не может измениться при повторном рендере), ему не нужно быть зависимостью:
exportfunctioncreateConnection(serverUrl,roomId){// A real implementation would actually connect to the serverreturn{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');},disconnect(){console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl);},};}
Вот почему теперь вы можете указать пустой ([]) список зависимостей Ваш эффект действительно больше не зависит ни от какого реактивного значения, поэтому ему действительно не нужно перезапускаться при изменении реквизитов или состояния компонента.
Чтобы изменить зависимости, измените код {/to-change-the-dependencies-change-the-code/}¶
Возможно, вы заметили закономерность в своем рабочем процессе:
Сначала вы изменяете код вашего Эффекта или то, как объявлены ваши реактивные значения.
Затем, следуя за линтером, вы настраиваете зависимости, чтобы *соответствовать измененному коду.
Если список зависимостей вас не устраивает, вы возвращаетесь к первому шагу (и снова изменяете код).
Последняя часть очень важна. Если вы хотите изменить зависимости, сначала измените окружающий код. Вы можете думать о списке зависимостей как о списке всех реактивных значений, используемых кодом вашего Эффекта Вы не выбираете, что включить в этот список. Список описывает ваш код. Чтобы изменить список зависимостей, измените код.
Это может показаться похожим на решение уравнения. Вы можете начать с цели (например, удалить зависимость), и вам нужно "найти" код, соответствующий этой цели. Не все находят решение уравнений забавным, и то же самое можно сказать о написании Effects! К счастью, существует список общих рецептов, которые вы можете попробовать ниже.
Если у вас есть существующая кодовая база, возможно, у вас есть Effects, которые подавляют линтер, например, так:
12345
useEffect(()=>{// ...// 🔴 Avoid suppressing the linter like this:// eslint-ignore-next-line react-hooks/exhaustive-deps},[]);
Когда зависимости не соответствуют коду, очень высок риск появления ошибок. Подавляя линтер, вы "лжете" React о значениях, от которых зависит ваш Effect.
Вместо этого используйте приемы, описанные ниже.
Почему подавление линтера зависимостей так опасно? {/why-is-suppressing-the-dependency-linter-so-dangerous/}¶
Подавление линтера приводит к очень неинтуитивным ошибкам, которые трудно найти и исправить. Вот один из примеров:
Допустим, вы хотите запустить эффект "только при монтировании". Вы прочитали, что пустые ([]) зависимости делают это, поэтому вы решили проигнорировать линтер и принудительно указали [] в качестве зависимостей.
Этот счетчик должен был увеличиваться каждую секунду на величину, настраиваемую с помощью двух кнопок. Однако, поскольку вы "соврали" React, что этот Effect ни от чего не зависит, React вечно продолжает использовать функцию onTick из начального рендера. Во время этого рендераcount был 0 и increment был 1. Вот почему onTick из этого рендера всегда вызывает setCount(0 + 1) каждую секунду, и вы всегда видите 1. Подобные ошибки труднее исправить, когда они распространяются на несколько компонентов.
Всегда есть лучшее решение, чем игнорирование linter! Чтобы исправить этот код, вам нужно добавить onTick в список зависимостей. (Чтобы гарантировать, что интервал устанавливается только один раз, сделайте onTick событием эффекта.)
Мы рекомендуем рассматривать ошибку dependency lint как ошибку компиляции. Если вы не подавите ее, вы никогда не увидите ошибок, подобных этой. Остальная часть этой страницы документирует альтернативы для этого и других случаев.
Каждый раз, когда вы изменяете зависимости Эффекта, чтобы отразить код, посмотрите на список зависимостей. Имеет ли смысл повторно запускать Эффект при изменении любой из этих зависимостей? Иногда ответ будет "нет":
Вы можете захотеть повторно выполнить различные части вашего Эффекта при различных условиях.
Вы можете захотеть прочитать только последнее значение какой-то зависимости вместо того, чтобы "реагировать" на ее изменения.
Зависимость может меняться слишком часто непреднамеренно, потому что это объект или функция.
Чтобы найти правильное решение, вам нужно ответить на несколько вопросов о вашем Effect. Давайте пройдемся по ним.
Должен ли этот код переместиться в обработчик событий? {/should-this-code-move-to-an-event-handler/}¶
Первое, о чем вы должны подумать, это о том, должен ли этот код вообще быть Эффектом.
Представьте себе форму. При отправке вы устанавливаете переменную состояния submitted в значениеtrue. Вам нужно отправить POST-запрос и показать уведомление. Вы поместили эту логику в Effect, который "реагирует" на то, чтоsubmitted стала true:
1 2 3 4 5 6 7 8 91011121314151617
functionForm(){const[submitted,setSubmitted]=useState(false);useEffect(()=>{if(submitted){// 🔴 Avoid: Event-specific logic inside an Effectpost('/api/register');showNotification('Successfully registered!');}},[submitted]);functionhandleSubmit(){setSubmitted(true);}// ...}
Позже вы захотите стилизовать сообщение уведомления в соответствии с текущей темой, поэтому вы читаете текущую тему. Поскольку theme объявлена в теле компонента, она является реактивным значением, поэтому вы добавляете ее как зависимость:
1 2 3 4 5 6 7 8 9101112131415161718192021
functionForm(){const[submitted,setSubmitted]=useState(false);consttheme=useContext(ThemeContext);useEffect(()=>{if(submitted){// 🔴 Avoid: Event-specific logic inside an Effectpost('/api/register');showNotification('Successfully registered!',theme);}},[submitted,theme]);// ✅ All dependencies declaredfunctionhandleSubmit(){setSubmitted(true);}// ...}
Сделав это, вы ввели ошибку. Представьте, что вы сначала отправляете форму, а затем переключаетесь между темной и светлой темами. Тема изменится, Эффект запустится заново и снова покажет то же самое уведомление!
Проблема в том, что это вообще не должно быть Эффектом. Вы хотите отправить POST-запрос и показать уведомление в ответ на отправку формы, что является определенным взаимодействием. Чтобы выполнить некоторый код в ответ на определенное взаимодействие, поместите эту логику непосредственно в соответствующий обработчик события:
1 2 3 4 5 6 7 8 91011
functionForm(){consttheme=useContext(ThemeContext);functionhandleSubmit(){// ✅ Good: Event-specific logic is called from event handlerspost('/api/register');showNotification('Successfully registered!',theme);}// ...}
Ваш Эффект делает несколько несвязанных вещей? {/is-your-effect-doing-several-unrelated-things/}¶
Следующий вопрос, который вы должны задать себе, - не делает ли ваш Эффект несколько несвязанных вещей.
Представьте, что вы создаете форму доставки, в которой пользователю нужно выбрать город и область. Вы получаете список городов с сервера в соответствии с выбранной страной, чтобы показать их в выпадающем списке:
1 2 3 4 5 6 7 8 910111213141516171819
functionShippingForm({country}){const[cities,setCities]=useState(null);const[city,setCity]=useState(null);useEffect(()=>{letignore=false;fetch(`/api/cities?country=${country}`).then(response=>response.json()).then(json=>{if(!ignore){setCities(json);}});return()=>{ignore=true;};},[country]);// ✅ All dependencies declared// ...
Это хороший пример [получения данных в эффекте] (/learn/you-might-not-need-an-effect#fetching-data) Вы синхронизируете состояние cities с сетью в соответствии с параметром country. Вы не можете сделать это в обработчике событий, потому что вам нужно получить данные, как только отображается ShippingForm и всякий раз, когда изменяется country (независимо от того, какое взаимодействие это вызывает).
Теперь предположим, что вы добавляете второе поле выбора для районов города, которое должно получить районы для текущего выбранного города. Вы можете начать с добавления второго вызова fetch для списка областей внутри того же Effect:
functionShippingForm({country}){const[cities,setCities]=useState(null);const[city,setCity]=useState(null);const[areas,setAreas]=useState(null);useEffect(()=>{letignore=false;fetch(`/api/cities?country=${country}`).then(response=>response.json()).then(json=>{if(!ignore){setCities(json);}});// 🔴 Avoid: A single Effect synchronizes two independent processesif(city){fetch(`/api/areas?city=${city}`).then(response=>response.json()).then(json=>{if(!ignore){setAreas(json);}});}return()=>{ignore=true;};},[country,city]);// ✅ All dependencies declared// ...
Однако, поскольку Эффект теперь использует переменную состояния city, вам пришлось добавить city в список зависимостей. Это, в свою очередь, создало проблему: когда пользователь выберет другой город, Эффект запустится заново и вызовет fetchCities(country). В результате вы будете без необходимости многократно перевыполнять список городов.
Проблема этого кода в том, что вы синхронизируете две разные несвязанные вещи:.
Вы хотите синхронизировать состояние cities с сетью на основе реквизита country.
Вы хотите синхронизировать состояние areas с сетью на основе состояния city.
Разделите логику на два Effects, каждый из которых реагирует на реквизит, с которым ему нужно синхронизироваться:
functionShippingForm({country}){const[cities,setCities]=useState(null);useEffect(()=>{letignore=false;fetch(`/api/cities?country=${country}`).then(response=>response.json()).then(json=>{if(!ignore){setCities(json);}});return()=>{ignore=true;};},[country]);// ✅ All dependencies declaredconst[city,setCity]=useState(null);const[areas,setAreas]=useState(null);useEffect(()=>{if(city){letignore=false;fetch(`/api/areas?city=${city}`).then(response=>response.json()).then(json=>{if(!ignore){setAreas(json);}});return()=>{ignore=true;};}},[city]);// ✅ All dependencies declared// ...
Теперь первый Эффект запускается повторно только при изменении страны, а второй Эффект запускается повторно при изменении города. Вы разделили их по цели: две разные вещи синхронизируются двумя разными Эффектами. Два отдельных Эффекта имеют два отдельных списка зависимостей, поэтому они не будут непреднамеренно вызывать друг друга.
Окончательный код длиннее исходного, но разделение этих Эффектов все равно правильное. Каждый Эффект должен представлять независимый процесс синхронизации В этом примере удаление одного Эффекта не нарушает логику другого Эффекта. Это означает, что они синхронизируют разные вещи, и хорошо бы их разделить. Если вас беспокоит дублирование, вы можете улучшить этот код, [извлекая повторяющуюся логику в пользовательский хук] (/learn/reusing-logic-with-custom-hooks#when-to-use-custom-hooks).
Читаете ли вы какое-то состояние для вычисления следующего состояния? {/are-you-reading-some-state-to-calculate-the-next-state/}¶
Этот Эффект обновляет переменную состояния messages вновь созданным массивом каждый раз, когда приходит новое сообщение:
Она использует переменную messages для создания нового массива, начиная со всех существующих сообщений и добавляя новое сообщение в конец. Однако, поскольку messages - это реактивное значение, считываемое Эффектом, оно должно быть зависимым:
1 2 3 4 5 6 7 8 91011
functionChatRoom({roomId}){const[messages,setMessages]=useState([]);useEffect(()=>{constconnection=createConnection();connection.connect();connection.on('message',(receivedMessage)=>{setMessages([...messages,receivedMessage]);});return()=>connection.disconnect();},[roomId,messages]);// ✅ All dependencies declared// ...
А если сделать messages зависимостью, то возникает проблема.
Каждый раз, когда вы получаете сообщение, setMessages() вызывает повторное отображение компонента с новым массивом messages, который включает полученное сообщение. Однако, поскольку этот Эффект теперь зависит от messages, это также пересинхронизирует Эффект. Таким образом, каждое новое сообщение будет заставлять чат заново соединяться. Пользователю это не понравится!
Чтобы решить эту проблему, не читайте messages внутри Эффекта. Вместо этого, передайте функцию updater в setMessages:
1 2 3 4 5 6 7 8 91011
functionChatRoom({roomId}){const[messages,setMessages]=useState([]);useEffect(()=>{constconnection=createConnection();connection.connect();connection.on('message',(receivedMessage)=>{setMessages(msgs=>[...msgs,receivedMessage]);});return()=>connection.disconnect();},[roomId]);// ✅ All dependencies declared// ...
Поскольку ваш Effect теперь использует isMuted в своем коде, вы должны добавить его в зависимости:
1 2 3 4 5 6 7 8 910111213141516
functionChatRoom({roomId}){const[messages,setMessages]=useState([]);const[isMuted,setIsMuted]=useState(false);useEffect(()=>{constconnection=createConnection();connection.connect();connection.on('message',(receivedMessage)=>{setMessages(msgs=>[...msgs,receivedMessage]);if(!isMuted){playSound();}});return()=>connection.disconnect();},[roomId,isMuted]);// ✅ All dependencies declared// ...
Проблема заключается в том, что каждый раз, когда isMuted изменяется (например, когда пользователь нажимает кнопку "Muted"), Effect будет повторно синхронизироваться и снова подключаться к чату. Это не является желаемым пользовательским опытом! (В этом примере даже отключение линтера не поможет - если вы это сделаете, isMuted "застрянет" со своим старым значением).
import{useState,useEffect,useEffectEvent}from'react';functionChatRoom({roomId}){const[messages,setMessages]=useState([]);const[isMuted,setIsMuted]=useState(false);constonMessage=useEffectEvent(receivedMessage=>{setMessages(msgs=>[...msgs,receivedMessage]);if(!isMuted){playSound();}});useEffect(()=>{constconnection=createConnection();connection.connect();connection.on('message',(receivedMessage)=>{onMessage(receivedMessage);});return()=>connection.disconnect();},[roomId]);// ✅ All dependencies declared// ...
События эффектов позволяют разделить эффект на реактивные части (которые должны "реагировать" на реактивные значения, такие как roomId и их изменения) и нереактивные части (которые считывают только последние значения, например, onMessage считывает isMuted). Теперь, когда вы читаете isMuted внутри события эффекта, оно не должно быть зависимым от вашего эффекта. В результате, чат не будет переподключаться, когда вы переключаете настройку "Muted", решая первоначальную проблему!
Обертывание обработчика события из реквизита {/wrapping-an-event-handler-from-the-props/}¶
Вы можете столкнуться с подобной проблемой, когда ваш компонент получает обработчик события в качестве реквизита:
1 2 3 4 5 6 7 8 9101112
functionChatRoom({roomId,onReceiveMessage}){const[messages,setMessages]=useState([]);useEffect(()=>{constconnection=createConnection();connection.connect();connection.on('message',(receivedMessage)=>{onReceiveMessage(receivedMessage);});return()=>connection.disconnect();},[roomId,onReceiveMessage]);// ✅ All dependencies declared// ...
Предположим, что родительский компонент передает различную функцию onReceiveMessage при каждом рендере:
Поскольку onReceiveMessage является зависимостью, это заставит Эффект повторно синхронизироваться после каждого повторного рендеринга родителя. Это заставило бы его заново подключаться к чату. Чтобы решить эту проблему, оберните вызов в событие эффекта:
1 2 3 4 5 6 7 8 910111213141516
functionChatRoom({roomId,onReceiveMessage}){const[messages,setMessages]=useState([]);constonMessage=useEffectEvent(receivedMessage=>{onReceiveMessage(receivedMessage);});useEffect(()=>{constconnection=createConnection();connection.connect();connection.on('message',(receivedMessage)=>{onMessage(receivedMessage);});return()=>connection.disconnect();},[roomId]);// ✅ All dependencies declared// ...
События эффектов не являются реактивными, поэтому вам не нужно указывать их в качестве зависимостей. В результате чат больше не будет переподключаться, даже если родительский компонент передает функцию, которая отличается при каждом повторном рендере.
Разделение реактивного и нереактивного кода {/separating-reactive-and-non-reactive-code/}¶
В этом примере вы хотите регистрировать посещение при каждом изменении roomId. Вы хотите включать текущее значение notificationCount в каждый журнал, но вы не хотите, чтобы изменение notificationCount вызывало событие журнала.
Решение снова состоит в том, чтобы разделить нереактивный код на события Effect Event:
1 2 3 4 5 6 7 8 910
functionChat({roomId,notificationCount}){constonVisit=useEffectEvent((visitedRoomId)=>{logVisit(visitedRoomId,notificationCount);});useEffect(()=>{onVisit(roomId);},[roomId]);// ✅ All dependencies declared// ...}
Меняется ли непреднамеренно какое-то реактивное значение? {/does-some-reactive-value-change-unintentionally/}¶
Иногда вы хотите, чтобы ваш Эффект "реагировал" на определенное значение, но это значение меняется чаще, чем вам хотелось бы - и может не отражать никаких реальных изменений с точки зрения пользователя. Например, допустим, вы создаете объект options в теле вашего компонента, а затем считываете этот объект внутри вашего Эффекта:
Этот объект объявлен в теле компонента, поэтому это [реактивное значение] (/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) Когда вы читаете реактивное значение, подобное этому, внутри Эффекта, вы объявляете его как зависимость. Это гарантирует, что ваш Эффект "реагирует" на его изменения:
1234567
// ...useEffect(()=>{constconnection=createConnection(options);connection.connect();return()=>connection.disconnect();},[options]);// ✅ All dependencies declared// ...
Важно объявить его как зависимость! Это гарантирует, например, что если roomId изменится, то ваш Effect заново подключится к чату с новыми options. Однако в приведенном выше коде также есть проблема. Чтобы увидеть ее, попробуйте ввести данные в поле ввода в песочнице ниже и посмотрите, что произойдет в консоли:
import{useState,useEffect}from'react';import{createConnection}from'./chat.js';constserverUrl='https://localhost:1234';functionChatRoom({roomId}){const[message,setMessage]=useState('');// Temporarily disable the linter to demonstrate the problem// eslint-disable-next-line react-hooks/exhaustive-depsconstoptions={serverUrl:serverUrl,roomId:roomId,};useEffect(()=>{constconnection=createConnection(options);connection.connect();return()=>connection.disconnect();},[options]);return(<><h1>Welcometothe{roomId}room!</h1><inputvalue={message}onChange={(e)=>setMessage(e.target.value)}/></>);}exportdefaultfunctionApp(){const[roomId,setRoomId]=useState('general');return(<><label>Choosethechatroom:{' '}<selectvalue={roomId}onChange={(e)=>setRoomId(e.target.value)}><optionvalue="general">general</option><optionvalue="travel">travel</option><optionvalue="music">music</option></select></label><hr/><ChatRoomroomId={roomId}/></>);}
1 2 3 4 5 6 7 8 910111213141516171819202122
exportfunctioncreateConnection({serverUrl,roomId}){// A real implementation would actually connect to the serverreturn{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');},disconnect(){console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl);},};}
В приведенной выше песочнице ввод только обновляет переменную состояния message. С точки зрения пользователя, это не должно влиять на соединение с чатом. Однако каждый раз, когда вы обновляете message, ваш компонент перерисовывается. Когда ваш компонент пересматривается, код внутри него снова запускается с нуля.
Новый объект options создается с нуля при каждом повторном рендеринге компонента ChatRoom. React видит, что объект options является отличным объектом от объекта options, созданного во время последнего рендеринга. Поэтому он повторно синхронизирует ваш Effect (который зависит от options), и чат снова подключается, когда вы набираете текст.
Эта проблема затрагивает только объекты и функции. В JavaScript каждый вновь созданный объект и функция считаются отдельными от всех остальных. Не имеет значения, что содержимое внутри них может быть одинаковым!.
1 2 3 4 5 6 7 8 91011121314
// During the first renderconstoptions1={serverUrl:'https://localhost:1234',roomId:'music',};// During the next renderconstoptions2={serverUrl:'https://localhost:1234',roomId:'music',};// These are two different objects!console.log(Object.is(options1,options2));// false
*Зависимости от объектов и функций могут заставить ваш Эффект пересинхронизироваться чаще, чем вам нужно.
Поэтому, по возможности, старайтесь избегать объектов и функций в качестве зависимостей вашего Эффекта. Вместо этого попробуйте переместить их за пределы компонента, внутрь Эффекта или извлечь из них примитивные значения.
Перемещение статических объектов и функций за пределы компонента {/move-static-objects-and-functions-outside-your-component/}¶
Если объект не зависит от реквизитов и состояния, вы можете переместить этот объект за пределы вашего компонента:
1 2 3 4 5 6 7 8 91011121314
constoptions={serverUrl:'https://localhost:1234',roomId:'music'};functionChatRoom(){const[message,setMessage]=useState('');useEffect(()=>{constconnection=createConnection(options);connection.connect();return()=>connection.disconnect();},[]);// ✅ All dependencies declared// ...
Таким образом, вы доказываете линтеру, что он не реактивный. Он не может измениться в результате повторного рендеринга, поэтому ему не нужно быть зависимостью. Теперь повторное отображение ChatRoom не заставит ваш Effect повторно синхронизироваться.
Это работает и для функций:
1 2 3 4 5 6 7 8 91011121314151617
functioncreateOptions(){return{serverUrl:'https://localhost:1234',roomId:'music'};}functionChatRoom(){const[message,setMessage]=useState('');useEffect(()=>{constoptions=createOptions();constconnection=createConnection();connection.connect();return()=>connection.disconnect();},[]);// ✅ All dependencies declared// ...
Поскольку createOptions объявляется вне вашего компонента, это не реактивное значение. Поэтому его не нужно указывать в зависимостях вашего Эффекта, и поэтому он никогда не заставит ваш Эффект пересинхронизироваться.
Перемещение динамических объектов и функций внутри вашего Эффекта {/move-dynamic-objects-and-functions-inside-your-effect/}¶
Если ваш объект зависит от какого-то реактивного значения, которое может измениться в результате повторного рендеринга, например, параметр roomId, вы не можете переместить его внутрь вашего компонента. Однако вы можете переместить его создание внутрь кода вашего Эффекта:
1 2 3 4 5 6 7 8 9101112131415
constserverUrl='https://localhost:1234';functionChatRoom({roomId}){const[message,setMessage]=useState('');useEffect(()=>{constoptions={serverUrl:serverUrl,roomId:roomId};constconnection=createConnection(options);connection.connect();return()=>connection.disconnect();},[roomId]);// ✅ All dependencies declared// ...
Теперь, когда options объявлено внутри вашего Эффекта, оно больше не является зависимостью вашего Эффекта. Вместо этого, единственным реактивным значением, используемым вашим Эффектом, является roomId. Поскольку roomId не является объектом или функцией, вы можете быть уверены, что оно не будет непреднамеренно отличаться. В JavaScript числа и строки сравниваются по их содержанию:
12345678
// During the first renderconstroomId1='music';// During the next renderconstroomId2='music';// These two strings are the same!console.log(Object.is(roomId1,roomId2));// true
Благодаря этому исправлению чат больше не переподключается, если вы редактируете входные данные:
exportfunctioncreateConnection({serverUrl,roomId}){// A real implementation would actually connect to the serverreturn{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');},disconnect(){console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl);},};}
Однако он делает повторное подключение, когда вы изменяете выпадающий roomId, как вы и ожидали.
Это работает и для функций:
1 2 3 4 5 6 7 8 910111213141516171819
constserverUrl='https://localhost:1234';functionChatRoom({roomId}){const[message,setMessage]=useState('');useEffect(()=>{functioncreateOptions(){return{serverUrl:serverUrl,roomId:roomId};}constoptions=createOptions();constconnection=createConnection(options);connection.connect();return()=>connection.disconnect();},[roomId]);// ✅ All dependencies declared// ...
Вы можете писать свои собственные функции для группировки частей логики внутри вашего Эффекта. Если вы также объявляете их внутри вашего Эффекта, они не являются реактивными значениями, и поэтому им не нужно быть зависимыми от вашего Эффекта.
Чтение примитивных значений из объектов {/read-primitive-values-from-objects/}¶
Иногда вы можете получить объект из реквизита:
123456789
functionChatRoom({options}){const[message,setMessage]=useState('');useEffect(()=>{constconnection=createConnection(options);connection.connect();return()=>connection.disconnect();},[options]);// ✅ All dependencies declared// ...
Риск здесь заключается в том, что родительский компонент создаст объект во время рендеринга:
Это приведет к тому, что ваш Эффект будет подключаться заново каждый раз, когда родительский компонент будет перестраиваться. Чтобы исправить это, считывайте информацию из объекта вне Эффекта и избегайте зависимостей между объектом и функцией:
1 2 3 4 5 6 7 8 910111213
functionChatRoom({options}){const[message,setMessage]=useState('');const{roomId,serverUrl}=options;useEffect(()=>{constconnection=createConnection({roomId:roomId,serverUrl:serverUrl});connection.connect();return()=>connection.disconnect();},[roomId,serverUrl]);// ✅ All dependencies declared// ...
Логика становится немного повторяющейся (вы считываете некоторые значения из объекта вне Эффекта, а затем создаете объект с теми же значениями внутри Эффекта). Но это позволяет четко определить, от какой информации на самом деле зависит ваш Эффект. Если объект непреднамеренно пересоздается родительским компонентом, чат не будет переподключаться. Однако, если options.roomId или options.serverUrl действительно отличаются, чат будет переподключен.
Вычисление примитивных значений из функций {/calculate-primitive-values-from-functions/}¶
Тот же подход может работать и для функций. Например, предположим, что родительский компонент передает функцию:
Чтобы не делать его зависимым (и не заставлять его переподключаться при повторных рендерах), вызывайте его вне Эффекта. Это даст вам значения roomId и serverUrl, которые не являются объектами, и которые вы можете прочитать изнутри вашего Эффекта:
1 2 3 4 5 6 7 8 910111213
functionChatRoom({getOptions}){const[message,setMessage]=useState('');const{roomId,serverUrl}=getOptions();useEffect(()=>{constconnection=createConnection({roomId:roomId,serverUrl:serverUrl});connection.connect();return()=>connection.disconnect();},[roomId,serverUrl]);// ✅ All dependencies declared// ...
Это работает только для чистых функций, потому что их безопасно вызывать во время рендеринга. Если ваша функция является обработчиком событий, но вы не хотите, чтобы ее изменения повторно синхронизировали ваш Эффект, оберните ее в событие Эффекта.
\<Recap>
Зависимости всегда должны соответствовать коду.
Если вы недовольны своими зависимостями, вам нужно отредактировать код.
Подавление линтера приводит к очень запутанным ошибкам, и вы всегда должны избегать этого.
Чтобы удалить зависимость, нужно "доказать" линтеру, что она не нужна.
Если какой-то код должен выполняться в ответ на определенное взаимодействие, перенесите его в обработчик событий.
Если разные части вашего Эффекта должны запускаться повторно по разным причинам, разделите его на несколько Эффектов.
Если вы хотите обновить некоторое состояние на основе предыдущего состояния, передайте функцию обновления.
Если вы хотите прочитать последнее значение, не "реагируя" на него, извлеките событие Effect Event из вашего Эффекта.
В JavaScript объекты и функции считаются разными, если они были созданы в разное время.
Старайтесь избегать зависимостей между объектами и функциями. Перенесите их за пределы компонента или внутрь Эффекта.
Этот Эффект устанавливает интервал, который тикает каждую секунду. Вы заметили, что происходит что-то странное: кажется, что интервал уничтожается и создается заново каждый раз, когда он тикает. Исправьте код так, чтобы интервал не создавался постоянно заново.
\<Намек>
Кажется, что код этого Эффекта зависит от count. Есть ли способ избежать этой зависимости? Должен быть способ обновить состояние count на основе его предыдущего значения без добавления зависимости от этого значения.
\</Hint>
1 2 3 4 5 6 7 8 910111213141516171819
import{useState,useEffect}from'react';exportdefaultfunctionTimer(){const[count,setCount]=useState(0);useEffect(()=>{console.log('✅ Creating an interval');constid=setInterval(()=>{console.log('⏰ Interval tick');setCount(count+1);},1000);return()=>{console.log('❌ Clearing an interval');clearInterval(id);};},[count]);return<h1>Counter:{count}</h1>;}
\<Решение>
Вы хотите обновить состояние count до count + 1 изнутри Эффекта. Однако это заставит ваш Эффект зависеть от count, который меняется с каждым тиком, и поэтому ваш интервал создается заново на каждом тике.
Чтобы решить эту проблему, используйте функцию updater и напишите setCount(c => c + 1) вместо setCount(count + 1):
1 2 3 4 5 6 7 8 910111213141516171819
import{useState,useEffect}from'react';exportdefaultfunctionTimer(){const[count,setCount]=useState(0);useEffect(()=>{console.log('✅ Creating an interval');constid=setInterval(()=>{console.log('⏰ Interval tick');setCount((c)=>c+1);},1000);return()=>{console.log('❌ Clearing an interval');clearInterval(id);};},[]);return<h1>Counter:{count}</h1>;}
Вместо чтения count внутри Effect, вы передаете React инструкцию c => c + 1 ("увеличить это число!"). React применит ее при следующем рендере. А поскольку вам больше не нужно считывать значение count внутри вашего Эффекта, вы можете держать зависимости вашего Эффекта пустыми ([]). Это предотвратит повторное создание интервала в каждом тике вашего Эффекта.
В этом примере, когда вы нажимаете кнопку "Показать", появляется приветственное сообщение. Анимация длится секунду. При нажатии кнопки "Убрать" приветственное сообщение сразу же исчезает. Логика анимации затухания реализована в файле animation.js в виде обычного JavaScript animation loop. Вам не нужно изменять эту логику. Вы можете обращаться с ней как со сторонней библиотекой. Ваш Effect создает экземпляр FadeInAnimation для узла DOM, а затем вызывает start(duration) или stop() для управления анимацией. Длительность" контролируется ползунком. Отрегулируйте ползунок и посмотрите, как изменится анимация.
Этот код уже работает, но есть кое-что, что вы хотите изменить. В настоящее время, когда вы перемещаете ползунок, управляющий переменной состояния duration, он перезапускает анимацию. Измените поведение так, чтобы Эффект не "реагировал" на переменную duration. Когда вы нажимаете "Show", Эффект должен использовать текущую duration на ползунке. Однако перемещение ползунка само по себе не должно запускать анимацию.
\<Hint>
Есть ли внутри Эффекта строка кода, которая не должна быть реактивной? Как вы можете переместить нереактивный код из Эффекта?
\</Hint>
1 2 3 4 5 6 7 8 910111213
{"dependencies":{"react":"experimental","react-dom":"experimental","react-scripts":"latest"},"scripts":{"start":"react-scripts start","build":"react-scripts build","test":"react-scripts test --env=jsdom","eject":"react-scripts eject"}}
exportclassFadeInAnimation{constructor(node){this.node=node;}start(duration){this.duration=duration;if(this.duration===0){// Jump to end immediatelythis.onProgress(1);}else{this.onProgress(0);// Start animatingthis.startTime=performance.now();this.frameId=requestAnimationFrame(()=>this.onFrame());}}onFrame(){consttimePassed=performance.now()-this.startTime;constprogress=Math.min(timePassed/this.duration,1);this.onProgress(progress);if(progress<1){// We still have more frames to paintthis.frameId=requestAnimationFrame(()=>this.onFrame());}}onProgress(progress){this.node.style.opacity=progress;}stop(){cancelAnimationFrame(this.frameId);this.startTime=null;this.frameId=null;this.duration=0;}}
Ваш Эффект должен считывать последнее значение duration, но вы не хотите, чтобы он "реагировал" на изменения duration. Вы используете duration для запуска анимации, но запуск анимации не является реактивным. Извлеките нереактивную строку кода в событие эффекта и вызовите эту функцию из вашего эффекта.
1 2 3 4 5 6 7 8 910111213
{"dependencies":{"react":"experimental","react-dom":"experimental","react-scripts":"latest"},"scripts":{"start":"react-scripts start","build":"react-scripts build","test":"react-scripts test --env=jsdom","eject":"react-scripts eject"}}
exportclassFadeInAnimation{constructor(node){this.node=node;}start(duration){this.duration=duration;this.onProgress(0);this.startTime=performance.now();this.frameId=requestAnimationFrame(()=>this.onFrame());}onFrame(){consttimePassed=performance.now()-this.startTime;constprogress=Math.min(timePassed/this.duration,1);this.onProgress(progress);if(progress<1){// We still have more frames to paintthis.frameId=requestAnimationFrame(()=>this.onFrame());}}onProgress(progress){this.node.style.opacity=progress;}stop(){cancelAnimationFrame(this.frameId);this.startTime=null;this.frameId=null;this.duration=0;}}
В этом примере каждый раз, когда вы нажимаете кнопку "Toggle theme", чат переподключается. Почему это происходит? Исправьте ошибку, чтобы чат переподключался только тогда, когда вы редактируете URL сервера или выбираете другой чат.
Относитесь к chat.js как к внешней сторонней библиотеке: вы можете обратиться к ней, чтобы проверить ее API, но не редактируйте ее.
\<Hint>
Есть несколько способов исправить это, но в конечном итоге вы хотите избежать наличия объекта в качестве зависимости.
exportfunctioncreateConnection({serverUrl,roomId}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}return{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');},disconnect(){console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl);},};}
Ваш Эффект запускается повторно, потому что он зависит от объекта options. Объекты могут быть созданы заново непреднамеренно, вы должны стараться избегать их в качестве зависимостей ваших Эффектов, когда это возможно.
Наименее инвазивное решение - это считывать roomId и serverUrl прямо вне Эффекта, а затем сделать Эффект зависимым от этих примитивных значений (которые не могут измениться непреднамеренно). Внутри эффекта создайте объект и передайте его в createConnection:
exportfunctioncreateConnection({serverUrl,roomId}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}return{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');},disconnect(){console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl);},};}
exportfunctioncreateConnection({serverUrl,roomId}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}return{connect(){console.log('✅ Connecting to "'+roomId+'" room at '+serverUrl+'...');},disconnect(){console.log('❌ Disconnected from "'+roomId+'" room at '+serverUrl);},};}
Придерживаясь по возможности примитивных реквизитов, легче оптимизировать компоненты в дальнейшем.
\</Solution>
Исправление переподключающегося чата, снова {/fix-a-reconnecting-chat-again/}¶
Этот пример подключается к чату либо с шифрованием, либо без него. Переключите флажок и обратите внимание на разные сообщения в консоли, когда шифрование включено и выключено. Попробуйте сменить комнату. Затем попробуйте переключить тему. Когда вы подключены к чату, вы будете получать новые сообщения каждые несколько секунд. Убедитесь, что их цвет соответствует выбранной вами теме.
В данном примере чат подключается заново каждый раз, когда вы пытаетесь сменить тему. Исправьте это. После исправления смена темы не должна переподключать чат, но переключение настроек шифрования или смена комнаты должны переподключать.
Не изменяйте никакой код в файле chat.js. Кроме этого, вы можете изменять любой код, если он приводит к такому же поведению. Например, вы можете счесть полезным изменить, какие реквизиты передаются вниз.
\<Hint>
Вы передаете две функции: onMessage и createConnection. Обе они создаются с нуля каждый раз при повторном рендеринге App. Они рассматриваются как новые значения каждый раз, поэтому они повторно запускают ваш Effect.
Одна из этих функций является обработчиком событий. Знаете ли вы какой-нибудь способ вызвать Эффект из обработчика события, не "реагируя" на новые значения функции обработчика события? Это было бы очень удобно!
Еще одна из этих функций существует только для того, чтобы передать некоторое состояние импортированному методу API. Действительно ли эта функция необходима? Какая важная информация передается? Возможно, вам нужно перенести некоторые импорты из App.js в ChatRoom.js.
\</Hint>
1 2 3 4 5 6 7 8 91011121314
{"dependencies":{"react":"experimental","react-dom":"experimental","react-scripts":"latest","toastify-js":"1.12.0"},"scripts":{"start":"react-scripts start","build":"react-scripts build","test":"react-scripts test --env=jsdom","eject":"react-scripts eject"}}
exportfunctioncreateEncryptedConnection({serverUrl,roomId,}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}letintervalId;letmessageCallback;return{connect(){console.log('✅ 🔐 Connecting to "'+roomId+'" room... (encrypted)');clearInterval(intervalId);intervalId=setInterval(()=>{if(messageCallback){if(Math.random()>0.5){messageCallback('hey');}else{messageCallback('lol');}}},3000);},disconnect(){clearInterval(intervalId);messageCallback=null;console.log('❌ 🔐 Disconnected from "'+roomId+'" room (encrypted)');},on(event,callback){if(messageCallback){throwError('Cannot add the handler twice.');}if(event!=='message'){throwError('Only "message" event is supported.');}messageCallback=callback;},};}exportfunctioncreateUnencryptedConnection({serverUrl,roomId,}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}letintervalId;letmessageCallback;return{connect(){console.log('✅ Connecting to "'+roomId+'" room (unencrypted)...');clearInterval(intervalId);intervalId=setInterval(()=>{if(messageCallback){if(Math.random()>0.5){messageCallback('hey');}else{messageCallback('lol');}}},3000);},disconnect(){clearInterval(intervalId);messageCallback=null;console.log('❌ Disconnected from "'+roomId+'" room (unencrypted)');},on(event,callback){if(messageCallback){throwError('Cannot add the handler twice.');}if(event!=='message'){throwError('Only "message" event is supported.');}messageCallback=callback;},};}
Существует не один правильный способ решения этой проблемы, но вот одно из возможных решений.
В оригинальном примере переключение темы вызывало создание и передачу различных функций onMessage и createConnection. Поскольку эффект зависел от этих функций, чат переподключался каждый раз, когда вы переключали тему.
Чтобы решить проблему с onMessage, нужно было обернуть ее в событие эффекта:
В отличие от реквизита onMessage, событие эффекта onReceiveMessage не является реактивным. Поэтому оно не должно быть зависимым от вашего эффекта. В результате, изменения в onMessage не приведут к повторному подключению чата.
Вы не можете сделать то же самое с createConnection, потому что он должен быть реактивным. Вы хотите, чтобы Эффект повторно срабатывал, если пользователь переключается между зашифрованным и незашифрованным соединением, или если пользователь переключает текущую комнату. Однако, поскольку createConnection - это функция, вы не можете проверить, изменилась ли информация, которую она считывает, фактически или нет. Чтобы решить эту проблему, вместо передачи createConnection вниз из компонента App, передайте необработанные значения roomId и isEncrypted:
После этих двух изменений ваш Effect больше не зависит ни от каких значений функции:
1 2 3 4 5 6 7 8 9101112131415161718192021
exportdefaultfunctionChatRoom({roomId,isEncrypted,onMessage}){// Reactive valuesconstonReceiveMessage=useEffectEvent(onMessage);// Not reactiveuseEffect(()=>{functioncreateConnection(){constoptions={serverUrl:'https://localhost:1234',roomId:roomId// Reading a reactive value};if(isEncrypted){// Reading a reactive valuereturncreateEncryptedConnection(options);}else{returncreateUnencryptedConnection(options);}}constconnection=createConnection();connection.on('message',(msg)=>onReceiveMessage(msg));connection.connect();return()=>connection.disconnect();},[roomId,isEncrypted]);// ✅ All dependencies declared
В результате чат переподключается только тогда, когда меняется что-то значимое (roomId или isEncrypted):
1 2 3 4 5 6 7 8 91011121314
{"dependencies":{"react":"experimental","react-dom":"experimental","react-scripts":"latest","toastify-js":"1.12.0"},"scripts":{"start":"react-scripts start","build":"react-scripts build","test":"react-scripts test --env=jsdom","eject":"react-scripts eject"}}
exportfunctioncreateEncryptedConnection({serverUrl,roomId,}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}letintervalId;letmessageCallback;return{connect(){console.log('✅ 🔐 Connecting to "'+roomId+'" room... (encrypted)');clearInterval(intervalId);intervalId=setInterval(()=>{if(messageCallback){if(Math.random()>0.5){messageCallback('hey');}else{messageCallback('lol');}}},3000);},disconnect(){clearInterval(intervalId);messageCallback=null;console.log('❌ 🔐 Disconnected from "'+roomId+'" room (encrypted)');},on(event,callback){if(messageCallback){throwError('Cannot add the handler twice.');}if(event!=='message'){throwError('Only "message" event is supported.');}messageCallback=callback;},};}exportfunctioncreateUnencryptedConnection({serverUrl,roomId,}){// A real implementation would actually connect to the serverif(typeofserverUrl!=='string'){throwError('Expected serverUrl to be a string. Received: '+serverUrl);}if(typeofroomId!=='string'){throwError('Expected roomId to be a string. Received: '+roomId);}letintervalId;letmessageCallback;return{connect(){console.log('✅ Connecting to "'+roomId+'" room (unencrypted)...');clearInterval(intervalId);intervalId=setInterval(()=>{if(messageCallback){if(Math.random()>0.5){messageCallback('hey');}else{messageCallback('lol');}}},3000);},disconnect(){clearInterval(intervalId);messageCallback=null;console.log('❌ Disconnected from "'+roomId+'" room (unencrypted)');},on(event,callback){if(messageCallback){throwError('Cannot add the handler twice.');}if(event!=='message'){throwError('Only "message" event is supported.');}messageCallback=callback;},};}