Coder Social home page Coder Social logo

blog's People

Contributors

andyjxli avatar

Stargazers

 avatar  avatar

Watchers

 avatar

blog's Issues

React Hooks 之 useFetch

前言

自 React Hooks 16.8.0 后带来了 React hooks 这一特性。这一特性在没有破坏性的更新下为我们带来了更加舒爽的开发方式。过去我们常常因providers,consumers,高阶组件,render props 等形成“嵌套地狱”。尽管 Class Component 在某种程度上为我们提供了更方便的写法以及生命周期,但同时也带来了一些不好的地方。例如难以理解的 class 内部原理、难以测试的声明周期。而 React Hooks 为我们提供了一种 Function Component 的写法,让我们用更少的代码写出更加优雅、易懂的代码。本文不做 React Hooks API的讲述,如有不懂,请移步 Hooks 简介

发送服务端请求所面临的问题

1. try / catch问题

在开发代码时,我们发送后端请求后接受到的数据,需要使用try/catch来捕获错误。而每次捕获出的错误可能需要打印出来以检测bug。这样我们每次都会写同样的代码,这样在开发过程中很不友好。同时有些同学不习惯使用 try/catch 来捕获错误,这就可能造成不可预计的问题。

import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'

type ParamsType = PageType & TimeNumberType

const reducer = (state: ParamsType, action: Actions) => {
  const { payload } = action
  return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
  pageSize: 10,
  pageNumber: 1,
  startTime: 0,
  endTime: 0
}

const ListComponent = () => {
  const [params, dispatch] = useReducer(reducer, initialState)
  const getList = async () => {
    // try catch
    try {
      const res = await postListData(params)
      console.log(res)
    } catch (err) {
      console.error(err)
    }
  }

  useEffect(() => {
    getList()
  }, [params])
}

demo中展示了在业务场景中发送请求的场景,当发送请求多了之后我们会每次手动try / catch,虽然不是大问题,但是重复代码写多了会觉得难受...。下面看第二个功能。

2. 请求状态

在实际的业务场景中,我们向后端发送请求时,往往伴随着用户点击多次,但是只能发送一次请求的问题,这时我们需要手动加锁。并且在很多场景中我们需要知道请求状态来为页面设置loading。例如:

import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'
import { DateRangePicker, Table } from 'UI'

type ParamsType = PageType & TimeNumberType

const TIME = Symbol('time')
const PAGE = Symbol('page')
const reducer = (state: ParamsType, action: Actions) => {
  const { payload } = action
  return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
  pageSize: 10,
  pageNumber: 1,
  startTime: 0,
  endTime: 0
}

const ListComponent = () => {
  const [params, dispatch] = useReducer(reducer, initialState)
  const [loading, setLoading] = useState(false)
  const [list, setList] = useState({})
  
  const getList = async () => {
    // loading is true
    if (loading) return
    // set loading status
    setLoading(true)
    // try catch
    try {
      const res = await postListData(params)
      setList(res)
      setLoading(false)
    } catch (err) {
      console.error(err)
      setLoading(false)
    }
  }

  useEffect(() => {
    getList()
  }, [params])
  
  return (
      <div style={{ marginBottom: '20px' }}>
        <DateRangePicker
          onChange={handleDateChange}
        />
      <Table
        onPageChange={(pageNumber: number) => {
          dispatch({ payload: { pageNumber }, type: PAGE })
        }}
        list={list}
        // 数据是否正在加载,以此来判断是否需要展示loading
        loading={loading}
      />
    </div>
  )
}

demo中展示了日期组件以及包含有分页器的 Table组件,当日期发生变更,或者分页器发生变更时,我们需要dispatch来更新请求参数,从而发送请求。在发送请求时如果正在请求,则忽略,而不在请求时需要手动加锁,来防止多次请求。


同时Table需要根据请求状态来判断是否需要展示loading。

解决问题

基于以上的问题,我们能否通过 Hooks 来封装一个 custom hooks来解决问题。

1. 明确目标

custom hooks 解决的问题

  • 解决每个函数都要统一写try/catch的流程
  • 解决发送请求需要手动加锁防止多次重复请求的痛点
  • 不需要在手动useState loading,直接获取loading值

所以我们需要在 custom hooks 中发送请求、暴露出请求后的值、暴露 loading 状态、以及用户可能需要多次请求,这就需要暴露一个勾子。在发生请求错误时可能需要做某些操作,所以还需要暴露在错误时回调的勾子函数。

是否立即请求并接受初始化返回值

业务我们并不希望初始化的是否立即发送请求。
并且能够有初始化的返回值

支持泛型

在TS中,开发者希望能够自定义请求的参数类型,以及请求结果的类型

2. 定义函数

useFetch 函数

import { useState, useEffect } from "react";

