React中如何通过useEffect实现类似Vue的watch监视属性功能?

2026-04-02 06:331阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计4227个文字,预计阅读时间需要17分钟。

React中如何通过useEffect实现类似Vue的watch监视属性功能?

目录 + React的watch监视属性 + useEffect使用指南 + 最基本的用法 + 响应更新 + 如何处理Loading和Error + 处理表格 + 自定义hooks + 使用useReducer整合逻辑 + 取消数据请求 + React的watch监视属性 + useEffect +

目录
  • react的watch监视属性-useEffect
  • useEffect使用指南
    • 最基本的使用
    • 响应更新
    • 如何处理Loading和Error
    • 处理表单
    • 自定义hooks
    • 使用useReducer整合逻辑
    • 取消数据请求

react的watch监视属性-useEffect

在vue中可以使用watch属性,去监视一个值,当这个值进行变化的时候就去执行一些操作。在react是没有这个属性的,但是它也一样可以达到相同的效果,那么接下来看看它是怎么实现的呢?

在react中实现监听效果有一个比较简单的方法,就是使用useEffect 这个hook,在我们刚接触这个hook时经常会被代入到类组件中的生命周期上,其实它不光有生命周期的功能更是可以实现监听的效果。

1、首先我们先来看下useEffect 用来当作生命周期时是怎么使用的,代码如下:

import React, { useEffect, useState } from 'react' export default function Wtach() { const [num, setNum] = useState(0) useEffect(() => { console.log('我是初始化的hook') return () => { console.log('我是卸载的hook') } }, []) useEffect(() => { console.log('我是更新的hook') }) return ( <div> <p>{`当前数量是${num}`}</p> <button onClick={() => setNum(num + 1)}>增加数量</button> </div> ) }

上述代码中,第一个useEffect 中传入两个参数第一个参数为一个callback回调函数, 第二个参数传入一个空数组,callback回调函数又返回了一个用于卸载组件时调用的函数。

他们的调用时期分别为初始化加载完成页面调用useEffect 传入的第一个函数(此时对应omponentDidMount生命周期钩子),在卸载的时候react会调用第一个callback函数里面返回的函数(此时对应componentWillUnmount生命周期钩子),这样以来他们就完全实现了和生命周期一样的两个功能。

打印如下:

再来看第二个useEffect ,它只传入了一个callback回调参数,这个传入的callback就会在每次页面更新完成后进行调用(它对应了componentDidUpdate生命周期钩子)。

效果如下:

2、上面看完了useEffect 实现生命周期的功能,下面就来看下它是怎么实现类似watch属性的。

代码如下:

import React, { useEffect, useState } from 'react' export default function Wtach() { const [num, setNum] = useState(0) useEffect(() => { console.log('我是改变后的num', num) }, [num]) return ( <div> <p>{`当前数量是${num}`}</p> <button onClick={() => setNum(num + 1)}>增加数量</button> </div> ) }

上述代码useEffect 中除了正常传入第一个callback函数,第二个参数传入一个数组里面的值是num,此时传入的callback函数的调用就依赖了第二个参数数组中num的值,每当num的值改变后react就会去调用传入的第一个callback函数,这样就实现了类似watch属性的功能。

效果如下:

最后如果你想要监听多个值,不需要写多个useEffect ,只需要在第二个数组参数中增加新的值即可,如下:

useEffect使用指南

最近在写一些React的应用,用上了最新的Hooks。Hooks好用,但是对于刚上手Hooks的小伙伴来说,坑也挺多的。所以决定总结一下Hooks的使用经验,从useEffect开始。useEffect用于处理组件中的effect,通常用于请求数据,事件处理,订阅等相关操作。这里以数据请求为例,来深入介绍useEffect的用法。

最基本的使用

首先,举一个简单的例子:

import React, { useState } from 'react';   function App() {   const [data, setData] = useState({ hits: [] });     return (     <ul>       {data.hits.map(item => (         <li key={item.objectID}>           <a href={item.url}>{item.title}</a>         </li>       ))}     </ul>   ); }   export default App;

App组件显示了一个项目列表,状态和状态更新函数来自与useState这个hooks,通过调用useState,来创建App组件的内部状态。初始状态是一个object,其中的hits为一个空数组,目前还没有请求后端的接口。

为了获取后端提供的数据,接下来将使用axios来发起请求,同样也可以使用fetch,这里会使用useEffect来隔离副作用。

React中如何通过useEffect实现类似Vue的watch监视属性功能?

import React, { useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });     useEffect(async () => {     const result = await axios(       'localhost/api/v1/search?query=redux',     );       setData(result.data);   });     return (     <ul>       {data.hits.map(item => (         <li key={item.objectID}>           <a href={item.url}>{item.title}</a>         </li>       ))}     </ul>   ); }   export default App;

在useEffect中,不仅会请求后端的数据,还会通过调用setData来更新本地的状态,这样会触发view的更新。

