React中实现返回列表时保持滚动位置scrollTop不变(二)

上一篇: React中实现返回列表时保持滚动位置scrollTop不变(一)

根据上一篇得知,我们需要处理的是:

  1. 保证列表页的滚动容器是body
  2. 缓存列表页的数据到状态管理,返回列表页的时候,直接使用缓存的数据
  3. 处理React中前进(跳转)时,总会保持scrollTop的问题

1. useMobxAntdTable 处理问题2

我的项目是后台管理系统,主要使用的是antd4 + ahooks的useAntdTable 绘制列表页

这里我封装了一个 useMobxAntdTable 用来处理缓存列表数据,用法和useAntdTable一模一样,具体用法详见注释

原理

通过mobx缓存数据,页面载入的时候,通过 location.state.stateType 判断是使用缓存数据,还是使用新请求的数据

以下是源码:

结构:

useMobxAntdTable/_pageStore.ts:

ts 复制代码
import { autorun, makeAutoObservable, runInAction } from 'mobx'

interface IOnePageState {
  formValues: any
  pageState: {
    current: number
    pageSize: number
    total: number
  }
  tableData: any[]
}

class _PageStore {
  constructor() {
    makeAutoObservable(this, {}, { autoBind: true })
  }

  state: Record<string, IOnePageState> = {}

  // actions
  setState = (pageKey: string, key2: string, v: any) => {
    if (!this.state[pageKey]) {
      this.state[pageKey] = {
        formValues: {},
        pageState: {
          current: 1,
          pageSize: 10,
          total: 0,
        },
        tableData: [],
      }
    }
    this.state[pageKey][key2] = v
  }
  removeState = (pageKey: string) => {
    delete this.state[pageKey]
  }
}

const _pageStore = new _PageStore()

export default _pageStore

useMobxAntdTable/index.ts:

ts 复制代码
import { useAntdTable, useLatest, useUpdateEffect } from 'ahooks'
import { useEffect, useRef } from 'react'
import _pageStore from './_pageStore'
import { AntdTableOptions, AntdTableResult, Data, Params, Service } from 'ahooks/lib/useAntdTable/types'
import { useLocation, useNavigate } from 'react-router-dom'

type IStateType = undefined | 'allNew' | 'refresh'

/**
 *
 * @description 用法和 useAntdTable 一样
 * 但是会保存 form 数据和 table 数据到 mobx (注:在mobx中,这个页面的state是以pathname作为key的)
 *
 * 重新访问页面时,默认使用缓存数据,不刷新!(不重新请求table)
 *
 * 如果需要刷新,需要在跳转的时候,传递 state.stateType = 'allNew' | 'refresh'
 * 1. allNew: 使用新的form数据、页码数据,刷新table
 * 2. refresh: 保持form数据、页码数据,刷新table
 *
 * @example
 * navigate(-1) // 返回上一页,使用缓存数据,不刷新
 * navigate(`/web/company/goods/goodsmgr`) // 如果打开过这个页,使用缓存数据,不刷新
 * navigate(`/web/company/goods/goodsmgr`, { state: { stateType: 'allNew' } }) // 使用新的form数据、页码数据,刷新table
 * navigate(`/web/company/goods/goodsmgr`, { state: { stateType: 'refresh' } }) // 保持form数据、页码数据,刷新table
 *
 */
