从零开始-文件资源管理器-07-处理 context 重复渲染

现状

当改变 column 时,除了顶部面包屑导航部分,整个界面都会重新渲染,即使不相关的页脚功能区的 "x 个项目"文案也会触发重新渲染。

这是由于将 column 与 readdir 的数据放在了同一个 context 内,当修改 readdir 或 column 都会导致所有使用该 context value 的都会触发渲染。即使只使用其中一项,例如单用 readdir,没有使用 column。

解决方案

  1. 将 column 与 readdir 分离至不同的 context 内
  2. 使用更高级的状态管理工具,例如 redux 等

由于界面比较简单,这里就使用方案 1 将 context 分离。稍微封装下,方便之后需要分离 context 的地方使用。

纯读取的 readdir 改造

文件路径:explorer/src/lib/create-ctx.tsx

typescript 复制代码
import React, { createContext, Dispatch, SetStateAction, useContext, useState } from 'react'

const createCtx = <T extends any>(initial_value?: T) => {
  const storeContext = createContext<T>(null!)
  const dispatchContext = createContext<Dispatch<SetStateAction<T>>>(null!)

  const useStore = () => {
    const context = useContext(storeContext)
    if (context === undefined) {
      throw new Error('useStore:context undefined')
    }
    return context
  }

  const useDispatch = () => {
    const context = useContext(dispatchContext)
    if (context === undefined) {
      throw new Error('useDispatch:context undefined')
    }
    return context
  }

  const ContextProvider: React.FC<React.ProviderProps<T>> = ({ value, children }) => {
    const [state, dispatch] = useState<T>(initial_value || value)

    return (
      <dispatchContext.Provider value={dispatch}>
        <storeContext.Provider value={state}>{children}</storeContext.Provider>
      </dispatchContext.Provider>
    )
  }

  return { ContextProvider, useStore, useDispatch }
}
export default createCtx

文件路径:explorer/src/app/path/readdir-context.tsx

创建 readdir-context 上下文。

javascript 复制代码
'use client'
import createCtx from '@/lib/create-ctx'
import { ReaddirListType } from '@/explorer-manager/src/type'
import React from 'react'

const ReaddirContext = createCtx<ReaddirListType>()

export const useReaddirContext = () => {
  return ReaddirContext.useStore()
}

export const ReaddirProvider: React.FC<React.ProviderProps<ReaddirListType>> = ({ value, children }) => {
  return <ReaddirContext.ContextProvider value={value}>{children}</ReaddirContext.ContextProvider>
}

文件路径:explorer/src/app/path/context.tsx

将 ReaddirProvider 插入。将 readdir 从 PathContext 剔除。将 readdir 赋值给 ReaddirProvider 组件。

typescript 复制代码
...
import { ReaddirProvider } from '@/app/path/readdir-context'

type PathContextType = {
  display_type: 'card' | 'table'
  changeDisplayType: React.Dispatch<React.SetStateAction<PathContextType['display_type']>>
  column: number
  changeColumn: React.Dispatch<React.SetStateAction<number>>
}
...
export const PathContextProvider: React.FC<React.ProviderProps<ReaddirListType>> = ({ value, children }) => {
...
  return (
    <PathContext.Provider
      value={{
        display_type: display_type,
        changeDisplayType: changeDisplayType,
        column: column || def_column,
        changeColumn: changeColumn,
      }}
    >
      <ReaddirProvider value={value}>{children}</ReaddirProvider>
    </PathContext.Provider>
  )
}

文件路径:explorer/src/components/readdir-count.tsx

将从 const { readdir } = usePathContext() 读取值修改为 const readdir = useReaddirContext() 读取

javascript 复制代码
'use client'
import React from 'react'
import { Space } from 'antd'
import { useReaddirContext } from '@/app/path/readdir-context'

const ReaddirCount: React.FC = () => {
  const readdir = useReaddirContext()

  return (
    <Space>
      <span>{readdir.length}</span>
      <span>个项目</span>
    </Space>
  )
}

export default ReaddirCount

效果

可以看到页脚功能区的 "x 个项目"文案不会触发重新渲染。

读写的 column

上面的 readdir 仅涉及了读取,column 则涉及了读写

文件路径:explorer/src/app/path/card-colunm-context.tsx

创建 card column context 上下文文件

typescript 复制代码
'use client'
import createCtx from '@/lib/create-ctx'
import React, { useEffect, useState } from 'react'
import { useViewport } from '@/components/viewport/context'
import { useMount, useSessionStorageState } from 'ahooks'

const CardColumnContext = createCtx<number>()