但是,运行这个程序的时候,会出现无限循环的情况。useEffect在组件mount时执行,但也会在组件更新时执行。因为我们在每次请求数据之后都会设置本地的状态,所以组件会更新,因此useEffect会再次执行,因此出现了无限循环的情况。我们只想在组件mount时请求数据。我们可以传递一个空数组作为useEffect的第二个参数,这样就能避免在组件更新执行useEffect,只会在组件mount时执行。

import React, { useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });     useEffect(async () => {     const result = await axios(       'localhost/api/v1/search?query=redux',     );       setData(result.data);   }, []);     return (     <ul>       {data.hits.map(item => (         <li key={item.objectID}>           <a href={item.url}>{item.title}</a>         </li>       ))}     </ul>   ); }   export default App;

useEffect的第二个参数可用于定义其依赖的所有变量。如果其中一个变量发生变化,则useEffect会再次运行。如果包含变量的数组为空,则在更新组件时useEffect不会再执行,因为它不会监听任何变量的变更。

还有最后一个问题。在代码中,我们使用async / await从第三方API获取数据。如果你对async/await熟悉的话,你会知道,每个async函数都会默认返回一个隐式的promise。但是,useEffect不应该返回任何内容。这就是为什么会在控制台日志中看到以下警告:

Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect

这就是为什么不能直接在useEffect中使用async函数,因此,我们可以不直接调用async函数,而是像下面这样:

import React, { useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });     useEffect(() => {     const fetchData = async () => {       const result = await axios(         'localhost/api/v1/search?query=redux',       );         setData(result.data);     };       fetchData();   }, []);     return (     <ul>       {data.hits.map(item => (         <li key={item.objectID}>           <a href={item.url}>{item.title}</a>         </li>       ))}     </ul>   ); }   export default App;

响应更新

上面的例子中,我们实现了再组件mount时请求数据。但是很多情况下,我们需要响应用户的输入,然后再请求。这个时候我们会引入一个input框,监听query值的变化:

import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         'localhost/api/v1/search?query=redux',       );         setData(result.data);     };       fetchData();   }, []);     return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <ul>         {data.hits.map(item => (           <li key={item.objectID}>             <a href={item.url}>{item.title}</a>           </li>         ))}       </ul>     </Fragment>   ); }   export default App;

有个query值,已经更新query的逻辑,还需要将这个query值传递给后台,这个操作会在useEffect中进行:

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         `localhost/api/v1/search?query=${query}`,       );         setData(result.data);     };       fetchData();   }, []);     return (     ...   ); }   export default App;

前面我们说了,目前的useEffect只会在组件mount时执行,并且useEffect的第二个参数是依赖的变量,一旦这个依赖的变量变动,useEffect就会重新执行,所以我们需要添加query为useEffect的依赖:

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         `localhost/api/v1/search?query=${query}`,       );         setData(result.data);     };       fetchData();   }, [query]);     return (     ...   ); }   export default App;

一旦更改了query值,就可以重新获取数据。但这会带来另一个问题:query的任何一次变动都会请求后端,这样会带来比较大的访问压力。这个时候我们需要引入一个按钮,点击这个按钮再发起请求

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [search, setSearch] = useState('');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         `localhost/api/v1/search?query=${query}`,       );         setData(result.data);     };       fetchData();   }, [query]);     return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <button type="button" onClick={() => setSearch(query)}>         Search       </button>         <ul>         {data.hits.map(item => (           <li key={item.objectID}>             <a href={item.url}>{item.title}</a>           </li>         ))}       </ul>     </Fragment>   ); }

可以看到上面我们添加了一个新的按钮,然后创建新的组件state:search。每次点击按钮时,会把search的值设置为query,这个时候我们需要修改useEffect中的依赖项为search,这样每次点击按钮,search值变更,useEffect就会重新执行,避免不必要的变更:

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [search, setSearch] = useState('redux');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         `localhost/api/v1/search?query=${search}`,       );         setData(result.data);     };       fetchData();   }, [search]);     return (     ...   ); }   export default App;

此外,search state的初始状态设置为与query state 相同的状态,因为组件首先会在mount时获取数据。所以简单点,直接将的要请求的后端URL设置为search state的初始值。

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [url, setUrl] = useState(     'localhost/api/v1/search?query=redux',   );     useEffect(() => {     const fetchData = async () => {       const result = await axios(url);         setData(result.data);     };       fetchData();   }, [url]);     return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <button         type="button"         onClick={() =>           setUrl(`localhost/api/v1/search?query=${query}`)         }       >         Search       </button>     <ul>         {data.hits.map(item => (           <li key={item.objectID}>             <a href={item.url}>{item.title}</a>           </li>         ))}       </ul>     </Fragment>   ); }

如何处理Loading和Error

良好的用户体验是需要在请求后端数据,数据还没有返回时展现loading的状态,因此,我们还需要添加一个loading的state

