useEffect 和异步任务搭配使用的时候会遇到的一些坑总结。

三个常见的问题:

1、如何在组件加载的时候发起异步任务

2、如何在组件交互的时候发起异步任务

3、其他陷阱

一、react hooks发异步请求

1、使用useEffect发起异步任务,第二个参数使用空数组可以实现组件加载的时候执行方法体,返回值函数在组件卸载时执行一次,用来清理一些东西。

2、使用 AbortController 或者某些库自带的信号量 ( axios.CancelToken) 来控制中止请求,更加优雅地退出

3、当需要在其他地方(例如点击处理函数中)设定定时器,在useEffect返回值中清理时,使用局部变量或者useRef来记录这个timer,不要使用useState.

4、组件中出现setTimeout等闭包时,尽量在闭包内部引用ref而不是state,否则容易出现读取到旧的值

5、useState返回的更新状态方法是异步的,要在下次重绘的时候才能获取新的值。不要试图在更改状态之后立马获取状态。

二、如何在组件加载的时候发起异步任务

这类需求非常常见,典型的例子是在列表组件加载的时候发送请求到后端,获取列表展现。

import React, { useState, useEffect } from 'react';
  const SOME_API = '/api/get/value';
  export const MyComponent: React.FC<{}> = => {
  const [loading, setLoading] = useState(true);
  const [value, setValue] = useState(0);
  useEffect( => {
    (async => {
      const res = await fetch(SOME_API);
      const data = await res.json;
      setValue(data.value);
      setLoading(false);
    });
  }, []);
  return (
    <>
      {
        loading ? (
        <h2>Loading...</h2>
        ) : (
        <h2>value is {value}</h2>
        )
      }
    </>
  );
} 

如上是一个基础的带 Loading 功能的组件,会发送异步请求到后端获取一个值并显示到页面上。如果以示例的标准来说已经足够,但要实际运用到项目中,还不得不考虑几个问题

三、如果在响应回来之前组件被销毁了会怎样?

这时候React会报一个⚠️警告信息:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subions and asynchronous tasks in a useEffect cleanup http://function.in Notification 

就是说一个组件卸载之后不应该再修改他的状态。虽然不影响运行,但是这种问题不应该存在。那么如何解决?

问题的核心在于,在组件卸载后依然调用了 setValue(data.value)和 setLoading(false)来更改状态。因此一个简单的办法是标记一下组件有没有被卸载,可以利用 useEffect的返回值。

// 省略组件其他内容,只列出 diff
useEffect( => {
  let isUnmounted = false;
  (async => {
    const res = await fetch(SOME_API);
    const data = await res.json;
    if (!isUnmounted) {
      setValue(data.value);
      setLoading(false);
    }
  });
  return => {
  isUnmounted = true;
  }
}, []); 

这样可以顺利避免这个 Warning。

有没有更加优雅的解法?

上述做法是在收到响应时进行判断,即无论如何需要等响应完成,略显被动。一个更加主动的方式是探知到卸载时直接中断请求,自然也不必再等待响应了。这种主动方案需要用到 AbortController。

AbortController 是一个浏览器的实验接口,它可以返回一个信号量(singal) ,从而中止发送的请求。这个接口的兼容性不错,除了 IE 之外全都兼容(如 Chrome, Edge, FF 和绝大部分移动浏览器,包括 Safari)。

useEffect( => {
  let isUnmounted = false;
  const abortController = new AbortController; // 创建
  (async => {
    const res = await fetch(SOME_API, {
    singal: abortController.singal, // 当做信号量传入
    });
    const data = await res.json;
    if (!isUnmounted) {
      setValue(data.value);
      setLoading(false);
    }
  });
  return => {
    isUnmounted = true;
    abortController.abort; // 在组件卸载时中断
  }
}, []); 

singal 的实现依赖于实际发送请求使用的方法,如上述例子的 fetch方法接受 singal属性。如果使用的是 axios,它的内部已经包含了 axios.CancelToken,可以直接使用,例子在这里。

