react+hooks实现滚动加载之坑

本文探讨了在React应用中实现滚动分页加载数据时遇到的问题,即快速滚动导致请求接口多次调用,loading状态无法正确阻止新请求。分析了由于useEffect的闭包特性导致的state值缓存问题,并提出了两种解决方案:一是通过将loading状态加入useEffect依赖数组;二是使用useRef来跟踪最新的loading状态。这两种方法都能有效防止接口的重复调用,提高组件性能。
摘要由CSDN通过智能技术生成

转载: https://www.jianshu.com/p/f4a6ab8b4bca

本人刚刚入职新公司,以前都是写Vue的,现在新公司技术栈使用的是react。一顿恶补后在实际的项目中还是避免不了踩坑,花大量的时间找原因,debug。。。不知所措的想哭QAQ

公司项目中需要实现一个滚动分页加载数据得效果,按照咱们逻辑应该是这样的:
1.请求前先判断loading是否为true, 为true时return掉阻止请求函数调用,为false时将loading设置为true然后发起请求
2.请求完毕后再将loading设置为false,允许下次再发起请求

import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'
import { LoadingOutlined } from '@ant-design/icons'; 
import {getScrollLoadList} from '../../api/scrollLoad'
function ScrollLoad() {
  const [list, setList] = useState<number[]>([])
  const [pageNum, setPageNum]=useState<number>(1)
  const [loading, setLoading] = useState<boolean>(false)
  const wrapRef = useRef<any>(null)
  useEffect(() => {
    const Dom = wrapRef.current
    Dom.addEventListener('scroll',loadMore)
    return () => {
      Dom.removeEventListener('scroll',loadMore)
    }
    // eslint-disable-next-line
  },[])
  useEffect(() => {
    getList()
    // eslint-disable-next-line
  },[pageNum])
  const getList = () => {
    setLoading(true)  // 设为请求状态
    getScrollLoadList({pageNum}).then((res:any) => {
      const temp = res.result
      const nowList = pageNum === 1 ? temp : [...list,...temp]
      setList(nowList)
    }).finally(() => {
      setLoading(false)  // 请求完毕置为false
    })
  }
  const loadMore = (e:any) => {
    const {offsetHeight, scrollTop, scrollHeight} = e.target
    if(offsetHeight + scrollTop === scrollHeight) {
      if(loading) return    // 判断是否在请求状态
      setPageNum((pageNum)=> pageNum + 1)
    }
  }
  return (
    <div ref={wrapRef} className={styles.scroll_wrap}>
      {
        list && list.length && list.map(item => (
          <div key={item} className={styles.wrap_item}>{item}</div>
        ))
      }
      {loading && <div className={styles.loading}><LoadingOutlined /></div> }
    </div>
  )
}
export default ScrollLoad
咋一看好像代码没啥问题啊,但实际上问题大的去了。当连续快速的滚动时,这货始终能调用接口。loadingMore函数里的if(loading) return 并没有产生什么卵用,并且打印出来始终为我们的初始值false,百思不得其解!!!
那么为什么出现这种问题呢?经过一番研究。是因为useEffect(() => {},[])这个副作用相当于class组件内的生命周期componentDidMount,在组件渲染中只执行一次。

重点来了

当上面代码useEffect(() => {},[])执行时会产生闭包,里面用到的state变量会进行缓存,只要这个闭包不被释放,里面的state变量就不会是最新的值。即loading始终为初始状态下的值false。
那么怎么解决这个问题呢?

方案1: 可以将pageNum这个参数传进去即useEffect(() =>
{},[loading]),当loading改变后,会销毁之前的闭包,产生新的闭包,这样就能保证里面使用的state变量是最新的,不过这种方法每次都得重新获取dom元素,设置监听和移除监听事件,比较耗性能。(useEffect(()
=> {})也可以,但是不传只要有状态变化就会销毁和新建相对来说更耗性能) 改动代码如下:

useEffect(() => {
    const Dom = wrapRef.current
    Dom.addEventListener('scroll',loadMore)
    return () => {
      Dom.removeEventListener('scroll',loadMore)
    }
    // eslint-disable-next-line
  },[loading])         // 传入loading,监听loading变化