import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [url, setUrl] = useState(     'hn.algolia.com/api/v1/search?query=redux',   );   const [isLoading, setIsLoading] = useState(false);     useEffect(() => {     const fetchData = async () => {       setIsLoading(true);         const result = await axios(url);         setData(result.data);       setIsLoading(false);     };       fetchData();   }, [url]);   return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <button         type="button"         onClick={() =>           setUrl(`localhost/api/v1/search?query=${query}`)         }       >         Search       </button>         {isLoading ? (         <div>Loading ...</div>       ) : (         <ul>           {data.hits.map(item => (             <li key={item.objectID}>               <a href={item.url}>{item.title}</a>             </li>           ))}         </ul>       )}     </Fragment>   ); }   export default App;

在useEffect中,请求数据前将loading置为true,在请求完成后,将loading置为false。我们可以看到useEffect的依赖数据中并没有添加loading,这是因为,我们不需要再loading变更时重新调用useEffect。请记住:只有某个变量更新后,需要重新执行useEffect的情况,才需要将该变量添加到useEffect的依赖数组中。

loading处理完成后,还需要处理错误,这里的逻辑是一样的,使用useState来创建一个新的state,然后在useEffect中特定的位置来更新这个state。由于我们使用了async/await,可以使用一个大大的try-catch:

import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [url, setUrl] = useState(     'localhost/api/v1/search?query=redux',   );   const [isLoading, setIsLoading] = useState(false);   const [isError, setIsError] = useState(false);     useEffect(() => {     const fetchData = async () => {       setIsError(false);       setIsLoading(true);         try {         const result = await axios(url);           setData(result.data);       } catch (error) {         setIsError(true);       }         setIsLoading(false);     };       fetchData();   }, [url]);   return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <button         type="button"         onClick={() =>           setUrl(`localhost/api/v1/search?query=${query}`)         }       >         Search       </button>         {isError && <div>Something went wrong ...</div>}         {isLoading ? (         <div>Loading ...</div>       ) : (         <ul>           {data.hits.map(item => (             <li key={item.objectID}>               <a href={item.url}>{item.title}</a>             </li>           ))}         </ul>       )}     </Fragment>   ); }   export default App;

每次useEffect执行时,将会重置error;在出现错误的时候,将error置为true;在正常请求完成后,将error置为false。

处理表单

通常,我们不仅会用到上面的输入框和按钮,更多的时候是一张表单,所以也可以在表单中使用useEffect来处理数据请求,逻辑是相同的:

function App() {   ...     return (     <Fragment>       <form         onSubmit={() =>           setUrl(`localhost/api/v1/search?query=${query}`)         }       >         <input           type="text"           value={query}           onChange={event => setQuery(event.target.value)}         />         <button type="submit">Search</button>       </form>         {isError && <div>Something went wrong ...</div>}         ...     </Fragment>   ); }

上面的例子中,提交表单的时候,会触发页面刷新;就像通常的做法那样,还需要阻止默认事件,来阻止页面的刷新。

function App() {   ...     const doFetch = () => {     setUrl(`localhost/api/v1/search?query=${query}`);   };     return (     <Fragment>       <form onSubmit={event => {         doFetch();           event.preventDefault();       }}>         <input           type="text"           value={query}           onChange={event => setQuery(event.target.value)}         />         <button type="submit">Search</button>       </form>         {isError && <div>Something went wrong ...</div>}         ...     </Fragment>   ); }

自定义hooks

我们可以看到上面的组件,添加了一系列hooks和逻辑之后,已经变得非常的庞大。而hooks的一个非常的优势,就是能够很方便的提取自定义的hooks。这个时候,我们就能把上面的一大堆逻辑抽取到一个单独的hooks中,方便复用和解耦:

function useFetchApi = () => {   const [data, setData] = useState({ hits: [] });   const [url, setUrl] = useState(     'localhost/api/v1/search?query=redux',   );   const [isLoading, setIsLoading] = useState(false);   const [isError, setIsError] = useState(false);     useEffect(() => {     const fetchData = async () => {       setIsError(false);       setIsLoading(true);         try {         const result = await axios(url);           setData(result.data);       } catch (error) {         setIsError(true);       }         setIsLoading(false);     };       fetchData();   }, [url]);     const doFetch = () => {     setUrl(`localhost/api/v1/search?query=${query}`);   };     return { data, isLoading, isError, doFetch }; }

在自定义的hooks抽离完成后,引入到组件中

function App() {   const [query, setQuery] = useState('redux');   const { data, isLoading, isError, doFetch } = useHackerNewsApi();     return (     <Fragment>       ...     </Fragment>   ); }

然后我们需要在form组件中设定初始的后端URL

const useHackerNewsApi = () => {   ...     useEffect(     ...   );     const doFetch = url => {     setUrl(url);   };     return { data, isLoading, isError, doFetch }; };   function App() {   const [query, setQuery] = useState('redux');   const { data, isLoading, isError, doFetch } = useHackerNewsApi();     return (     <Fragment>       <form         onSubmit={event => {           doFetch(             `localhost/api/v1/search?query=${query}`,           );             event.preventDefault();         }}       >         <input           type="text"           value={query}           onChange={event => setQuery(event.target.value)}         />         <button type="submit">Search</button>       </form>         ...     </Fragment>   ); }