import React, { Component } from 'react';
import axios from 'axios';
​
class Example extends Component {
  signal = axios.CancelToken.source();
​
  state = {
    isLoading: false,
    user: {},
  }
  
  componentDidMount() {
    this.onLoadUser();
  }
  
  componentWillUnmount() {
    this.signal.cancel('Api is being canceled');
  }
  
  onLoadUser = async () => {
    try {
      this.setState({ isLoading: true });
      const response = await axios.get('https://randomuser.me/api/', {
        cancelToken: this.signal.token,
      })
      this.setState({ user: response.data, isLoading: true });
    } catch (err) {
      if (axios.isCancel(err)) {
        console.log('Error: ', err.message); // => prints: Api is being canceled
      } else {
        this.setState({ isLoading: false });
      }
    }
   } 
    render() {
      return (
        <div>
          <pre>{JSON.stringify(this.state.user, null, 2)}</pre>
        </div>
      )
    }
 
} 

四、如何在组件交互时发起异步任务

另一种常见的需求是要在组件交互(比如点击某个按钮)时发送请求或者开启计时器,待收到响应后修改数据进而影响页面。这里和上面一节(组件加载时)最大的差异在于 React Hooks 只能在组件级别编写,不能在方法(dealClick)或者控制逻辑(if, for 等)内部编写,所以不能在点击的响应函数中再去调用 useEffect。但我们依然要利用 useEffect 的返回函数来做清理工作。

以计时器为例,假设我们想做一个组件,点击按钮后开启一个计时器(5s),计时器结束后修改状态。但如果在计时未到就销毁组件时,我们想停止这个计时器,避免内存泄露。用代码实现的话,会发现开启计时器和清理计时器会在不同的地方,因此就必须记录这个 timer。看如下的例子:

import React, { useState, useEffect } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [value, setValue] = useState(0);
 
    let timer: number;
 
    useEffect(() => {
        // timer 需要在点击时建立,因此这里只做清理使用
        return () => {
            console.log('in useEffect return', timer); // <- 正确的值
            window.clearTimeout(timer);
        }
    }, []);
 
    function dealClick() {
        timer = window.setTimeout(() => {
            setValue(100);
        }, 5000);
    }
 
    return (
        <>
            Value is {value}
            <button onClick={dealClick}>Click Me!</button>
        </>
    );
} 

既然要记录 timer,自然是用一个内部变量来存储即可(暂不考虑连续点击按钮导致多个 timer 出现,假设只点一次。因为实际情况下点了按钮还会触发其他状态变化,继而界面变化,也就点不到了)。

这里需要注意的是,如果把 timer 升级为状态(state) ,则代码反而会出现问题。考虑如下代码:

import React, { useState, useEffect } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [value, setValue] = useState(0);
    const [timer, setTimer] = useState(0); // 把 timer 升级为状态
 
    useEffect(() => {
        // timer 需要在点击时建立,因此这里只做清理使用
        return () => {
            console.log('in useEffect return', timer); // <- 0
            window.clearTimeout(timer);
        }
    }, []);
 
    function dealClick() {
        let tmp = window.setTimeout(() => {
            setValue(100);
        }, 5000);
        setTimer(tmp);
    }
 
    return (
        <>
             Value is {value}
            <button onClick={dealClick}>Click Me!</button>
        </>
    );
} 

有关语义上 timer 到底算不算作组件的状态我们先抛开不谈,仅就代码层面来看。利用 useState 来记住 timer 状态,利用 setTimer 去更改状态,看似合理。但实际运行下来,在 useEffect 返回的清理函数中,得到的 timer 却是初始值,即 0。

为什么两种写法会有差异呢?

其核心在于写入的变量和读取的变量是否是同一个变量。

第一种写法: 代码是把 timer 作为组件内的局部变量使用。在初次渲染组件时,useEffect 返回的闭包函数中指向了这个局部变量 timer。在 dealClick 中设置计时器时返回值依旧写给了这个局部变量(即读和写都是同一个变量),因此在后续卸载时,虽然组件重新运行导致出现一个_新的_局部变量 timer,但这不影响闭包内_老的_ timer,所以结果是正确的。

