大家好,上一篇文章我们学习了 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; });
仅仅一行代码,我们就实现了两个生命周期等效的效果。
还有一个简单例子,在某些情况下,你需要在组件卸载(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> ); }
运行后的效果界面如下所示:
显而易见,我们使用 hook 代码完成了同样的事情,代码量更少,结构更紧凑。你是否注意到我们在这个 useEffect Hook 中返回了一个函数?这种写法就等同 componentWillUnmount(),你可以在这里做一些和清楚逻辑相关的一些处理逻辑。
在开篇的时候,我们使用 useEffect Hook 实现了等同 componentDidMount,componentDidUpdate 两个钩子函数的一致的效果,这就意味着DOM加载完成,状态发生变化都会执行 useEffect Hook 中的逻辑,在一些场景下,我们没必要在状态发生变化时,调用次函数的逻辑,比如我们在这里定义数据接口更改数据状态,数据状态发生变化,会重新调用 useEffect Hook 中的请求逻辑,这样岂不是进入了死循环,数据量大的话,说不定就把接口请求死了。
但是还好, useEffect Hook 提供了依赖使用参数,第一个参数是定义方法,开篇已经介绍过了,第二个参数是依赖数组,用于自定义控制是否再次执行,接下来我们来看几个示例效果:
useEffect(() => { // run after every rendering console.log('render') })
如上图所示,我们每次更改状态值,导致组件重新渲染,我们在 useEffect 中定义的输出将会反复的被执行输出。
我们可以在第二个参数上定义一个空数组,告诉 hook 组件只执行一次,示例代码如下:
useEffect(() => { // Just run the first time console.log('render') }, [])
如上图运行效果所示,你会发现 hook 函数中定义的输出,无论我们怎么更改状态值,其只输出一次。
如果你想依赖特定的状态值、属性,如果其发生变化时,再次执行 hook 函数中定义的逻辑,你可以将其写在数组内,示例代码如下:
useEffect(() => { // When title or name changed will render console.log('render') }, [title, name])
说了这么多,我们做一下总结,说白了就是整合了 componentDidMount,componentDidUpdate,与 componentWillUnmount 这三个生命钩子函数,变成了一个API,其用法可以用如下一张图进行精简概括
大家好,在上一篇系列文章里《 React Hooks 学习笔记 | State Hook(一)》,我们通过做一个简单的购物清单的形式实践了 State Hook,本篇文章我们通过继续完善这个实例,加深我们对useEffect Hook 的学习,学习之前大家可以先提前下载上一篇文章的源码。
本节案例,为了更加接近实际更加实用的场景,这里我使用了 Firebase 快速构建后端的数据库和接口服务。(谷歌的产品,目前需要科学上网才能使用,Firebase是Google Cloud Platform为应用开发者们推出的应用后台服务。借助Firebase,应用开发者们可以快速搭建应用后台,集中注意力在开发client上,并且可以享受到Google Cloud的稳定性和scalability)。
1、在 https://firebase.google.com/(科学上网才能访问),使用谷歌账户登录 ,进入控制台创建项目。
2、这里我创建了一个 react-hook-update 项目里,创建了 Realtime Database 实时数据库(非关系数据库),用于存储项目的数据,其数据库又提供了相关的接口用于数据的增删改查。每个数据库都会提供一个链接用于操作,本项目数据库链接为 https://react-hook-update-350d4-default-rtdb.firebaseio.com/
接下来我们添加状态加载进度组件和错误提示对话框组件,分别用于状态加载中状态提示和系统错误状态提示,代码比较简单,这里就是贴下相关代码。
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 */
接下来,我们在购物清单页 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 函数中,依赖参数为空数组[ ],表示只加载一次,数据状态更新时,就不会发生无限循环去请求接口了,这个很重要、很重要、很重要!
这里我们要改写删除清单的方法,将删除的数据更新到云端数据库 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代码块里捕捉错误,更新错误状态和加载状态。
接着我们改写添加清单的方式,通过接口请求的方式,将添加的数据添加至 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
我们继续完善购物清单的功能,为界面添加搜索功能,方便我们搜索清单的内容,界面效果如下图所示,在中间添加一个搜索框。
我们创建 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,敬请期待。
注:本文属于原创文章,版权属于「前端达人」公众号及 qianduandaren.com 所有,未经授权,谢绝一切形式的转载