通过 useFooController/useFooHook 限制使用 useContext 的组件的渲染

Posted

技术标签:

【中文标题】通过 useFooController/useFooHook 限制使用 useContext 的组件的渲染【英文标题】:Limit renders of component that uses useContext via a useFooController/useFooHook 【发布时间】:2021-05-17 23:48:16 【问题描述】:

我正在为组件使用自定义挂钩,而自定义挂钩使用自定义上下文。考虑

/* assume FooContext has  state: FooState, dispatch: () => any  */

const useFoo = () => 
  const  state, dispatch  = useContext(FooContextContext)
  return apiCallable : () => apiCall(state) 


const Foo = () => 
  const  apiCallable  = useFoo()
  return (
    <Button onClick=apiCallable/>
  )

许多组件将从其他组件(表单输入等)更改FooState。在我看来,Foo 使用 useFoo,它使用来自 FooStateContextstate。这是否意味着对FooContext 的每次更改都会重新渲染Foo 组件?它只需要在有人单击按钮时使用状态,否则不需要。看起来很浪费。

我在想 useCallback 是专门针对这个的,所以我在想 return apiCallable : useCallback(() =&gt; apiCall(state)) 但是我需要添加 [state] 作为 useCallback 的第二个参数。那么这意味着回调将在状态更新时重新渲染,所以我又回到了同样的问题,对吧?

这是我第一次做这样的自定义钩子。很难理解useCallback。如何完成我想要的?

编辑 换句话说,我有很多组件会向这个状态的深层嵌套属性发送小的更改,但是这个特定的组件必须发送 整个 状态对象通过 RESTful API,否则将永远使用该状态。它与完全渲染此组件无关。我想做到这一点,即使我通过输入上的按键不断更改状态(例如),这个组件也永远不会呈现。

【问题讨论】:

这是 react-redux 在尝试早期采用 react context 来做 store state 时面临的一个问题。钩子不够聪明,无法知道您正在使用上下文的哪些部分,因此如果上下文的任何部分发生更改,则必须重新渲染组件。如果上下文不是很大,您可能会考虑将其分成两部分,一个用于您的操作的上下文,一个用于您不断变化的状态的上下文。这样,提供组件可以处理这两个上下文如何交互,并且您可以在不重新渲染的情况下获得您的操作。 你试过用 React.memo 记忆 Foo 组件吗? 【参考方案1】:

由于您在问题中提供了 Typescript 类型,我将在回复中使用它们。

方法一:拆分上下文

给定以下类型的上下文:

type ItemContext = 
    items: Item[];
    addItem: (item: Item) => void;
    removeItem: (index: number) => void;

您可以将上下文拆分为具有以下类型的两个单独的上下文:

type ItemContext = Item[];
type ItemActionContext = 
    addItem: (item: Item) => void;
    removeItem: (index: number) => void;

然后提供组件将处理这两个上下文之间的交互:

const ItemContextProvider = () => 
    const [items, setItems] = useState([]);

    const actions = useMemo(() => 
        return 
            addItem: (item: Item) => 
                setItems(currentItems => [...currentItems, item]);
            ,
            removeItem: (index: number) => 
                setItems(currentItems => currentItems.filter((item, i) => index === i));
            
        ;
    , [setItems]);

    return (
        <ItemActionContext.Provider value=actions>
            <ItemContext.Provider value=items>
                children
            </ItemContext.Provider>
        </ItemActionContext.Provider>
    )
;

这将允许您访问两个不同的上下文,它们是一个更大的组合上下文的一部分。

基础 ItemContext 将随着项目的添加和删除而更新,从而导致重新呈现正在使用它的任何内容。

关联的 ItemActionContext 永远不会更新(setState 函数在其生命周期内不会更改)并且永远不会直接导致消费组件的重新渲染。

方式二:某些版本的基于订阅的价值

如果你让你的上下文的值永远不会改变(改变而不是替换,世界变得疯狂了吗?!)你可以设置一个简单的对象来保存你需要访问的数据并最小化重新渲染,有点像可怜的人 Redux(也许是时候使用 Redux 了?)。