第二种写法: timer 是一个 useState 的返回值,并不是一个简单的变量。从 React Hooks 的源码来看,它返回的是 [hook.memorizedState, dispatch],对应我们接的值和变更方法。当调用 setTimer 和 setValue 时,分别触发两次重绘,使得 hook.memorizedState 指向了 newState(注意:不是修改,而是重新指向)。但 useEffect 返回闭包中的 timer 依然指向旧的状态,从而得不到新的值。(即读的是旧值,但写的是新值,不是同一个)

如果觉得阅读 Hooks 源码有困难,可以从另一个角度去理解:虽然 React 在 16.8 推出了 Hooks,但实际上只是加强了函数式组件的写法,使之拥有状态,用来作为类组件的一种替代,但 React 状态的内部机制没有变化。在 React 中 setState 内部是通过 merge 操作将新状态和老状态合并后,重新返回一个_新的_状态对象。不论 Hooks 写法如何,这条原理没有变化。现在闭包内指向了旧的状态对象,而 setTimer 和 setValue 重新生成并指向了新的状态对象,并不影响闭包,导致了闭包读不到新的状态。

我们注意到 React 还提供给我们一个 useRef, 它的定义是:

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

ref 对象可以确保在整个生命周期中值不变,且同步更新,是因为 ref 的返回值始终只有一个实例,所有读写都指向它自己。所以也可以用来解决这里的问题。

import React, { useState, useEffect, useRef } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [value, setValue] = useState(0);
    const timer = useRef(0);
 
    useEffect(() => {
        // timer 需要在点击时建立,因此这里只做清理使用
        return () => {
            window.clearTimeout(timer.current);
        }
    }, []);
 
    function dealClick() {
        timer.current = window.setTimeout(() => {
            setValue(100);
        }, 5000);
    }
 
    return (
        <>
              Value is {value}
            <button onClick={dealClick}>Click Me!</button>
        </>
    );
} 

事实上我们后面会看到,useRef 和异步任务配合更加安全稳妥。

五、其他陷阱

修改状态是异步的

//错误例子
import React, { useState } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [value, setValue] = useState(0);
 
    function dealClick() {
        setValue(100);
        console.log(value); // <- 0
    }
 
    return (
             Value is {value}, AnotherValue is {anotherValue}
    );
} 

useState 返回的修改函数是异步的,调用后并不会直接生效,因此立马读取 value 获取到的是旧值(0)。

React 这样设计的目的是为了性能考虑,争取把所有状态改变后只重绘一次就能解决更新问题,而不是改一次重绘一次,也是很容易理解的。

在 timeout 中读不到其他状态的新值

//错误例子
import React, { useState, useEffect } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [value, setValue] = useState(0);
    const [anotherValue, setAnotherValue] = useState(0);
 
    useEffect(() => {
        window.setTimeout(() => {
            console.log('setAnotherValue', value) // <- 0
            setAnotherValue(value);
        }, 1000);
        setValue(100);
    }, []);
 
    return (
             Value is {value}, AnotherValue is {anotherValue}
    );
} 

这个问题和上面使用 useState 去记录 timer 类似,在生成 timeout 闭包时,value 的值是 0。虽然之后通过 setValue 修改了状态,但 React 内部已经指向了新的变量,而旧的变量仍被闭包引用,所以闭包拿到的依然是旧的初始值,也就是 0。

要修正这个问题,也依然是使用 useRef,如下:

import React, { useState, useEffect, useRef } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [value, setValue] = useState(0);
    const [anotherValue, setAnotherValue] = useState(0);
    const valueRef = useRef(value);
    valueRef.current = value;
 
    useEffect(() => {
        window.setTimeout(() => {
            console.log('setAnotherValue', valueRef.current) // <- 100
            setAnotherValue(valueRef.current);
        }, 1000);
        setValue(100);
    }, []);
 
    return (
             Value is {value}, AnotherValue is {anotherValue}
    );
} 

