reactjs 在Typescript中使用React Context(使用useState)的最佳方法

628mspwn  于 2023-10-17  发布在  React
关注(0)|答案(4)|浏览(122)

我的代码看起来像这样:

SomeContext.ts:

export interface SomeContext {
  someValue: string;
  someFunction: () => void;
}

export const defaultValue: SomeContext = {
  someValue: "",
  someFunction: () => {},
};

export const SomeContext = React.createContext<SomeContext>(defaultValue);

SomeComponent.tsx:

function SomeComponent() {
  const [someValue, setSomeValue] = useState(defaultValue.someValue);

  return (
    <SomeContext.Provider value={{ someValue, setSomeValue }}>
      {/*...*/}
    </SomeContext.Provider>
  );
}

让我感到困扰的是,我必须使用defaultValue来初始化上下文和控制该上下文的状态。
难道没有更直接的方法来创建一个由状态控制的上下文吗?我的意思是,难道没有一种方法可以单独初始化状态和上下文吗?这里的最佳做法是什么?
我尽量不给给予someContext一个默认值,但是Typescript(也许这样做是正确的)给出了一个警告。

qjp7pelc

qjp7pelc1#

下面是我用来创建标准化上下文的抽象

import React, { createContext, useCallback, useContext, useMemo, useReducer } from 'react'

type initialCtx<T> = {
    state: T,
    updateState: (payload: Partial<T>) => void
}

function makeUseProvider<T extends Record<string, any>>(initialState: T) {
    const Context = createContext<initialCtx<T>>({
        state: initialState,
        updateState: () => null,
    })

    const Provider = (Component: React.FC<any>) => {
        const useContextProvider = () => {
            function reducer<T>(state: T, payload: Partial<T>) {
                return {
                    ...state,
                    ...payload,
                }
            }

            const [state, dispatch] = useReducer(reducer, initialState) as [T, initialCtx<T>["updateState"]]

            const updateState = useCallback((partialState: Partial<T>) => {
                dispatch(partialState)
            }, [])

            const resetState = useCallback(() => {
                dispatch(initialState)
            }, [dispatch])

            return useMemo(() => ({
                state,
                updateState,
                resetState,
            }), [state, updateState, resetState])
        }

        function ContextHOC<T>(props: T) {
            const { updateState, state, resetState } = useContextProvider()
            const ctx = {
                state,
                updateState,
                resetState,
            }

            return (
                <Context.Provider value={ctx}>
                    <Component {...props} />
                </Context.Provider>
            )
        }
        return ContextHOC
    }

    return {
        Provider,
        useProvider: () => useContext(Context),
    }
}

export default makeUseProvider

然后就这样使用了。您可以导入useProvider以访问所需位置的数据和setter

const { Provider, useProvider } = makeUseProvider({
    someValue: "",
    someFunction: () => { },
})

const Component = () => {
    const { state, updateState } = useProvider()
    return <div />
}

export default Provider(Component)

这个函数是一个抽象出状态管理的工厂(通过useReducer + Context)。将makeUseProvider转换为初始状态。这将是初始值,不需要在其他任何地方重新定义。

const { Provider, useProvider } = makeUseProvider({
    someValue: "",
    someFunction: () => { },
})

Provider是高阶分量。这与在上下文中 Package 一组组件是一样的。您可以用它 Package 最顶层的组件,并且状态管理将在组件层次结构中的所有子组件中可用

const Table = Provider(() => {
  return <Row />
})

const Row = () => {
  const {state, updateState} = useProvider()
  return <div />
}

updateState接受已定义数据结构的子集(makeUseProvider的初始状态),并将以前的状态与新数据合并。

s3fp2yjn

s3fp2yjn2#

我同意,必须定义(和维护)默认状态是令人讨厌的(特别是当有几个状态值时)。我通常采取以下方法:

import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';

export interface SomeContextValue {
   someValue: string;
   someFunction: () => void;
}