如果你创建一个类似于以下的类:

type Subscription<T> = (val: T) => void;
type Unsubscribe = () => void;

class SubscribableValue<T> 
    private subscriptions: Subscription<T>[] = [];
    private value: T;

    constructor(val: T) 
        this.value = val;

        this.get = this.get.bind(this);
        this.set = this.set.bind(this);
        this.subscribe = this.subscribe.bind(this);
    

    public get(): T 
        return this._val;
    

    public set(val: T) 
        if (this.value !== val) 
            this.value = val;
            this.subscriptions.forEach(s => 
                s(val)
            );
        
    

    public subscribe(subscription: Subscription<T>): Unsubscriber 
        this.subscriptions.push(subscription);
        return () => 
            this.subscriptions = this.subscriptions.filter(s => s !== subscription);
        ;
    

然后可以创建以下类型的上下文:

type ItemContext = SubscribableValue<Item[]>;

提供组件看起来类似于:

const ItemContextProvider = () => 

    const subscribableValue = useMemo(() => new SubscribableValue<Item[]>([]), []);

    return (
        <ItemContext.Provider value=subscribableValue>
            children
        </ItemContext.Provider>
    )
;

然后您可以根据需要使用一些自定义挂钩来访问该值:

// Get access to actions to add or remove an item.
const useItemContextActions = () => 
    const subscribableValue = useContext(ItemContext);

    const addItem = (item: Item) => subscribableValue.set([...subscribableValue.get(), item]);
    const removeItem = (index: number) => subscribableValue.set(subscribableValue.get().filter((item, i) => i === index));

    return 
        addItem,
        removeItem
    


type Selector = (items: Item[]) => any;

// get access to data stored in the subscribable value.
// can provide a selector which will check if the value has change each "set"
// action before updating the state.
const useItemContextValue = (selector: Selector) => 
    const subscribableValue = useContext(ItemContext);

    const selectorRef = useRef(selector ?? (items: Item[]) => items)

    const [value, setValue] = useState(selectorRef.current(subscribableValue.get()));

    const useEffect(() => 
        const unsubscribe = subscribableValue.subscribe(items => 
            const newValue = selectorRef.current(items);

            if (newValue !== value) 
                setValue(newValue);
            
        )

        return () => 
            unsubscribe();
        ;
    , [value, selectorRef, setValue]);

    return value;

这将允许您使用选择器函数(如 React Redux 的 useSelector 的一个非常基本的版本)减少重新渲染,因为可订阅值(根对象)在其生命周期内永远不会更改引用。

这样做的缺点是您必须管理订阅并始终使用set 函数来更新持有的值,以确保订阅会得到通知。

结论:

不同的人可能会通过多种其他方式来解决此问题,您必须找到一种适合您的确切问题的方式。

如果您的上下文/状态要求范围更大,有第三方库(如 Redux)也可以帮助您。

【讨论】:

【参考方案2】:

这是否意味着对 FooContext 的每次更改都会重新渲染 Foo 组件?

目前 (v17),没有针对 Context API 的救助。检查我的another answer for examples。所以是的,它会总是在上下文变化时重新渲染。

它只需要在有人单击按钮时使用状态,否则从不使用。看起来很浪费。

可以通过拆分上下文提供程序来修复,请参阅上面的相同答案以进行解释。

【讨论】:

以上是关于通过 useFooController/useFooHook 限制使用 useContext 的组件的渲染的主要内容,如果未能解决你的问题,请参考以下文章

下拉框多选框单选框 通过TagHelper绑定数据

酶:测试孩子通过安装渲染失败,但通过浅时通过

java是通过值传递,也就是通过拷贝传递——通过方法操作不同类型的变量加深理解

通过代码进行 Spring 配置与通过注释进行配置

如何理解“不要通过共享内存来通信,而应该通过通信来共享内存”?

通过邮递员通过 API 使用 Rails 主动存储上传文件(.pdf、.jpg 等)? (不通过 Rails 视图)