export default function useMobxAntdTable<TData extends Data, TParams extends Params>(
  service: Service<TData, TParams>,
  _options?: AntdTableOptions<TData, TParams>
): AntdTableResult<TData, TParams> {
  const options = _options ?? {}
  const form = options?.form

  const navigate = useNavigate()
  const location = useLocation()
  const locationRef = useLatest(location)
  const stateType = location.state?.stateType as IStateType
  const thisPageKey = location.pathname

  const isUseStoreData = useRef(false) // 标记

  const result = useAntdTable(
    (...args) => {
      if (form) {
        // 每次请求的时候,保存form数据
        const values = form.getFieldsValue()
        _pageStore.setState(thisPageKey, 'formValues', values)
        // 通过 tableProps.onChange 触发时,会缺失 formData 数据,这里传上
        if (!args[1]) {
          args[1] = values
        }
      }

      // 使用缓存数据 不刷新
      if (isUseStoreData.current) {
        isUseStoreData.current = false
        return new Promise((resolve) => {
          return resolve({
            // @ts-ignore
            list: _pageStore.state[thisPageKey]?.tableData ?? [],
            total: _pageStore.state[thisPageKey]?.pageState.total ?? 0,
          })
        })
      }

      // 请求
      return service(...args)
    },
    {
      ...options, //
      manual: true,
    }
  )

  const { tableProps, search, refresh, mutate } = result

  // 初始加载
  useEffect(() => {
    if (stateType === 'allNew') {
      // 不用回显 form 数据,和 table 数据
      _pageStore.removeState(thisPageKey)
      search.submit()
    } else {
      // undifined | 'refresh' 都会回显 form 数据
      // 回显 form 数据
      if (form) {
        if (_pageStore.state[thisPageKey]?.formValues && JSON.stringify(_pageStore.state[thisPageKey].formValues) !== '{}') {
          form.setFieldsValue(_pageStore.state[thisPageKey].formValues)
        }
      }

      // 根据 undifined | 'refresh' 两种情况 选择是否回显 table 数据
      if (_pageStore.state[thisPageKey]?.tableData && _pageStore.state[thisPageKey].tableData.length > 0) {
        // 表格有缓存数据

        if (stateType === 'refresh') {
          isUseStoreData.current = false
        } else {
          isUseStoreData.current = true
        }

        tableProps.onChange?.(_pageStore.state[thisPageKey].pageState) // 使用缓存页码
      } else {
        // 如果表格之前是空的,总要刷新一下
        search.submit()
      }
    }

    // 最后,重置 state.stateType
    if (locationRef.current.state?.stateType && locationRef.current.pathname === location.pathname) {
      navigate(location.pathname, {
        state: {
          ...location.state,
          stateType: undefined,
        },
      })
    }
  }, [])

  ////
  /// 因为 manual=true, 所以 refreshDeps 会失效, 这里写一个 useEffect 来处理
  useUpdateEffect(() => {
    search.submit()
  }, [...(options.refreshDeps ?? [])])

  /// 保存页码和table数据
  useEffect(() => {
    _pageStore.setState(thisPageKey, 'pageState', tableProps.pagination)
  }, [tableProps.pagination])
  useEffect(() => {
    _pageStore.setState(thisPageKey, 'tableData', tableProps.dataSource)
  }, [tableProps.dataSource])

  return result
}

2. useScrollToTopByNavigationType 处理问题3

使用:在详情页调用这个hooks就行

原理:使用 react-router 的useNavigationType 判断跳转类型(是浏览器的返回,还是跳转)

源码:

useScrollToTopByNavigationType.ts:

ts 复制代码
import { useEffect } from 'react'
import { useNavigationType } from 'react-router-dom'

export default function useScrollToTopByNavigationType() {
  const navigationType = useNavigationType()

  useEffect(() => {
    // POP 代表是通过浏览器的前进后退按钮触发的页面切换 或者 navigate(-1) navigate(1)
    if (navigationType !== 'POP') {
      window.scrollTo(0, 0)
    }
  }, [])
}

3. 效果展示

3.1 列表页=>详情页,详情页回到顶部,详情页返回,列表不刷新且滚动位置不变

3.2 如果不想用缓存的数据,可以在 location的state中增加 stateType: allNew

我左侧的菜单跳转都增加了state.stateType = allNew ,所以都是重新加载数据,并且scrollTop回到顶部

效果:

3. 如果不想和useAntdTable耦合, useMobxState

可以参考 useMobxState 这个hooks,仅控制单个变量

源码:

useMobxState/_store.ts:

ts 复制代码
import { autorun, makeAutoObservable, runInAction } from 'mobx'

class _Store {
  constructor() {
    makeAutoObservable(this, {}, { autoBind: true })
  }

  state: {
    [pageKey in string]: {
      [stateName in string]: any
    }
  } = {}

  // actions
  setState = (pageKey: string, stateName: string, v: any) => {
    if (!this.state[pageKey]) {
      this.state[pageKey] = {}
    }
    this.state[pageKey][stateName] = v
  }
  removeState = (pageKey: string, stateName: string) => {
    if (this.state[pageKey]) {
      delete this.state[pageKey][stateName]
    }
  }
}

const _store = new _Store()

export default _store

useMobxState/index.ts:

ts 复制代码
import { Dispatch, SetStateAction, useState, useEffect, useDeferredValue } from 'react'
import _store from './_store'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLatest } from 'ahooks'

/**
 *
 * @description 用法基本和 useState 一样
 * 多一个参数 stateName
 * 保存数据到 mobx (注:在mobx中,这个state是以 pathname + stateName 作为key的)
 *
 * 重新访问页面时,默认使用缓存数据
 *
 * 只有在跳转的时候,传递 state.stateType = 'allNew' 时,才使用新数据
 * 一般和 useMobxAntdTable 配合使用
 */

export default function useMobxState<S>(initialState: S | (() => S), stateName: string): [S, Dispatch<SetStateAction<S>>] {
  const navigate = useNavigate()
  const location = useLocation()
  const locationRef = useLatest(location)
  const stateType = location.state?.stateType as undefined | 'allNew'
  const thisPageKey = location.pathname

  const [state, setState] = useState(() => {
    function getDefaultInitialState() {
      return typeof initialState === 'function' ? (initialState as any)() : initialState
    }

    if (stateType === 'allNew') {
      // 清空 _store
      _store.removeState(thisPageKey, stateName)
      return getDefaultInitialState()
    } else {
      if (_store.state[thisPageKey] && _store.state[thisPageKey][stateName] !== undefined) {
        return _store.state[thisPageKey][stateName]
      } else {
        return getDefaultInitialState()
      }
    }
  })

  // setState
  const mySetState: Dispatch<SetStateAction<S>> = (newValue) => {
    setState((cur) => {
      const _newValue = typeof newValue === 'function' ? (newValue as any)(cur) : newValue
      _store.setState(thisPageKey, stateName, _newValue) // 更新 _store
      return _newValue as S
    })
  }

  useEffect(() => {
    // 最后,重置 state.stateType
    if (locationRef.current.state?.stateType && locationRef.current.pathname === location.pathname) {
      navigate(location.pathname, {
        state: {
          ...location.state,
          stateType: undefined,
        },
      })
    }
  }, [])

  return [state, mySetState]
}
相关推荐
布瑞泽的童话16 分钟前
无需切换平台?TuneFree如何搜罗所有你爱的音乐
前端·vue.js·后端·开源
白鹭凡28 分钟前
react 甘特图之旅
前端·react.js·甘特图
2401_8628867832 分钟前
蓝禾,汤臣倍健,三七互娱,得物,顺丰,快手,游卡,oppo,康冠科技,途游游戏,埃科光电25秋招内推
前端·c++·python·算法·游戏
书中自有妍如玉39 分钟前
layui时间选择器选择周 日月季度年
前端·javascript·layui
Riesenzahn40 分钟前
canvas生成图片有没有跨域问题?如果有如何解决?
前端·javascript
f89790707042 分钟前
layui 可以使点击图片放大
前端·javascript·layui
忘不了情1 小时前
左键选择v-html绑定的文本内容,松开鼠标后出现复制弹窗
前端·javascript·html
世界尽头与你1 小时前
HTML常见语法设计
前端·html
写bug如流水1 小时前
【Git】Git Commit Angular规范详解
前端·git·angular.js