Context

coderljw 2024-10-13 React
  • React
  • Context
大约 2 分钟

# 1. Context

  • index.tsx
import {
  createContext as reactCreateContext,
  useEffect,
  useContext as useReactContext,
  useReducer,
  useRef,
} from 'react'
import isEqual from 'react-fast-compare'
import { createReducer } from './reducer'
import type { InitialReducer } from './reducer'

export type CreateContextParams<S extends State = State, R extends Reducers<S> = Reducers<S>> = {
  initialState?: S
  reducers?: R
}

export const createContext = <S extends State, R extends Reducers<S>>({
  initialState = {} as S,
  reducers = {} as R,
}: CreateContextParams<S, R>) => {
  const reducer = createReducer<S, R>(reducers)
  type Dispatch = React.Dispatch<Parameters<typeof reducer>[1]>
  const Context = reactCreateContext({} as { state: S; dispatch: Dispatch })
  const useContext = () => useReactContext(Context)

  const context =
    <P extends Record<string, any> = Record<string, any>>(Component: React.FC<P>) =>
    (props: P) => {
      const [state, dispatch] = useReducer(
        reducer as Reducer<S, InitialReducer<S>, S>,
        initialState,
      )

      return (
        <Context.Provider value={{ state, dispatch: dispatch as Dispatch }}>
          <Component {...props} />
        </Context.Provider>
      )
    }

  const useBond = (dep: S) => {
    const prevDep = useRef(dep)
    const { dispatch } = useContext()

    useEffect(() => {
      if (!dep || typeof dep !== 'object') {
        console.warn(
          'The useBond dep has no value or non-object type; the expected value is an object of type State.',
        )
        return
      }

      dispatch({ type: 'state', payload: dep })
      prevDep.current = dep
    }, [isEqual(dep, prevDep.current)])
  }

  return { Context, context, useContext, useBond }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  • reducer
import { produce } from 'immer'

export type InitialReducer<S extends State = State> = {
  type: 'state'
  payload: S
}

export const createReducer = <S extends State = State, R extends Reducers<S> = Reducers<S>>(
  reducers = {} as R,
) => {
  const initialReducers = {
    state: (state: S, payload: S) => {
      return produce(state, (draft) => {
        Object.assign(draft, payload)
      })
    },
  }

  const externalReducers = Object.entries(reducers).reduce((acc, [type, reducer]) => {
    return {
      ...acc,
      [type]: (state: S, payload: Parameters<typeof reducer>[1]) => {
        return produce(state, (draft) => {
          reducer(draft as S, payload)
        })
      },
    }
  }, {} as R)

  const mergeReducers = { ...initialReducers, ...externalReducers }
  const reducer: Reducer<S, Action<S, typeof mergeReducers>> = (state, action) => {
    // @ts-ignore
    return mergeReducers[action.type]?.(state, action.payload)
  }

  return reducer
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  • types.d.ts
type State = Record<string, any>
type Reducer<S extends State = State, P = any, R = void> = (state: S, payload: P) => R
type Reducers<S extends State = State> = Record<string, Reducer<S, Parameters<Reducer>[1]>>

type ActionTypeMap<S extends State = State, R extends Reducers<S> = Reducers<S>> = {
  [K in keyof R]: Parameters<R[K]>[1] extends undefined
    ? { type: K }
    : { type: K; payload: Parameters<R[K]>[1] }
}

type Action<S extends State = State, R extends Reducers<S> = Reducers<S>> = ActionTypeMap<
  S,
  R
>[keyof ActionTypeMap<S, R>]
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2. Zustand-Context

import React, { useEffect, useRef } from 'react'
import isEqual from 'react-fast-compare'
import { createStore, useStore as zUseStore } from 'zustand'
import type { StateCreator, StoreApi } from 'zustand'

export const createContext = <S extends Record<string, any>>(initializer: StateCreator<S>) => {
  const Context = React.createContext<StoreApi<S>>({} as StoreApi<S>)

  const context =
    <P extends Record<string, any> = Record<string, any>>(Component: React.FC<P>) =>
    (props: P) => {
      const [store] = React.useState(() => createStore<S>(initializer))
      return (
        <Context.Provider value={store}>
          <Component {...props} />
        </Context.Provider>
      )
    }

  const useStore = <U = S,>(selector?: (state: S) => U) => {
    const store = React.useContext(Context)
    return zUseStore(store, selector!)
  }

  const useBond = (dep: S) => {
    const prevDep = useRef(dep)
    const { setState } = React.useContext(Context)

    useEffect(() => {
      if (!dep || typeof dep !== 'object') {
        console.warn(
          'The useBond dep has no value or non-object type; the expected value is an object of type State.',
        )
        return
      }

      setState(dep)
      prevDep.current = dep
    }, [isEqual(dep, prevDep.current)])
  }

  return { Context, context, useStore, useBond }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
以父之名
周杰伦.mp3