Reactjs 通过useFooController/useFooHook限制使用useContext的组件的呈现

Reactjs 通过useFooController/useFooHook限制使用useContext的组件的呈现,reactjs,react-hooks,use-context,Reactjs,React Hooks,Use Context,我正在为组件使用自定义挂钩,自定义挂钩使用自定义上下文。考虑 /*假设FooContext有{state:FooState,dispatch:()=>any}*/ const useFoo=()=>{ const{state,dispatch}=useContext(FooContextContext) 返回{apiCallable:()=>apiCall(状态)} } 常量Foo=()=>{ const{apiCallable}=useFoo() 返回( ) } 许多组件将从其他组件(表单输

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

/*假设FooContext有{state:FooState,dispatch:()=>any}*/
const useFoo=()=>{
const{state,dispatch}=useContext(FooContextContext)
返回{apiCallable:()=>apiCall(状态)}
}
常量Foo=()=>{
const{apiCallable}=useFoo()
返回(
)
}
许多组件将从其他组件(表单输入等)更改
FooState
。在我看来,它就像
Foo
使用
useFoo
,它使用
FooStateContext
中的
state
。这是否意味着对
FooContext
的每次更改都将重新呈现
Foo
组件?它只需要在有人单击按钮时使用state,而不需要其他方式。看起来很浪费

我想
useCallback
就是专门针对这个问题的,所以我想
return{apiCallable:useCallback(()=>apiCall(state))}
但是我需要添加
[state]
作为
useCallback
的第二个参数。这意味着只要状态更新,回调就会被重新呈现,所以我又回到了同一个问题上,对吗

这是我第一次做这样的定制钩子。理解
useCallback
有实际困难。我如何实现我想要的


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

既然您在问题中提供了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>
    )
};
这将允许您使用选择器函数(如React-Redux的极其基本的版本
useSelector
)减少重新渲染,因为可下标值(根对象)在其生命周期内永远不会更改引用

这样做的缺点是,您必须管理订阅,并始终使用
set
功能更新保留值,以确保订阅将得到通知

结论:

不同的人可能会有很多其他的方法来解决这个问题,你必须找到一个适合你的问题

如果您的上下文/状态需求范围更大,那么第三方库(如Redux)也可以帮助您实现这一点

这是否意味着对FooContext的每次更改都将重新呈现Foo组件

目前(
v17
),上下文API没有紧急援助。检查我的电脑。因此,是的,它将始终在上下文更改时重新播放

它只需要在有人单击按钮时使用state,而不需要其他方式。看起来很浪费


可以通过拆分上下文提供程序来修复此问题,请参见上面的相同答案以获取解释。

这是react-redux在尝试提前采用react-context来执行存储状态时遇到的问题。钩子不够聪明,无法知道您正在使用的上下文的哪些部分,因此,如果上下文的任何部分发生更改,则必须重新启动组件。如果上下文不太大,则可以考虑将其拆分为两个,一个用于上下文的上下文,另一个是用于更改状态的上下文。通过这种方式,提供组件可以处理这两个上下文的交互方式,并且您可以在不重新引发的情况下执行操作。您是否尝试过使用React.memo来记忆
Foo
组件?
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;
}