// I generally don't export the Context itself.
// I export 'SomeProvider' for creating the context and 'useSomeContext' hook to consume it.
// That way, we can skip the type checking here and verify the actual values later (if necessary).
const SomeContext = React.createContext<SomeContextValue>({} as SomeContextValue);

// The provider is responsible for managing its own state.
// If you want to reuse it in slightly different ways, pass some extra props to configure it.
export const SomeProvider: React.FC<PropsWithChildren> = (props) => {

   // The state could be initialised via some default value from props...
   // const [someValue, setSomeValue] = useState(props.defaultValue);

   // ...or by some async logic inside a useEffect.
   const [someValue, setSomeValue] = useState<string>();
   useEffect(() => {
      loadSomeValue().then(setSomeValue);
   }, []);

   // wrapping the state-mutation function in useCallback is optional,
   // but it can stop unnecessary re-renders, depending on where the provider sits in the tree
   const someFunction = useCallback(() => {
      const nextValue = ''; // Some logic
      setSomeValue(nextValue);
   }, []);

   // We ensure the state value actually exists before letting the children render
   // If waiting for some data to load, we may render a spinner, text, or something useful instead of null
   if (!someValue) return null;

   return (
      <SomeContext.Provider value={{ someValue, someFunction }}>
         {props.children}
      </SomeContext.Provider>
   );
};

// This hook is used for consuming the context.
// I usually add a check to make sure it can only be used within the provider.
export const useSomeContext = () => {
   const ctx = React.useContext(SomeContext);
   if (!ctx) throw new Error('useSomeContext must be used within SomeProvider');
   return ctx;
};

注意:这个样板文件的大部分可以抽象成一个helper/factory函数(很像@Andrew的makeUseProvider),但我们发现这使得开发人员更难调试。事实上,当你自己在6个月后重新审视代码时,很难弄清楚发生了什么。所以我更喜欢这种明确的方法。

pw9qyyiw

pw9qyyiw3#

SomeContext.ts无需使用defaultValue

import React from 'react';

export interface SomeContext {
  someValue: string;
  setSomeValue: React.Dispatch<React.SetStateAction<string>>;
}

export const SomeContext = React.createContext<SomeContext | undefined>(undefined);

在您的SomeComponent.tsx

import React, { useState, useContext } from 'react';
import { SomeContext } from './SomeContext';

function SomeComponent() {
  const [someValue, setSomeValue] = useState<string>("");     
  return (
    <SomeContext.Provider value={{ someValue, setSomeValue }}>
      {/* ... */}
    </SomeContext.Provider>
  );
}

export default SomeComponent;

这种方法可以使您的代码更具可读性和可维护性。

baubqpgj

baubqpgj4#

你可以这样写typeScript:

SomeContext.tsx:

定义你的上下文,不需要在这里定义默认值。

import React, { createContext, useContext, useState } from 'react';

export interface SomeContextType {
    someValue: string;
    setSomeValue: React.Dispatch<React.SetStateAction<string>>;
}

const SomeContext = createContext<SomeContextType | undefined>(undefined);

export const useSomeContext = () => {
    const context = useContext(SomeContext);
    if (context === undefined) {
        throw new Error('context not found');
    }
    return context;
};

SomeComponent.tsx:

定义使用上下文的组件。

import { useSomeContext } from 'SomeContext';

const SomeComponent = () => {
    const { someValue, setSomeValue } = useSomeContext();

    // Rest of your component code
}

App.tsx

使用SomeContext.Provider Package 应用程序。

import { SomeContext, SomeContextType } from 'SomeContext';

const App = () => {
    const defaultValue: SomeContext = {
        someValue: '',
        setSomeValue: () => {}
    };

    const [someValue, setSomeValue] = useState<string>(defaultValue.someValue);

    return (
        <SomeContext.Provider value={{ someValue, setSomeValue }}>
            // Rest of your code
        </SomeContext.Provider>
    );
}

我认为这种方法通常被认为是在TypeScript中使用React Context的最佳方法。

相关问题