上一篇: React中实现返回列表时保持滚动位置scrollTop不变(一)
根据上一篇得知,我们需要处理的是:
- 保证列表页的滚动容器是body
- 缓存列表页的数据到状态管理,返回列表页的时候,直接使用缓存的数据
- 处理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]
}