Хук useEffect позволяет управлять различными сопутствующими действиями в функциональном компоненте
или то, что называется "side effects" (побочные эффекты), например, извлечение данных, ручное изменение структуры DOM, использование таймеров,
логгирование и т.д.. То есть в useEffect
выполняет те действия, которые мы не можем выполнить в основной части функционального компонента.
Этот хук фактически служит той же цели, что методы жизненного цикла componentDidMount, componentDidUpdate
и componentWillUnmount
в классах-компонентах.
В качестве параметра в useEffect()
передается функция. При вызове хука useEffect
по сути определяется "эффект", который затем применяется в приложении.
Когда именно применяется? По умолчанию React применяет эффект после каждого рендеринга, в том числе при первом рендеринге приложения.
Причем поскольку подобные эффекты определены внутри компонента, они имеют доступ к объекту props
и к состоянию компонента.
Например, изменение структуры DOM через useEffect
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="app"></div> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> function User() { const [name, setName] = React.useState("Tom"); React.useEffect(() => { // Изменяем заголовок html-страницы document.title = `Привет ${name}`; }); function changeName(event) { setName(event.target.value); } return ( <div> <h3>Имя: {name}</h3> <div> <p>Имя: <input type="text" value={name} onChange={changeName} /></p> </div> </div> ); } ReactDOM.createRoot( document.getElementById("app") ) .render( <User /> ); </script> </body> </html>
Здесь мы определяем эффект, который изменяет заголовок страницы. Причем в заголовок выводится значение переменной состояния - переменной name
.
То есть при загрузке страницы мы увидим в ее заголовке "Привет Tom".
Однако поскольку при вводе в текстовое поле мы изменяем значение в переменной name, и соответственно React будет выполнять перерендеринг приложения, то одновременно с этим будет изменяться и заголовок страницы:
По умолчанию эффект выполняется при каждом повторном рендеринге на веб-странице, однако мы можем указать, чтобы React не применял эффект, если определенные значения не изменились между с момента последнего рендеринга. Для этого в useEffect в качестве необязательного параметра передается массив аргументов.
Сначала рассмотрим саму проблему применения эффекта. Допустим у нас есть следующая страница:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="app"></div> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> function User() { const [name, setName] = React.useState("Tom"); const [age, setAge] = React.useState(36); React.useEffect(() => { // Изменяем заголовок html-страницы document.title = `Привет ${name}`; console.log("useEffect"); }); const changeName = (event) => setName(event.target.value); const changeAge =(event) => setAge(event.target.value); return ( <div> <h3>Имя: {name}</h3> <h3>Возраст: {age}</h3> <div> <p>Имя: <input type="text" value={name} onChange={changeName} /></p> <p>Возраст: <input type="number" value={age} onChange={changeAge} /></p> </div> </div> ); } ReactDOM.createRoot( document.getElementById("app") ) .render( <User /> ); </script> </body> </html>
В данном случае в компоненте определены две переменных состояния: name и age. При этом эффект использует только переменную name. Однако даже если переменная name останется без изменений, но переменная age будет изменена, в этом случае эффект будет повторно срабатывать:
Это не самое желательное поведение, в котором нет никакого смысла. И чтобы указать, что эффект применяется только при изменении переменной name,
передадим ее в качестве необязательного параметра в функцию useEffect
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="app"></div> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> function User() { const [name, setName] = React.useState("Tom"); const [age, setAge] = React.useState(36); React.useEffect(() => { // Изменяем заголовок html-страницы document.title = `Привет ${name}`; console.log("useEffect"); }, [name]); // эффект срабатывает только при изменении name const changeName = (event) => setName(event.target.value); const changeAge =(event) => setAge(event.target.value); return ( <div> <h3>Имя: {name}</h3> <h3>Возраст: {age}</h3> <div> <p>Имя: <input type="text" value={name} onChange={changeName} /></p> <p>Возраст: <input type="number" value={age} onChange={changeAge} /></p> </div> </div> ); } ReactDOM.createRoot( document.getElementById("app") ) .render( <User /> ); </script> </body> </html>
Если мы хотим, чтобы эффект вызывался только один раз при самом первом рендеринге, то в качестве параметра передаются пустые
квадратные скобки - []
.
React.useEffect(() => { // Изменяем заголовок html-страницы document.title = `Привет ${name}`; console.log("useEffect"); }, []); // эффект срабатывает только один раз - при самом первом рендеринге
Нередко в приложении возникает необходимость подисывается на различные ресурсы, а после окончания работы и отписываться от них.
В этом случае мы можем использовать специальную форму хука useEffect()
:
useEffect(() => { // код подписки на ресурс return () => { // код отписки от ресурса }; });
В начале функции хука идет подписка на ресурс, а далее оператор return
возвращает функцию, которая выполняет отписку от ресурса.
В качестве примера используем подписку/отписку на событие кнопки:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="app"></div> <button id="unmountBtn">Unmount</button> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> const root = ReactDOM.createRoot( document.getElementById("app") ); function User() { const [name, setName] = React.useState("Tom"); const unmount = () => root.unmount(); React.useEffect(() => { const unmountBtn = document.getElementById("unmountBtn"); // подписываемся на событие onclick кнопки unmountBtn unmountBtn.addEventListener("click", unmount); console.log("EventListener added"); return()=>{ // отписываемся от события unmountBtn.removeEventListener("click", unmount); console.log("EventListener removed"); } }, []); // эффект срабатывает только один раз - при самом первом рендеринге return (<div> <h3>Имя: {name}</h3> <p>Имя: <input type="text" value={name} onChange={(e) => setName(e.target.value)} /></p> </div>); } root.render( <User /> ); </script> </body> </html>
Итак, на странице определеная кнопка с id=unmountBtn
, на событие которой мы будем подписываться. В качестве действия, которое будет выполнять кнопка,
в компоненте User определена функция unmount()
, которая удаляет данный компонент с веб-страницы:
root.unmount();
В хуке useEffect
сначала подписываемся на событие "click" кнопки (то есть на событие нажатия):
unmountBtn.addEventListener("click", unmount);
Оператор return
возвращает функцию, которая выполняет отписку от события:
unmountBtn.removeEventListener("click", unmount);
И теперь важный момент: хук useEffect
в данном случае будет срабатывать один раз - при самом первом рендеринге приложения -
для этого в функцию в качестве необязательного параметра передается пустой массив. И соответственно функция очистки ресурсов, которая возвращаается
оператором return будет выполняться один раз - при удалении компонента с веб-страницы.
React.useEffect(() => { //............................ return()=>{ //................. } }, []); // пустой массив - хук выполняется один раз
То есть получится следущее: при загрузке компонент подисывается на событие кнопки. По нажатию на кнопку удаляется компонент. При удалении в хуке происходит удаление подписки - она нам больше не нужна, так как компонент уже удален.