使用useReducer整合逻辑

到目前为止,我们已经使用了各种state hooks来管理数据,包括loading、error、data等状态。但是我们可以看到,这三个有关联的状态确是分散的,它们通过分离的useState来创建,为了有关联的状态整合到一起,我们需要用到useReducer。

如果你写过redux,那么将会对useReducer非常的熟悉,可以把它理解为一个轻量额redux。useReducer 返回一个状态对象和一个可以改变状态对象的dispatch函数。跟redux类似的,dispatch函数接受action作为参数,action包含type和payload属性。我们看一个简单的例子吧:

import React, {   Fragment,   useState,   useEffect,   useReducer, } from 'react'; import axios from 'axios';   const dataFetchReducer = (state, action) => {   ... };   const useDataApi = (initialUrl, initialData) => {   const [url, setUrl] = useState(initialUrl);     const [state, dispatch] = useReducer(dataFetchReducer, {     isLoading: false,     isError: false,     data: initialData,   });     ... };

useReducer将reducer函数和初始状态对象作为参数。在我们的例子中,data,loading和error状态的初始值与useState创建时一致,但它们已经整合到一个由useReducer创建对象,而不是多个useState创建的状态。

const dataFetchReducer = (state, action) => {   ... };   const useDataApi = (initialUrl, initialData) => {   const [url, setUrl] = useState(initialUrl);     const [state, dispatch] = useReducer(dataFetchReducer, {     isLoading: false,     isError: false,     data: initialData,   });     useEffect(() => {     const fetchData = async () => {       dispatch({ type: 'FETCH_INIT' });         try {         const result = await axios(url);           dispatch({ type: 'FETCH_SUCCESS', payload: result.data });       } catch (error) {         dispatch({ type: 'FETCH_FAILURE' });       }     };       fetchData();   }, [url]);     ... };

在获取数据时,可以调用dispatch函数,将信息发送给reducer。使用dispatch函数发送的参数为object,具有type属性和可选payload的属性。type属性告诉reducer需要应用哪个状态转换,并且reducer可以使用payload来创建新的状态。在这里,我们只有三个状态转换:发起请求,请求成功,请求失败。

在自定义hooks的末尾,state像以前一样返回,但是因为我们拿到的是一个状态对象,而不是以前那种分离的状态,所以需要将状态对象解构之后再返回。这样,调用useDataApi自定义hooks的人仍然可以访问data,isLoading 和 isError这三个状态。

const useDataApi = (initialUrl, initialData) => {   const [url, setUrl] = useState(initialUrl);     const [state, dispatch] = useReducer(dataFetchReducer, {     isLoading: false,     isError: false,     data: initialData,   });     ...     const doFetch = url => {     setUrl(url);   };     return { ...state, doFetch }; }; 

接下来添加reducer函数的实现。它需要三种不同的状态转换FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE。每个状态转换都需要返回一个新的状态对象。让我们看看如何使用switch case语句实现它:

  switch (action.type) {     case 'FETCH_INIT':       return {         ...state,         isLoading: true,         isError: false       };     case 'FETCH_SUCCESS':       return {         ...state,         isLoading: false,         isError: false,         data: action.payload,       };     case 'FETCH_FAILURE':       return {         ...state,         isLoading: false,         isError: true,       };     default:       throw new Error();   } };

取消数据请求

React中的一种很常见的问题是:如果在组件中发送一个请求,在请求还没有返回的时候卸载了组件,这个时候还会尝试设置这个状态,会报错。我们需要在hooks中处理这种情况,可以看下是怎样处理的:

const useDataApi = (initialUrl, initialData) => {   const [url, setUrl] = useState(initialUrl);     const [state, dispatch] = useReducer(dataFetchReducer, {     isLoading: false,     isError: false,     data: initialData,   });     useEffect(() => {     let didCancel = false;       const fetchData = async () => {       dispatch({ type: 'FETCH_INIT' });         try {         const result = await axios(url);           if (!didCancel) {           dispatch({ type: 'FETCH_SUCCESS', payload: result.data });         }       } catch (error) {         if (!didCancel) {           dispatch({ type: 'FETCH_FAILURE' });         }       }     };   fetchData();       return () => {       didCancel = true;     };   }, [url]);     const doFetch = url => {     setUrl(url);   };     return { ...state, doFetch }; };

我们可以看到这里新增了一个didCancel变量,如果这个变量为true,不会再发送dispatch,也不会再执行设置状态这个动作。这里我们在useEffe的返回函数中将didCancel置为true,在卸载组件时会自动调用这段逻辑。也就避免了再卸载的组件上设置状态。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持易盾网络。

本文共计4227个文字,预计阅读时间需要17分钟。