/**
 * 1. 解决每个函数都要统一写try/catch的流程
 * 2. 解决发送请求需要手动加锁防止多次重复请求的痛点
 * 3. 不需要在手动useState loading了~,直接获取fetching值
 * 4. (甚至在参数发生变化时只需要传入更改的参数就OK)已删除
 * @param getFunction 发送请求的函数
 * @param params 参数
 * @param initRes 初始化值
 * @param execute 是否立即执行请求函数
 */

// R, P支持泛型
function UseFetch<R, P>(
  getFunction: any,
  params: P,
  initRes?: R,
  execute: boolean = true
): [
  R,
  boolean,
  (params?: Partial<P>) => void,
  (fn?: (err: any) => void) => void
] {
  type ErrorFunction = ((fn?: (err: any) => void) => void) | null;
  const [res, setRes] = useState(initRes as R);
  const [fetching, setFetch] = useState(false);
  const [failed, setFailed] = useState<ErrorFunction>(null);

  // 参数也许并不是每次都完整需要 Partial<P>
  const fetchData: (params?: Partial<P>) => void = async (params?: any) => {
    if (fetching) return;
    setFetch(true);
    try {
      setRes(await getFunction(params));
      setFetch(false);
    } catch (err) {
      console.error(err);
      setFetch(false);
      failed && failed(err);
    }
  };

  const setError: ErrorFunction = fn => fn && setFailed(fn);

  // 首次执行只请求一次
  useEffect(() => {
    execute && fetchData(params);
  }, []);

  /**
   * res 返回的数据
   * fetching 是否在请求中
   * fetchData 手动再次触发请求
   * setError 当发生请求错误时,需要执行的回掉函数
   */
  return [res, fetching, fetchData, setError];
}

const useFetch = UseFetch;

export default useFetch;

3. 如何使用

根据最初的demo我们改造一下代码

import React, { useCallback, useReducer, useEffect } from 'react'
import { TimeNumberType, PageType } from 'common/constant/interface'
import { DateRangePicker, Table } from 'UI'
// 导入 useFetch
import { useFetch } from 'custom-hooks' 

type ParamsType = PageType & TimeNumberType
type ListInfo = {list: Array<any>, total: number}

const TIME = Symbol('time')
const PAGE = Symbol('page')
const reducer = (state: ParamsType, action: Actions) => {
  const { payload } = action
  return { ...state, ...payload }
}
const postListData = (params: ParamsType) => post('/network/api/test/getlist', params)
const initialParams = {
  pageSize: 10,
  pageNumber: 1,
  startTime: 0,
  endTime: 0
}

const ListComponent = () => {
  const [params, dispatch] = useReducer(reducer, initialState)
  
  const [list, loading, getList] = useFetch<ListInfo, ParamsType>(
    getWithDraw,
    state,
    { list: [], total: 0 },
    false
  )

  useEffect(() => {
    getList()
  }, [params])
  
  return (
      <div style={{ marginBottom: '20px' }}>
        <DateRangePicker
          onChange={handleDateChange}
        />
      <Table
        onPageChange={(pageNumber: number) => {
          dispatch({ payload: { pageNumber }, type: PAGE })
        }}
        list={list}
        // 数据是否正在加载,以此来判断是否需要展示loading
        loading={loading}
      />
    </div>
  )
}

对比代码我们可以看到中间的请求的代码被我们干掉了,使用 useFetch 来将状态以及发送请求封装在一起。能够让我们写更少的代码。


同时 useFetch的第3个参数当传入的为 null 时,可以模拟请求发送错误,这样我们可以在开发时做兜底方案。

4. 也许并不想要那么多值。

也许有些请求不需要关注请求状态

  // 解构赋值、空着就好
  const [list, , getList] = useFetch<ListInfo, ParamsType>(
    getWithDraw,
    state,
    { list: [], total: 0 },
    false
  )

本文完~

如有问题,欢迎指出~

React Hooks 之 useIntersectionObserver

前言

本文使用 React Hooks 构建一个监听 DOM 元素曝光的 Custom Hooks,主要功能是监听 DOM 元素是否在规定内曝光,从而可以完成曝光打点。

过去我们监听 DOM 元素都是通过监听 scroll 事件来监听目标元素是否在可视区,这样我们需要获取目标元素的一些数据。这样似乎很麻烦,而通过 Intersection Observer 来完成监听,更加方便、友好。

Intersection Observer