方案2 通过设置一个局部变量
可以在函数组件外定义一个变量或者函数内使用useRef()创建一个变量(这里简称loadingRef),然后将state值loading赋值给这个变量,当loading改变时,会触发loadingRef的改变,这样就会保证loadingRef是最新的值,然后通过loadingRef活loadingRef.current
去判断即可

import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'
import { LoadingOutlined } from '@ant-design/icons'; 
import {getScrollLoadList} from '../../api/scrollLoad'
// let loadingRef = false
function ScrollLoad() {
  const [list, setList] = useState<number[]>([])
  const [pageNum, setPageNum]=useState<number>(1)
  const [loading, setLoading] = useState<boolean>(false)
  const wrapRef = useRef<any>(null)
  const loadingRef = useRef<boolean>()
  loadingRef.current = loading
  // loadingRef = loading
  useEffect(() => {
    const Dom = wrapRef.current
      Dom.addEventListener('scroll',loadMore)
      console.log(Dom,loading)
    return () => {
      console.log('清空监听事件')
      Dom.removeEventListener('scroll',loadMore)
    }
    // eslint-disable-next-line
  },[])
  useEffect(() => {
    getList()
    // eslint-disable-next-line
  },[pageNum])
  const getList = () => {
    setLoading(true)
    getScrollLoadList({pageNum}).then((res:any) => {
      const temp = res.result
      const nowList = pageNum === 1 ? temp : [...list,...temp]
      setList(nowList)
    }).finally(() => {
      setLoading(false)
    })
  }
  const loadMore = (e:any) => {
    const {offsetHeight, scrollTop, scrollHeight} = e.target
    if(offsetHeight + scrollTop === scrollHeight) {
      console.log(loadingRef, '下拉加载之前')
      // if(loadingRef) return
      if(loadingRef.current) return
      setPageNum((pageNum)=> pageNum + 1)
    }
  }
  return (
    <div ref={wrapRef} className={styles.scroll_wrap}>
      {
        list && list.length && list.map(item => (
          <div key={item} className={styles.wrap_item}>{item}</div>
        ))
      }
      {loading && <div className={styles.loading}><LoadingOutlined /></div> }
    </div>
  )
}

export default ScrollLoad
React Hooks可以方便地实现图片懒加载。你可以使用Effect Hook来监听滚动事件,并根据滚动位置判断是否加载图片。 首先,你可以使用useState Hook来定义一个状态变量,用于保存当前是否需要加载图片的状态。初始化时,可以设置为false,表示初始状态下不需要加载图片。 然后,你可以使用useEffect Hook来监听滚动事件。在useEffect的回调函数中,可以使用window对象的scroll事件来监听页面滚动。当滚动事件触发时,可以获取当前的滚动位置,并判断是否达到了加载图片的条件。 如果达到了加载图片的条件,可以更新状态变量为true,表示需要加载图片。这样,在组件渲染时,可以根据状态变量的值来判断是否渲染图片。 下面是一个使用React Hooks实现图片懒加载的例子: ``` import React, { useState, useEffect } from 'react'; const LazyImage = ({ src, alt }) => { const [shouldLoad, setShouldLoad = useState(false); useEffect(() => { const handleScroll = () => { const { top } = document.documentElement.getBoundingClientRect(); if (top < window.innerHeight * 2) { setShouldLoad(true); } }; window.addEventListener('scroll', handleScroll); return () => { window.removeEventListener('scroll', handleScroll); }; }, []); return shouldLoad ? <img src={src} alt={alt} /> : null; }; export default LazyImage; ``` 在这个例子中,LazyImage组件接收一个src属性和一个alt属性,分别表示图片的路径和替代文本。 在组件内部,使用useState Hook定义一个shouldLoad状态变量,并初始化为false。 然后,使用useEffect Hook来监听scroll事件,并在滚动事件触发时更新shouldLoad状态变量。 最后,在组件的返回值中,根据shouldLoad的值来决定是否渲染img标签。 这样,当LazyImage组件渲染到可见区域时,图片才会被加载显示。这就实现了图片的懒加载效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值