React中如何通过useEffect实现类似Vue的watch监视属性功能?

目录 + React的watch监视属性 + useEffect使用指南 + 最基本的用法 + 响应更新 + 如何处理Loading和Error + 处理表格 + 自定义hooks + 使用useReducer整合逻辑 + 取消数据请求 + React的watch监视属性 + useEffect +

目录
  • react的watch监视属性-useEffect
  • useEffect使用指南
    • 最基本的使用
    • 响应更新
    • 如何处理Loading和Error
    • 处理表单
    • 自定义hooks
    • 使用useReducer整合逻辑
    • 取消数据请求

react的watch监视属性-useEffect

在vue中可以使用watch属性,去监视一个值,当这个值进行变化的时候就去执行一些操作。在react是没有这个属性的,但是它也一样可以达到相同的效果,那么接下来看看它是怎么实现的呢?

在react中实现监听效果有一个比较简单的方法,就是使用useEffect 这个hook,在我们刚接触这个hook时经常会被代入到类组件中的生命周期上,其实它不光有生命周期的功能更是可以实现监听的效果。

1、首先我们先来看下useEffect 用来当作生命周期时是怎么使用的,代码如下:

import React, { useEffect, useState } from 'react' export default function Wtach() { const [num, setNum] = useState(0) useEffect(() => { console.log('我是初始化的hook') return () => { console.log('我是卸载的hook') } }, []) useEffect(() => { console.log('我是更新的hook') }) return ( <div> <p>{`当前数量是${num}`}</p> <button onClick={() => setNum(num + 1)}>增加数量</button> </div> ) }

上述代码中,第一个useEffect 中传入两个参数第一个参数为一个callback回调函数, 第二个参数传入一个空数组,callback回调函数又返回了一个用于卸载组件时调用的函数。

他们的调用时期分别为初始化加载完成页面调用useEffect 传入的第一个函数(此时对应omponentDidMount生命周期钩子),在卸载的时候react会调用第一个callback函数里面返回的函数(此时对应componentWillUnmount生命周期钩子),这样以来他们就完全实现了和生命周期一样的两个功能。

打印如下:

再来看第二个useEffect ,它只传入了一个callback回调参数,这个传入的callback就会在每次页面更新完成后进行调用(它对应了componentDidUpdate生命周期钩子)。

效果如下:

2、上面看完了useEffect 实现生命周期的功能,下面就来看下它是怎么实现类似watch属性的。

代码如下:

import React, { useEffect, useState } from 'react' export default function Wtach() { const [num, setNum] = useState(0) useEffect(() => { console.log('我是改变后的num', num) }, [num]) return ( <div> <p>{`当前数量是${num}`}</p> <button onClick={() => setNum(num + 1)}>增加数量</button> </div> ) }

上述代码useEffect 中除了正常传入第一个callback函数,第二个参数传入一个数组里面的值是num,此时传入的callback函数的调用就依赖了第二个参数数组中num的值,每当num的值改变后react就会去调用传入的第一个callback函数,这样就实现了类似watch属性的功能。

效果如下:

最后如果你想要监听多个值,不需要写多个useEffect ,只需要在第二个数组参数中增加新的值即可,如下:

useEffect使用指南

最近在写一些React的应用,用上了最新的Hooks。Hooks好用,但是对于刚上手Hooks的小伙伴来说,坑也挺多的。所以决定总结一下Hooks的使用经验,从useEffect开始。useEffect用于处理组件中的effect,通常用于请求数据,事件处理,订阅等相关操作。这里以数据请求为例,来深入介绍useEffect的用法。

最基本的使用

首先,举一个简单的例子:

import React, { useState } from 'react';   function App() {   const [data, setData] = useState({ hits: [] });     return (     <ul>       {data.hits.map(item => (         <li key={item.objectID}>           <a href={item.url}>{item.title}</a>         </li>       ))}     </ul>   ); }   export default App;

App组件显示了一个项目列表,状态和状态更新函数来自与useState这个hooks,通过调用useState,来创建App组件的内部状态。初始状态是一个object,其中的hits为一个空数组,目前还没有请求后端的接口。

为了获取后端提供的数据,接下来将使用axios来发起请求,同样也可以使用fetch,这里会使用useEffect来隔离副作用。

React中如何通过useEffect实现类似Vue的watch监视属性功能?

import React, { useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });     useEffect(async () => {     const result = await axios(       'localhost/api/v1/search?query=redux',     );       setData(result.data);   });     return (     <ul>       {data.hits.map(item => (         <li key={item.objectID}>           <a href={item.url}>{item.title}</a>         </li>       ))}     </ul>   ); }   export default App;

在useEffect中,不仅会请求后端的数据,还会通过调用setData来更新本地的状态,这样会触发view的更新。

但是,运行这个程序的时候,会出现无限循环的情况。useEffect在组件mount时执行,但也会在组件更新时执行。因为我们在每次请求数据之后都会设置本地的状态,所以组件会更新,因此useEffect会再次执行,因此出现了无限循环的情况。我们只想在组件mount时请求数据。我们可以传递一个空数组作为useEffect的第二个参数,这样就能避免在组件更新执行useEffect,只会在组件mount时执行。