还是 timeout 的问题

假设我们要实现一个按钮,默认显示 false。当点击后更改为 true,但两秒后变回 false( true 和 false 可以互换)。考虑如下代码:

import React, { useState } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [flag, setFlag] = useState(false);
 
    function dealClick() {
        setFlag(!flag);
 
        setTimeout(() => {
            setFlag(!flag);
        }, 2000);
    }
 
    return (
        <button onClick={dealClick}>{flag ? "true" : "false"}</button>
    );
} 

我们会发现点击时能够正常切换,但是两秒后并不会变回来。究其原因,依然在于 useState 的更新是重新指向新值,但 timeout 的闭包依然指向了旧值。所以在例子中,flag 一直是 false,虽然后续 setFlag(!flag),但依然没有影响到 timeout 里面的 flag。

解决方法有二。

第一个还是利用 useRef

import React, { useState, useRef } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [flag, setFlag] = useState(false);
    const flagRef = useRef(flag);
    flagRef.current = flag;
 
    function dealClick() {
        setFlag(!flagRef.current);
 
        setTimeout(() => {
            setFlag(!flagRef.current);
        }, 2000);
    }
 
    return (
        <button onClick={dealClick}>{flag ? "true" : "false"}</button>
    );
} 

第二个是利用 setFlag 可以接收函数作为参数,并利用闭包和参数来实现(函数式更新)

import React, { useState } from 'react';
 
export const MyComponent: React.FC<{}> = () => {
    const [flag, setFlag] = useState(false);
 
    function dealClick() {
        setFlag(!flag);
 
        setTimeout(() => {
            setFlag(flag => !flag);
        }, 2000);
    }
 
    return (
        <button onClick={dealClick}>{flag ? "true" : "false"}</button>
    );
} 

当 setFlag 参数为函数类型时,这个函数的意义是告诉 React 如何从当前状态产生出新的状态(类似于 redux 的 reducer,不过是只针对一个状态的子 reducer)。既然是当前状态,因此返回值取反,就能够实现效果。

总结

在 Hook 中出现异步任务尤其是 timeout 的时候,我们要格外注意。useState 只能保证多次重绘之间的状态值是一样的,但不保证它们就是同一个对象,因此出现闭包引用的时候,尽量使用 useRef 而不是直接使用 state 本身,否则就容易踩坑。反之如果的确碰到了设置了新值但读取到旧值的情况,也可以往这个方向想想,可能就是这个原因所致。

到此这篇关于React useEffect异步操作常见问题小结的文章就介绍到这了,更多相关React useEffect异步 内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

React useEffect异步操作常见问题小结的更多相关文章

  1. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  2. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  3. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  4. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  5. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  6. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  7. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

  8. 反应原生 – 如何通过Xcode构建React Native iOS应用程序到设备?

    我试图将AwesomeProject应用程序构建到设备上.构建成功并启动屏幕显示,但后来我看到一个红色的“无法连接到开发服务器”屏幕.它表示“确保节点服务器正在运行–从Reactroot运行”npmstart“.看起来节点服务器已经运行,因为当我做npm启动时,我收到一个EADDRINUSE消息,表示该端口已经在使用.解决方法从设备访问开发服务器您可以使用开发服务器快速迭代设备.要做到这一点,你的

  9. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

  10. 如何为iOS的React Native设置分析

    所以我已经完成了一个针对iOS的ReactNative项目,但是我想在其中分析.我尝试了react-native-google-analytics软件包,但是问题阻止了它的正常工作.此外,react-native-cordova-plugin软件包只适用于Android,因此插入Cordova插件进行分析的能力现在已成为问题.我也没有Swift/ObjectiveC的经验,所以将完全失去GA的插入.有没有人有任何建议如何连接GoogleAnalytics的ReactNativeforiOS?

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部