首页与我联系

React Hooks 学习笔记 | useEffect Hook(二)

By 前端达人
Published in 4-React
August 13, 2022
2 min read
React Hooks 学习笔记 | useEffect Hook(二)

大家好,上一篇文章我们学习了 State Hook 的基础用法,还没看的亲们可以看《 React Hooks 学习笔记 | State Hook(一)》这篇文章,今天我们继续来了解 useEffect Hook 的基础用法。

一、开篇

大多数的组件都需要特殊的操作,比如获取数据、监听数据变化或更改DOM的相关操作,这些操作被称作 “side effects(副作用)”。

在类组件中,我们通常会在 componentDidMount 和 componentDidUpdate 两个钩子函数进行操作,这些生命周期的相关方法便于我们更加精确的控制组件的相关行为。

这有一个简单代码示例,页面加载完成后,更改页面的标题

componentDidMount() {
  document.title = this.state.name + " from " + this.state.location;
}

当你尝试更改对应的状态值时,页面的标题不会发生任何变化,你需要添加另一个声明周期的方法componentDidUpdate(),监听状态值的变化,示例代码如下:

componentDidUpdate() {
  document.title = this.state.name + " from " + this.state.location;
}

从上述代码,要实现动态更改页面标题的方法,我们需要调用两个生命钩子函数,写上重复的实现逻辑,我们可以使用 useEffect Hook 函数,示例代码如下:

import React, { useState, useEffect } from "react";
//...
useEffect(() => {
  document.title = name + " from " + location;
});

仅仅一行代码,我们就实现了两个生命周期等效的效果。

react-hooks-class-title.png

二、添加清除代码

还有一个简单例子,在某些情况下,你需要在组件卸载(unmounted)或销毁(destroyed)之前,做一些有必要的清楚操作,比如无效的timers、interval,或者取消网络请求,或者清理任何在componentDidMount()中创建的DOM元素(elements),你可能会想到类组件中的 componentWillUnmount()这个钩子函数,示例代码如下:

import React from "react";
export default class ClassDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      resolution: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    };
    this.handleResize = this.handleResize.bind(this);
  }
  componentDidMount() {
    window.addEventListener("resize", this.handleResize);
  }
  componentDidUpdate() {
    window.addEventListener("resize", this.handleResize);
  }
  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
  }
  handleResize() {
    this.setState({
      resolution: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    });
  }
  render() {
    return (
      <section>
        <h3>
          {this.state.resolution.width} x {this.state.resolution.height}
        </h3>
      </section>
    )
  }
}

上面的代码将显示浏览器窗口的当前分辨率。当你调整窗口大小,您应该会看到数字自动更新,同时我们又添加了组件销毁时,在 componentWillUnmount() 函数中定义清除监听窗口大小的逻辑。

如果我们使用 Hook 的方式改写上述代码,看起来更加简洁,示例代码如下:

import React, { useState, useEffect } from "react";
export default function HookDemo(props) {
  ...
  const [resolution, setResolution] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  useEffect(() => {
    const handleResize = () => {
      setResolution({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    window.addEventListener("resize", handleResize);
    // return clean-up function
    return () => {
      document.title = 'React Hooks Demo';
      window.removeEventListener("resize", handleResize);
    };
  });
  ...
  return (
    <section>
      ...
      <h3>
        {resolution.width} x {resolution.height}
      </h3>
    </section>
  );
}

运行后的效果界面如下所示:

react-hooks-class-resolution.png

显而易见,我们使用 hook 代码完成了同样的事情,代码量更少,结构更紧凑。你是否注意到我们在这个 useEffect Hook 中返回了一个函数?这种写法就等同 componentWillUnmount(),你可以在这里做一些和清楚逻辑相关的一些处理逻辑。

三、关于 [ ] 依赖数组参数的说明

在开篇的时候,我们使用 useEffect Hook 实现了等同 componentDidMount,componentDidUpdate 两个钩子函数的一致的效果,这就意味着DOM加载完成,状态发生变化都会执行 useEffect Hook 中的逻辑,在一些场景下,我们没必要在状态发生变化时,调用次函数的逻辑,比如我们在这里定义数据接口更改数据状态,数据状态发生变化,会重新调用 useEffect Hook 中的请求逻辑,这样岂不是进入了死循环,数据量大的话,说不定就把接口请求死了。

但是还好, useEffect Hook 提供了依赖使用参数,第一个参数是定义方法,开篇已经介绍过了,第二个参数是依赖数组,用于自定义控制是否再次执行,接下来我们来看几个示例效果:

3.1、after every render

useEffect(() => { 
// run after every rendering 
console.log('render') 
})

React hook 示例/1_Ql4B8N4dsEyCjZvEMCB5VA.gif

如上图所示,我们每次更改状态值,导致组件重新渲染,我们在 useEffect 中定义的输出将会反复的被执行输出。

3.2 Once(执行一次)

我们可以在第二个参数上定义一个空数组,告诉 hook 组件只执行一次,示例代码如下:

useEffect(() => {
    // Just run the first time
    console.log('render')
  }, [])

React 示例.gif

如上图运行效果所示,你会发现 hook 函数中定义的输出,无论我们怎么更改状态值,其只输出一次。

3.3、依赖 state/props 的改变再执行

如果你想依赖特定的状态值、属性,如果其发生变化时,再次执行 hook 函数中定义的逻辑,你可以将其写在数组内,示例代码如下:

useEffect(() => {
    // When title or name changed will render
    console.log('render')
}, [title, name])

四、用一张图总结下

说了这么多,我们做一下总结,说白了就是整合了 componentDidMount,componentDidUpdate,与 componentWillUnmount 这三个生命钩子函数,变成了一个API,其用法可以用如下一张图进行精简概括

React hook 示例.jpeg

五、继续完善购物清单

大家好,在上一篇系列文章里《 React Hooks 学习笔记 | State Hook(一)》,我们通过做一个简单的购物清单的形式实践了 State Hook,本篇文章我们通过继续完善这个实例,加深我们对useEffect Hook 的学习,学习之前大家可以先提前下载上一篇文章的源码。

本节案例,为了更加接近实际更加实用的场景,这里我使用了 Firebase 快速构建后端的数据库和接口服务。(谷歌的产品,目前需要科学上网才能使用,Firebase是Google Cloud Platform为应用开发者们推出的应用后台服务。借助Firebase,应用开发者们可以快速搭建应用后台,集中注意力在开发client上,并且可以享受到Google Cloud的稳定性和scalability)。

React 示例.png

5.1、创建Firebase

1、在 https://firebase.google.com/(科学上网才能访问),使用谷歌账户登录 ,进入控制台创建项目。

React 示例.png

2、这里我创建了一个 react-hook-update 项目里,创建了 Realtime Database 实时数据库(非关系数据库),用于存储项目的数据,其数据库又提供了相关的接口用于数据的增删改查。每个数据库都会提供一个链接用于操作,本项目数据库链接为 https://react-hook-update-350d4-default-rtdb.firebaseio.com/

React-Hooks-示例.png

5.2、添加状态加载、错误提示UI组件

接下来我们添加状态加载进度组件和错误提示对话框组件,分别用于状态加载中状态提示和系统错误状态提示,代码比较简单,这里就是贴下相关代码。

LoadingIndicator 数据加载状态提示组件

import React from 'react';

import './LoadingIndicator.css';

const LoadingIndicator = () => (
    <div className="lds-ring">
        <div />
        <div />
        <div />
        <div />
    </div>
);

export default LoadingIndicator;

// components/UI/LoadingIndicator.js
.lds-ring {
  display: inline-block;
  position: relative;
  width: 54px;
  height: 54px;
}
.lds-ring div {
  box-sizing: border-box;
  display: block;
  position: absolute;
  width: 44px;
  height: 44px;
  margin: 6px;
  border: 6px solid #ff2058;
  border-radius: 50%;
  animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
  border-color: #ff2058 transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
  animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
  animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
  animation-delay: -0.15s;
}
@keyframes lds-ring {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

/*
 components/UI/LoadingIndicator.css
*/

ErrorModal 系统错误组件

import React from "react";
import './ErrorModal.css'

const ErrorModal = React.memo(props => {
    return (
        <React.Fragment>
            <div className="backdrop" onClick={props.onClose} />
            <div className="error-modal">
                <h2>An Error Occurred!</h2>
                <p>{props.children}</p>
                <div className="error-modal__actions">
                    <button type="button" onClick={props.onClose}>
                        Okay
                    </button>
                </div>
            </div>
        </React.Fragment>
    );
});

export default ErrorModal;

// components/UI/ErrorModal.js
.error-modal {
  position: fixed;
  top: 30vh;
  left: calc(50% - 15rem);
  width: 30rem;
  background: white;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
  z-index: 100;
  border-radius: 7px;
}

.error-modal h2 {
  margin: 0;
  padding: 1rem;
  background: #ff2058;
  color: white;
  border-radius: 7px 7px 0 0;
}

.error-modal p {
    padding: 1rem;
}

.error-modal__actions {
    display: flex;
    justify-content: flex-end;
    padding: 0 0.5rem;
}

.backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  background: rgba(0, 0, 0, 0.75);
  z-index: 50;
}

/*
 components/UI/ErrorModal.css
*/

5.3、增加接口显示购物清单列表

接下来,我们在购物清单页 Ingredients 组件里,我们使用今天所学的知识,在 useEffect() 里添加历史购物清单的列表接口请求,这里我们使用 firebase 的提供的API, 请求 https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients.json 这个地址,就会默认给你创建ingredients 的集合,并返回一个JSON形式的数据集合,示例代码如下:

useEffect(() => {
        fetch('https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients.json')
            .then(response => response.json())
            .then(responseData => {
                const loadedIngredients = [];
                for (const key in responseData) {
                    loadedIngredients.push({
                        id: key,
                        title: responseData[key].title,
                        amount: responseData[key].amount
                    });
                }
                setUserIngredients(loadedIngredients);
            })
    }, []);

// components/Ingredients/Ingredients.js

上述代码我们可以看出,我们使用 fetch 函数请求接口,请求完成后我们更新 UserIngredients 数据状态,最后别忘记了,同时在 useEffect 函数中,依赖参数为空数组[ ],表示只加载一次,数据状态更新时,就不会发生无限循环去请求接口了,这个很重要、很重要、很重要!

5.4 、更新删除清单的方法

这里我们要改写删除清单的方法,将删除的数据更新到云端数据库 Firebase ,为了显示更新状态和系统的错误信息,这里我们引入 ErrorModal ,添加数据加载状态和错误状态,示例如下:

import ErrorModal from '../UI/ErrorModal';
const Ingredients = () => {
...
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState();
...
}

// components/Ingredients/Ingredients.js

接下来我们来改写删除方法removeIngredientHandler

const removeIngredientHandler = ingredientId => {
        setIsLoading(true);
        fetch(`https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients/${ingredientId}.json`,
            {
                method: 'DELETE'
            }
        ).then(response => {
            setIsLoading(false);
            setUserIngredients(prevIngredients =>
                prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
            );
        }).catch(error => {
            setError('系统开了个小差,请稍后再试!');
            setIsLoading(false);
        })
    };

// components/Ingredients/Ingredients.js

从上述代码我们可以看出,首先我们先将加载状态定义为true,接下来请求删除接口,这里请注意 接口地址 ${ingredientId} 这个变量的使用(当前数据的ID),删除成功后,更新加载状态false。如果删除过程中发生错误,我们在catch代码块里捕捉错误,更新错误状态和加载状态。

5.5、更新添加清单的方法

接着我们改写添加清单的方式,通过接口请求的方式,将添加的数据添加至 Firebase 数据库,代码比较简单,就不多解释了,示例代码如下:

const addIngredientHandler = ingredient => {
        setIsLoading(true);
        fetch('https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients.json',
            {
            method: 'post',
            body: JSON.stringify(ingredient),
            headers: {'Content-Type': 'application/json'}
        })
            .then(response => {
            setIsLoading(false);
            return response.json();
        })
            .then(responseData => {
            setUserIngredients(prevIngredients => [
                ...prevIngredients,
                {id: responseData.name, ...ingredient}
            ]);
        });

    };

// components/Ingredients/Ingredients.js

5.5、添加搜索组件功能

我们继续完善购物清单的功能,为界面添加搜索功能,方便我们搜索清单的内容,界面效果如下图所示,在中间添加一个搜索框。

React-hook-示例.png

我们创建 Search.js 文件,然后在 useEffect 方法内通过 Firebase 提供的接口形式请求接口,基于商品名称搜索购物清单,然后定义 onLoadIngredients 方法属性,用于接收返回的数据,方便父组件集成使用。最后我们定义 enteredFilter 数据状态,用于接收用户输入框的输入内容。代码如下所示:

import React,{useState,useEffect,useRef} from "react";
import Card from "../UI/Card";
import './Search.css';

const Search = React.memo(props=>{
    const { onLoadIngredients } = props;
    const [enteredFilter,setEnterFilter]=useState('');
    const inputRef = useRef();
    useEffect(() => {
        const timer = setTimeout(() => {
            if (enteredFilter === inputRef.current.value) {
                const query =
                    enteredFilter.length === 0
                        ? ''
                        : `?orderBy="title"&equalTo="${enteredFilter}"`;
                fetch(
                    'https://react-hook-update-350d4-default-rtdb.firebaseio.com/ingredients.json' + query
                )
                    .then(response => response.json())
                    .then(responseData => {
                        const loadedIngredients = [];
                        for (const key in responseData) {
                            loadedIngredients.push({
                                id: key,
                                title: responseData[key].title,
                                amount: responseData[key].amount
                            });
                        }
                        onLoadIngredients(loadedIngredients);
                    });
            }
        }, 500);
        return () => {
            clearTimeout(timer);
        };
    }, [enteredFilter, onLoadIngredients, inputRef]);
    return(
        <section className="search">
            <Card>
                <div className="search-input">
                    <label>搜索商品名称</label>
                    <input
                        ref={inputRef}
                        type="text"
                        value={enteredFilter}
                        onChange={event=>setEnterFilter(event.target.value)}
                    />
                </div>
            </Card>
        </section>
    )

});

export  default Search;

// components/Ingredients/Search.js

上述代码,我们定义为了避免频繁触发接口,定义了一个定时器,在用户输入500毫秒后在请求接口。你可以看到 useEffect() 里,我们使用了 return 方法,用于清理定时器,要不我们会有无限多的接口请求。同时依赖参数有三个 [enteredFilter, onLoadIngredients,inputRef],只有用户的输入内容和事件属性发生变化时,才会再次触发 useEffect() 中的逻辑。这里我们用到了useRef 方法获取输入框的值,关于其详细的介绍,会在稍后的文章介绍。

接下来贴上 Search.css 的相关代码,由于内容比较简单,这里就不过多解释了。

.search {
  width: 30rem;
  margin: 2rem auto;
  max-width: 80%;
}

.search-input {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-direction: column;
}

.search-input input {
  font: inherit;
  border: 1px solid #ccc;
  border-radius: 5px;
  padding: 0.15rem 0.25rem;
}

.search-input input:focus {
  outline: none;
  border-color: #ff2058;
}

@media (min-width: 768px) {
  .search-input {
    flex-direction: row;
  }
}

/* 
components/Ingredients/Search.css
*/

最后我们将 Search 组件添加至清单页面,在这个页面里定义了一个 useCallback 的方法,类似Vue的 computed 缓存的特性,提升性能,这个方法主要用来接收 Search子组件传输数据,用于更新UserIngredients 数据中的状态,在稍后的文章里我会详细介绍,这里只是简单的贴下代码,示例代码如下:

const filteredIngredientsHandler = useCallback(filteredIngredients => {
        setUserIngredients(filteredIngredients)
    }, []);
// components/Ingredients/Ingredients.js

接下来在 return 里添加 Search 组件和 ErrorModal 组件,在 Search 组件的 ingredients 属性里添加上述定义的 filteredIngredientsHandler 方法接收组件返回的搜索列表更新 UserIngredients 数据状态,示例代码如下:

<div className="App">
     {error && <ErrorModal onClose={clearError}>{error}</ErrorModal>}
     <IngredientForm onAddIngredient={addIngredientHandler} loading={isLoading}/>
            <section>
                <Search onLoadIngredients={filteredIngredientsHandler}/>
                <IngredientList ingredients={userIngredients} onRemoveItem={removeIngredientHandler}/>
            </section>
        </div>

到这里,本节的实践练习就完了,你可以点击阅读原文进行体验(科学上网才能,主要本案例采用了Firebase )。

六、结束了

好了,本篇关于 useEffect() 的介绍就聊完了,感谢你的阅读,你可以点击阅读原文体验本文的案例部分,如果你想获取源码请回复”r2”,小编建议亲自动手做一下,这样才能加深对 useEffect Hook的认知,下一篇本系列文章将会介绍 useRef,敬请期待。

版权声明

qrcode.jpg

注:本文属于原创文章,版权属于「前端达人」公众号及 qianduandaren.com 所有,未经授权,谢绝一切形式的转载


Tags

reacthook
Previous Article
React Hooks 学习笔记 | State Hook(一)
前端达人

前端达人

专注前端知识分享

Table Of Contents

1
一、开篇
2
二、添加清除代码
3
三、关于 依赖数组参数的说明
4
四、用一张图总结下
5
五、继续完善购物清单
6
六、结束了
7
版权声明

相关文章

「React Hooks 学习笔记」关于Custom Hooks 的使用介绍(八)
October 28, 2022
1 min

前端站点

VUE官网React官网TypeScript官网

公众号:前端达人

前端达人公众号