import React, { useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });     useEffect(async () => {     const result = await axios(       'localhost/api/v1/search?query=redux',     );       setData(result.data);   }, []);     return (     <ul>       {data.hits.map(item => (         <li key={item.objectID}>           <a href={item.url}>{item.title}</a>         </li>       ))}     </ul>   ); }   export default App;

useEffect的第二个参数可用于定义其依赖的所有变量。如果其中一个变量发生变化,则useEffect会再次运行。如果包含变量的数组为空,则在更新组件时useEffect不会再执行,因为它不会监听任何变量的变更。

还有最后一个问题。在代码中,我们使用async / await从第三方API获取数据。如果你对async/await熟悉的话,你会知道,每个async函数都会默认返回一个隐式的promise。但是,useEffect不应该返回任何内容。这就是为什么会在控制台日志中看到以下警告:

Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect

这就是为什么不能直接在useEffect中使用async函数,因此,我们可以不直接调用async函数,而是像下面这样:

import React, { useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });     useEffect(() => {     const fetchData = async () => {       const result = await axios(         'localhost/api/v1/search?query=redux',       );         setData(result.data);     };       fetchData();   }, []);     return (     <ul>       {data.hits.map(item => (         <li key={item.objectID}>           <a href={item.url}>{item.title}</a>         </li>       ))}     </ul>   ); }   export default App;

响应更新

上面的例子中,我们实现了再组件mount时请求数据。但是很多情况下,我们需要响应用户的输入,然后再请求。这个时候我们会引入一个input框,监听query值的变化:

import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         'localhost/api/v1/search?query=redux',       );         setData(result.data);     };       fetchData();   }, []);     return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <ul>         {data.hits.map(item => (           <li key={item.objectID}>             <a href={item.url}>{item.title}</a>           </li>         ))}       </ul>     </Fragment>   ); }   export default App;

有个query值,已经更新query的逻辑,还需要将这个query值传递给后台,这个操作会在useEffect中进行:

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         `localhost/api/v1/search?query=${query}`,       );         setData(result.data);     };       fetchData();   }, []);     return (     ...   ); }   export default App;

前面我们说了,目前的useEffect只会在组件mount时执行,并且useEffect的第二个参数是依赖的变量,一旦这个依赖的变量变动,useEffect就会重新执行,所以我们需要添加query为useEffect的依赖:

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         `localhost/api/v1/search?query=${query}`,       );         setData(result.data);     };       fetchData();   }, [query]);     return (     ...   ); }   export default App;

一旦更改了query值,就可以重新获取数据。但这会带来另一个问题:query的任何一次变动都会请求后端,这样会带来比较大的访问压力。这个时候我们需要引入一个按钮,点击这个按钮再发起请求

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [search, setSearch] = useState('');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         `localhost/api/v1/search?query=${query}`,       );         setData(result.data);     };       fetchData();   }, [query]);     return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <button type="button" onClick={() => setSearch(query)}>         Search       </button>         <ul>         {data.hits.map(item => (           <li key={item.objectID}>             <a href={item.url}>{item.title}</a>           </li>         ))}       </ul>     </Fragment>   ); }

可以看到上面我们添加了一个新的按钮,然后创建新的组件state:search。每次点击按钮时,会把search的值设置为query,这个时候我们需要修改useEffect中的依赖项为search,这样每次点击按钮,search值变更,useEffect就会重新执行,避免不必要的变更:

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [search, setSearch] = useState('redux');     useEffect(() => {     const fetchData = async () => {       const result = await axios(         `localhost/api/v1/search?query=${search}`,       );         setData(result.data);     };       fetchData();   }, [search]);     return (     ...   ); }   export default App;

此外,search state的初始状态设置为与query state 相同的状态,因为组件首先会在mount时获取数据。所以简单点,直接将的要请求的后端URL设置为search state的初始值。

function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [url, setUrl] = useState(     'localhost/api/v1/search?query=redux',   );     useEffect(() => {     const fetchData = async () => {       const result = await axios(url);         setData(result.data);     };       fetchData();   }, [url]);     return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <button         type="button"         onClick={() =>           setUrl(`localhost/api/v1/search?query=${query}`)         }       >         Search       </button>     <ul>         {data.hits.map(item => (           <li key={item.objectID}>             <a href={item.url}>{item.title}</a>           </li>         ))}       </ul>     </Fragment>   ); }

如何处理Loading和Error

良好的用户体验是需要在请求后端数据,数据还没有返回时展现loading的状态,因此,我们还需要添加一个loading的state