export const useCardColumnContext = () => {
  return CardColumnContext.useStore()
}

export const useCardColumnContextDispatch = () => {
  return CardColumnContext.useDispatch()
}

export const CardColumnProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
  const { width } = useViewport()
  const def_column = Math.ceil(width / 280)
  const [column, changeColumn] = useState<number>(def_column)
  const [session_column, changeSessionColumn] = useSessionStorageState('card-column')

  useEffect(() => {
    changeSessionColumn(column)
  }, [column])

  useMount(() => {
    changeColumn(Number(session_column))
  })

  return <CardColumnContext.ContextProvider value={column || def_column}>{children}</CardColumnContext.ContextProvider>
}

将之前的内容迁移过来,

导出的 useCardColumnContext 为读取方法。useCardColumnContextDispatch 为改变方法。

文件路径:explorer/src/app/path/context.tsx

将 CardColumnProvider 插入

javascript 复制代码
...
import { CardColumnProvider } from '@/app/path/card-colunm-context'
...
  return (
    <PathContext.Provider
      value={{
        display_type: display_type,
        changeDisplayType: changeDisplayType,
      }}
    >
      <ReaddirProvider value={value}>
        <CardColumnProvider>{children}</CardColumnProvider>
      </ReaddirProvider>
    </PathContext.Provider>
  )
}

文件路径:explorer/src/app/path/[[...path]]/card-display.tsx

将之前读的 const { readdir, column } = usePathContext() 修改为 const readdir = useReaddirContext()const column = useCardColumnContext()

javascript 复制代码
...
import { useReaddirContext } from '@/app/path/readdir-context'
import { useCardColumnContext } from '@/app/path/card-colunm-context'

const CardDisplay: React.FC = () => {
  const pathname = usePathname()
  const readdir = useReaddirContext()
  const column = useCardColumnContext()
...

最后

整体改动还是挺简单,就是将各个 context 分离。createCtx 方法内部实现了读写分离。在消费数据,修改数据时使用了 useStoreuseDispatch 方法。

当内部消费的层级不对,useContext 调用是会返回 undefined。所以内部通过判断是否为 undefined 是抛出错误。

javascript 复制代码
if (context === undefined) {
  throw new Error('useDispatch:context undefined')
}

ContextProvider 内是使用 useState 进行状态管理,可以对应的替换为 useReduce 或 ahooks 的其他状态管理 hooks,对应的修改 useDispatch 的方法即可。

由于目前仅涉及简单的状态操作,坐等后续需要涉及到复杂状态管理。

文件路径:explorer/src/app/path/context.tsx

最后贴上 PathContextProvider 代码。第一版的上下文状态管理正式下线,后续仅将作为组合各个独立 context 的组件。

javascript 复制代码
import React from 'react'
import { ReaddirListType } from '@/explorer-manager/src/type'
import { ReaddirProvider } from '@/app/path/readdir-context'
import { CardColumnProvider } from '@/app/path/card-colunm-context'
import { DisplayTypeProvider } from '@/app/path/display-type-context'

export const PathContextProvider: React.FC<React.ProviderProps<ReaddirListType>> = ({ value, children }) => {
  return (
    <>
      <ReaddirProvider value={value}>
        <CardColumnProvider>
          <DisplayTypeProvider>{children}</DisplayTypeProvider>
        </CardColumnProvider>
      </ReaddirProvider>
    </>
  )
}

git-repo

yangWs29/share-explorer

相关推荐
早點睡3906 分钟前
ReactNative项目OpenHarmony三方库集成实战:@react-native-community/slider
javascript·react native·react.js
踩着两条虫9 分钟前
VTJ.PRO 在线应用开发平台的代码生成与模板系统
前端·低代码·ai编程
早點睡39011 分钟前
ReactNative项目OpenHarmony三方库集成实战:react-native-progress
javascript·react native·react.js
前端小崽子14 分钟前
线上复制按钮失效?也许是这个原因
前端
张元清15 分钟前
React 滚动效果:告别第三方库
前端·javascript·面试
有志16 分钟前
Vue 学习总结(Java 后端工程师视角)
前端
踩着两条虫18 分钟前
VTJ.PRO 在线应用开发平台的DSL生命周期
前端·低代码·ai编程
我是伪码农18 分钟前
JS 复习
开发语言·前端·javascript
小碗细面19 分钟前
Claude Code 很强,但为什么我越来越常打开 Codex App?
前端·chatgpt·ai编程
愿你如愿20 分钟前
React Fiber 的主要目标是什么
前端·react.js