MDN: IntersectionObserver 接口 (从属于 Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

其他相关概念可见 谈谈 IntersectionObserver 懒加载IntersectionObserver API 使用教程

简单来说这个对象会观察目标元素,当目标元素与它的祖先元素或者可视区发生交叉时,会触发回调函数。

通过这个对象我们可以监听需要曝光打点的元素,更方便的实现打点。同时我们不需要监听 scroll 事件以及相对应的 DOM 操作,减少了性能的开销。

Intersection Observer 在 React 中的实践

为了能够在不同的场景中使用,我们可以将其封装成 HOOKS,更方便的调用该方法

1. 定义 useIntersectionObserver 函数

// useIntersectionObserver.ts
// 定义参数函数类型以及返回值类型

import { useState, useCallback, useEffect } from 'react'

type NumberList = number[]
type ObserverList = Array<React.RefObject<any>>
type CallbackFunction = (indexList: NumberList) => void
type ResultType = [React.Dispatch<React.SetStateAction<React.RefObject<any>[]>>]

function UseIntersectionObserver(
  observerList: ObserverList,
  callback: CallbackFunction,
  infinite: boolean = false,
  opt: IntersectionObserverInit = {}
): ResultType {
  // list 为需要监听的元素列表。setList做为UseIntersectionObserver函数的返回值,可以让调用者修改需要监听的 list
  const [list, setList] = useState<ObserverList>(observerList)

  // intersectionObserver: 观察者对象
  let intersectionObserver: IntersectionObserver | null = null

  // ...
  return [setList]
}

const useIntersectionObserver = UseIntersectionObserver

export default useIntersectionObserver

UseIntersectionObserver 函数参数:

  • observerList: 由被观察目标所组成的数组,数组项是由 React.createRef 构建出来的对象
  • callback: 当目标元素被曝光所需要触发的函数,该函数接受一个参数 indexList,由被曝光元素在 observerList 数组中的索引组成
  • infinite:是否持续观察目标元素,默认值为 false。(因为曝光打点一般只需上报一次)
  • opt:可以自定义曝光条件(值的构成参考 MDN),默认为{ threshold: [1] },只有当目标元素完全暴露在可视区内才触发回调

UseIntersectionObserver 返回值:

  • 返回一个数组,数组的第一项元素由 React 的 useState 所返回。

2. 实现监听

定义 observeExposure 函数

// UseIntersectionObserver

const observeExposure = useCallback((list: ObserverList) => {}, [])

使用 useCallback 减少不必要的重复函数声明

判断浏览器环境以及 list 的是否为空

if (typeof IntersectionObserver === 'undefined') {
  throw new Error('Current browser does not support IntersectionObserver ')
}
if (list.length === 0) return
  • 目标主流浏览器都已经支持该对象,但是还是兼容一些低版本浏览器
  • 当 list 为空列表是直接 return

构造新的观察者实例

// 当观察者存在时销毁该对象
intersectionObserver && intersectionObserver.disconnect()
// 构造新的观察者实例
intersectionObserver = new IntersectionObserver(entries => {
  // 保存本次监听被曝光的元素
  let activeList: NumberList = []

  // 递归每一个本次被监听对象,如果按照曝光条件出现在可视区,则调用callback函数,并且取消监听
  entries.forEach(entrie => {
    // 找出本次被监听对象在list中的索引
    const index = Array.from(list).findIndex(
      item => item.current === entrie.target
    )
    // 防止意外发生
    if (index === -1) return

    // isIntersecting是每个被监听的元素所自带的属性,若为ture,则表示被曝光
    // 并且未被曝光过
    if (entrie.isIntersecting) {
      // 保存本次曝光元素索引
      activeList.push(index)

      // 解除观察, 若需要无限观察则不取消监听
      !infinite &&
        intersectionObserver &&
        intersectionObserver.unobserve(list[index].current)
    }
  })

  // callback函数
  activeList.length > 0 && callback(activeList)
}, opt)

使用 intersectionObserver 监听 list 中的元素

// 递归观察每一个元素
list.forEach(item => {
  item.current &&
    intersectionObserver &&
    intersectionObserver.observe(item.current)

  // 可以兼容直接传入DOM节点。
  // if((<React.RefObject<any>>item).current) {
  //   intersectionObserver.observe((<React.RefObject<any>>item).current)
  // } else if ((<HTMLElement>item)) {
  //   intersectionObserver.observe((<HTMLElement>item))
  // }
})

3. 当每次被暴露的 setList 被调用时会使 list 被改变,此时需要重新监听

useEffect(() => {
  observeExposure(list)

  // 当 umount 时取消链接
  return () => {
    intersectionObserver && intersectionObserver.disconnect()
  }
}, [list])

4. 完整代码实现

import { useState, useCallback, useEffect } from 'react'

type NumberList = number[]
type ObserverList = Array<React.RefObject<any>>
type CallbackFunction = (indexList: NumberList) => void
type ResultType = [React.Dispatch<React.SetStateAction<React.RefObject<any>[]>>]

/**
 * UseIntersectionObserver
 * @param observerList 由被观察目标所组成的数组,数组项是由React.createRef构建出来的对象
 * @param callback 当目标元素被曝光所需要触发的函数,该函数接受一个参数indexList,由被曝光元素在observerList数组中的索引组成
 * @param infinite 是否持续观察目标元素,默认值为false。(因为曝光打点一般只需上报一次)
 * @param opt 可以自定义曝光条件(值的构成参考MDN),默认为{ threshold: [1] },只有当目标元素完全暴露在可视区内才触发回调
 */
function UseIntersectionObserver(
  observerList: ObserverList,
  callback: CallbackFunction,
  infinite: boolean = false,
  opt: IntersectionObserverInit = {}
): ResultType {
  // list 为需要监听的元素列表。setList做为UseIntersectionObserver函数的返回值,可以让调用者修改需要监听的 list
  const [list, setList] = useState<ObserverList>(observerList)

  // intersectionObserver: 观察者对象
  let intersectionObserver: IntersectionObserver | null = null

  const observeExposure = useCallback((list: ObserverList) => {
    if (!IntersectionObserver) {
      throw new Error('Current browser does not support IntersectionObserver ')
    }
    if (list.length === 0) return
    // 当观察者存在时销毁该对象
    intersectionObserver && intersectionObserver.disconnect()
    // 构造新的观察者实例
    intersectionObserver = new IntersectionObserver(entries => {
      // 保存本次监听被曝光的元素
      let activeList: NumberList = []

      // 递归每一个本次被监听对象,如果按照曝光条件出现在可视区,则调用callback函数,并且取消监听
      entries.forEach(entrie => {
        // 找出本次被监听对象在list中的索引
        const index = Array.from(list).findIndex(
          item => item.current === entrie.target
        )
        // 防止意外发生
        if (index === -1) return

        // isIntersecting是每个被监听的元素所自带的属性,若为ture,则表示被曝光
        // 并且未被曝光过
        if (entrie.isIntersecting) {
          // 保存本次曝光元素索引
          activeList.push(index)

          // 解除观察, 若需要无限观察则不取消监听
          !infinite &&
            intersectionObserver &&
            intersectionObserver.unobserve(list[index].current)
        }
      })

      // callback函数
      activeList.length > 0 && callback(activeList)
    }, opt)

    list.forEach(item => {
      item.current &&
        intersectionObserver &&
        intersectionObserver.observe(item.current)

      // 可以兼容直接传入DOM节点。
      // if((<React.RefObject<any>>item).current) {
      //   intersectionObserver.observe((<React.RefObject<any>>item).current)
      // } else if ((<HTMLElement>item)) {
      //   intersectionObserver.observe((<HTMLElement>item))
      // }
    })
  }, [])

  useEffect(() => {
    observeExposure(list)

    // 当 umount 时取消链接
    return () => {
      intersectionObserver && intersectionObserver.disconnect()
    }
  }, [list])

  return [setList]
}

const useIntersectionObserver = UseIntersectionObserver

export default useIntersectionObserver

使用案例

实现一个简单的商品列表曝光打点的案例

import Card from 'components/goods-card/goods-card'
import { connect } from 'react-redux'
import { getSinglePromotionList } from '../../page_components/promotion/redux/creator'
import React, { useEffect, useState, useCallback } from 'react'
import useIntersectionObserver from 'page_components/promotion/useIntersectionObserver'

const List = (props: { info: any; getData: any }) => {
  const { info, getData } = props

  // 被监听元素的列表
  const [refList, setRefList] = useState<React.RefObject<any>[]>([])

  const callback = useCallback((indexList: number[]) => {
    console.log(indexList)
  }, [])

  // 调用
  const [setList] = useIntersectionObserver(refList, callback)

  // 当refList发生改变时,调用我们的Hook返回的方法以更新需要监听的元素
  useEffect(() => {
    setList(refList)
  }, [refList])

  // 当数据发生改变时,重新生成RefList
  useEffect(() => {
    const list: React.RefObject<any>[] = info.list.map(() => React.createRef())
    setRefList(list)
  }, [info])

  // 发送请求,获取商品数据
  useEffect(() => {
    getData()
  }, [])

  return (
    <div style={{ display: 'flex', flexWrap: 'wrap' }}>
      {info.list.map((item: any, index: number) => (
        <div ref={refList[index]} key={index}>
          <Card card={item} />
        </div>
      ))}
    </div>
  )
}

const mapStateToProps = (state: any) => {
  return {
    info: state.promotionStore.singlePromotionInfo,
    userInfo: state.userInfo
  }
}

const mapDispatchToProps = (dispatch: any) => {
  return {
    getData: () => dispatch(getSinglePromotionList(params, silence))
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(List)

案例效果

从动图可以看出,当 card 被曝光时打印出索引值,而已经被曝光的值不会再次曝光。



本文完~
更多文章。 我的掘金~
如有疑问,欢迎指出~

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.