import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [url, setUrl] = useState(     'hn.algolia.com/api/v1/search?query=redux',   );   const [isLoading, setIsLoading] = useState(false);     useEffect(() => {     const fetchData = async () => {       setIsLoading(true);         const result = await axios(url);         setData(result.data);       setIsLoading(false);     };       fetchData();   }, [url]);   return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <button         type="button"         onClick={() =>           setUrl(`localhost/api/v1/search?query=${query}`)         }       >         Search       </button>         {isLoading ? (         <div>Loading ...</div>       ) : (         <ul>           {data.hits.map(item => (             <li key={item.objectID}>               <a href={item.url}>{item.title}</a>             </li>           ))}         </ul>       )}     </Fragment>   ); }   export default App;

在useEffect中,请求数据前将loading置为true,在请求完成后,将loading置为false。我们可以看到useEffect的依赖数据中并没有添加loading,这是因为,我们不需要再loading变更时重新调用useEffect。请记住:只有某个变量更新后,需要重新执行useEffect的情况,才需要将该变量添加到useEffect的依赖数组中。

loading处理完成后,还需要处理错误,这里的逻辑是一样的,使用useState来创建一个新的state,然后在useEffect中特定的位置来更新这个state。由于我们使用了async/await,可以使用一个大大的try-catch:

import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios';   function App() {   const [data, setData] = useState({ hits: [] });   const [query, setQuery] = useState('redux');   const [url, setUrl] = useState(     'localhost/api/v1/search?query=redux',   );   const [isLoading, setIsLoading] = useState(false);   const [isError, setIsError] = useState(false);     useEffect(() => {     const fetchData = async () => {       setIsError(false);       setIsLoading(true);         try {         const result = await axios(url);           setData(result.data);       } catch (error) {         setIsError(true);       }         setIsLoading(false);     };       fetchData();   }, [url]);   return (     <Fragment>       <input         type="text"         value={query}         onChange={event => setQuery(event.target.value)}       />       <button         type="button"         onClick={() =>           setUrl(`localhost/api/v1/search?query=${query}`)         }       >         Search       </button>         {isError && <div>Something went wrong ...</div>}         {isLoading ? (         <div>Loading ...</div>       ) : (         <ul>           {data.hits.map(item => (             <li key={item.objectID}>               <a href={item.url}>{item.title}</a>             </li>           ))}         </ul>       )}     </Fragment>   ); }   export default App;

每次useEffect执行时,将会重置error;在出现错误的时候,将error置为true;在正常请求完成后,将error置为false。

处理表单

通常,我们不仅会用到上面的输入框和按钮,更多的时候是一张表单,所以也可以在表单中使用useEffect来处理数据请求,逻辑是相同的:

function App() {   ...     return (     <Fragment>       <form         onSubmit={() =>           setUrl(`localhost/api/v1/search?query=${query}`)         }       >         <input           type="text"           value={query}           onChange={event => setQuery(event.target.value)}         />         <button type="submit">Search</button>       </form>         {isError && <div>Something went wrong ...</div>}         ...     </Fragment>   ); }

上面的例子中,提交表单的时候,会触发页面刷新;就像通常的做法那样,还需要阻止默认事件,来阻止页面的刷新。

function App() {   ...     const doFetch = () => {     setUrl(`localhost/api/v1/search?query=${query}`);   };     return (     <Fragment>       <form onSubmit={event => {         doFetch();           event.preventDefault();       }}>         <input           type="text"           value={query}           onChange={event => setQuery(event.target.value)}         />         <button type="submit">Search</button>       </form>         {isError && <div>Something went wrong ...</div>}         ...     </Fragment>   ); }

自定义hooks

我们可以看到上面的组件,添加了一系列hooks和逻辑之后,已经变得非常的庞大。而hooks的一个非常的优势,就是能够很方便的提取自定义的hooks。这个时候,我们就能把上面的一大堆逻辑抽取到一个单独的hooks中,方便复用和解耦:

function useFetchApi = () => {   const [data, setData] = useState({ hits: [] });   const [url, setUrl] = useState(     'localhost/api/v1/search?query=redux',   );   const [isLoading, setIsLoading] = useState(false);   const [isError, setIsError] = useState(false);     useEffect(() => {     const fetchData = async () => {       setIsError(false);       setIsLoading(true);         try {         const result = await axios(url);           setData(result.data);       } catch (error) {         setIsError(true);       }         setIsLoading(false);     };       fetchData();   }, [url]);     const doFetch = () => {     setUrl(`localhost/api/v1/search?query=${query}`);   };     return { data, isLoading, isError, doFetch }; }

在自定义的hooks抽离完成后,引入到组件中

function App() {   const [query, setQuery] = useState('redux');   const { data, isLoading, isError, doFetch } = useHackerNewsApi();     return (     <Fragment>       ...     </Fragment>   ); }

然后我们需要在form组件中设定初始的后端URL

const useHackerNewsApi = () => {   ...     useEffect(     ...   );     const doFetch = url => {     setUrl(url);   };     return { data, isLoading, isError, doFetch }; };   function App() {   const [query, setQuery] = useState('redux');   const { data, isLoading, isError, doFetch } = useHackerNewsApi();     return (     <Fragment>       <form         onSubmit={event => {           doFetch(             `localhost/api/v1/search?query=${query}`,           );             event.preventDefault();         }}       >         <input           type="text"           value={query}           onChange={event => setQuery(event.target.value)}         />         <button type="submit">Search</button>       </form>         ...     </Fragment>   ); }

使用useReducer整合逻辑

到目前为止,我们已经使用了各种state hooks来管理数据,包括loading、error、data等状态。但是我们可以看到,这三个有关联的状态确是分散的,它们通过分离的useState来创建,为了有关联的状态整合到一起,我们需要用到useReducer。

如果你写过redux,那么将会对useReducer非常的熟悉,可以把它理解为一个轻量额redux。useReducer 返回一个状态对象和一个可以改变状态对象的dispatch函数。跟redux类似的,dispatch函数接受action作为参数,action包含type和payload属性。我们看一个简单的例子吧:

import React, {   Fragment,   useState,   useEffect,   useReducer, } from 'react'; import axios from 'axios';   const dataFetchReducer = (state, action) => {   ... };   const useDataApi = (initialUrl, initialData) => {   const [url, setUrl] = useState(initialUrl);     const [state, dispatch] = useReducer(dataFetchReducer, {     isLoading: false,     isError: false,     data: initialData,   });     ... };

useReducer将reducer函数和初始状态对象作为参数。在我们的例子中,data,loading和error状态的初始值与useState创建时一致,但它们已经整合到一个由useReducer创建对象,而不是多个useState创建的状态。

const dataFetchReducer = (state, action) => {   ... };   const useDataApi = (initialUrl, initialData) => {   const [url, setUrl] = useState(initialUrl);     const [state, dispatch] = useReducer(dataFetchReducer, {     isLoading: false,     isError: false,     data: initialData,   });     useEffect(() => {     const fetchData = async () => {       dispatch({ type: 'FETCH_INIT' });         try {         const result = await axios(url);           dispatch({ type: 'FETCH_SUCCESS', payload: result.data });       } catch (error) {         dispatch({ type: 'FETCH_FAILURE' });       }     };       fetchData();   }, [url]);     ... };

在获取数据时,可以调用dispatch函数,将信息发送给reducer。使用dispatch函数发送的参数为object,具有type属性和可选payload的属性。type属性告诉reducer需要应用哪个状态转换,并且reducer可以使用payload来创建新的状态。在这里,我们只有三个状态转换:发起请求,请求成功,请求失败。

在自定义hooks的末尾,state像以前一样返回,但是因为我们拿到的是一个状态对象,而不是以前那种分离的状态,所以需要将状态对象解构之后再返回。这样,调用useDataApi自定义hooks的人仍然可以访问data,isLoading 和 isError这三个状态。

const useDataApi = (initialUrl, initialData) => {   const [url, setUrl] = useState(initialUrl);     const [state, dispatch] = useReducer(dataFetchReducer, {     isLoading: false,     isError: false,     data: initialData,   });     ...     const doFetch = url => {     setUrl(url);   };     return { ...state, doFetch }; }; 

接下来添加reducer函数的实现。它需要三种不同的状态转换FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE。每个状态转换都需要返回一个新的状态对象。让我们看看如何使用switch case语句实现它:

  switch (action.type) {     case 'FETCH_INIT':       return {         ...state,         isLoading: true,         isError: false       };     case 'FETCH_SUCCESS':       return {         ...state,         isLoading: false,         isError: false,         data: action.payload,       };     case 'FETCH_FAILURE':       return {         ...state,         isLoading: false,         isError: true,       };     default:       throw new Error();   } };

取消数据请求

React中的一种很常见的问题是:如果在组件中发送一个请求,在请求还没有返回的时候卸载了组件,这个时候还会尝试设置这个状态,会报错。我们需要在hooks中处理这种情况,可以看下是怎样处理的:

const useDataApi = (initialUrl, initialData) => {   const [url, setUrl] = useState(initialUrl);     const [state, dispatch] = useReducer(dataFetchReducer, {     isLoading: false,     isError: false,     data: initialData,   });     useEffect(() => {     let didCancel = false;       const fetchData = async () => {       dispatch({ type: 'FETCH_INIT' });         try {         const result = await axios(url);           if (!didCancel) {           dispatch({ type: 'FETCH_SUCCESS', payload: result.data });         }       } catch (error) {         if (!didCancel) {           dispatch({ type: 'FETCH_FAILURE' });         }       }     };   fetchData();       return () => {       didCancel = true;     };   }, [url]);     const doFetch = url => {     setUrl(url);   };     return { ...state, doFetch }; };

我们可以看到这里新增了一个didCancel变量,如果这个变量为true,不会再发送dispatch,也不会再执行设置状态这个动作。这里我们在useEffe的返回函数中将didCancel置为true,在卸载组件时会自动调用这段逻辑。也就避免了再卸载的组件上设置状态。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持易盾网络。