状态对象发生变化时更新上下文

Posted

技术标签:

【中文标题】状态对象发生变化时更新上下文【英文标题】:Update context when state object mutates 【发布时间】:2020-01-27 07:20:55 【问题描述】:

我有一个PageContext,它将User 对象的状态保存为数组。每个User 对象都包含一个ScheduledPost 对象,当用户决定添加新帖子时,该对象会发生变化。我不知道如何在我的PageContext 发生更新时触发更新(我想避免forceUpdate() 调用)。我需要以某种方式收到通知,以便重新渲染帖子、维护计时器等。

请看代码:

class User 
    name: string;
    createTime: number;
    scheduledPosts: ScheduledPost[] = [];

    /*
     * Creates a new scheduled post
     */
    public createScheduledPost(title : string, content : string, date : number): void 
        this.scheduledPosts.push(Object.assign(new ScheduledPost(), 
            title,
            content,
            date
        ));
    


class ScheduledPost 
    title: string;
    content: string;
    date: number;

    public load(): void 
        // Create timers etc.
    

    public publish(): void 
        // Publish post
    


// PageContext/index.tsx
export default React.createContext(
    users: [],
    editingUser: null,
    setEditingUser: (user: User) => 
);

// PageContextProvider.tsx
const PageContextProvider: React.FC = props => 
    const [users, setUsers] = useState<User[]>([]);
    const [editingUser, setEditingUser] = useState<User>(null);

    // Load users
    useEffect(() => 
        db.getUsers()
            .then(result => setUsers(result));
    , []);

    return (
        <PageContext.Provider value=
            users,
            editingUser,
            setEditingUser
        >
            props.children
        </PageContext.Provider>
    );
;

我想要实现的是,当使用 useContext 钩子使用我的提供程序时:

const ctx = useContext(PageContext);

我想从任何组件创建一个时间表帖子,如下所示:

// Schedule post (1 hour)
ctx.editingUser.createScheduledPost("My post title", "The post content", (new Date).getTime() + 1 * 60 * 60);

但是,这不起作用,因为 React 不知道 User 属性刚刚发生了变异。


问题:

如何让 React 收到任何 User 对象实例中的更改的通知?有什么方法可以正确解决(不包括 forceUpdate)? 我做得对吗?我是 React 新手,我觉得我在这里使用的结构很麻烦而且不正确。

【问题讨论】:

你能提供一个可行的例子吗?喜欢codesanbox? 使用Redux并将用户存储为对象(可序列化)。 【参考方案1】:

用户在哪里发生变异?如果您将它们存储在显示的状态中,则应检测到更改。但是,如果您使用 User 类中内置的方法让它们自己 directly update,那么 React 将不会接受它们。你需要在你的状态下更新整个 users 数组,以确保 React 可以响应更改。

如果不知道您当前在何处/如何更新用户,很难给出更具体的示例,但一般化的突变可能会像这样(如果需要,您仍然可以使用类方法):

const newUsers = Array.from(users); // Deep copy users array to prevent direct state mutation.
newUsers[someIndex].createScheduledPost(myTitle, myContent, myDate);
setUsers(newUsers); // Calling the setX function tied to your useState call will automatically trigger updates/re-renders for all (unless otherwise specified) components/operations that depend on it

【讨论】:

但是在这种情况下更新数组是没有意义的。这真的是每个人在 React 中的方式吗?这违背了性能如此糟糕。有一些 C# 背景和基于实例的对象,这变得很麻烦。你能举个例子说明我应该如何添加一个预定的帖子,这样每个使用上下文的组件都会收到更新的数组吗? 在这种情况下,我同意它来自像 C# 这样的语言可能是反直觉的,并且不如单一更新或拼接等性能好。但是,是的,这是一个基本原则React 的:存储在 state 中的数据必须用 setState (或在钩子 setX 中)更新,以便 React 可以正确地“响应”更改。它还提供了大量的性能benefits,并且渲染 DOM 的复杂性稍微改变了演算,而不是后端的静态数据对象。 我将更新我的答案,说明如何做到这一点,但要回答您的问题:用户数组将在您直接从状态下传递时在您的上下文中自动更新,前提是您正在使用setUsers 函数对其进行更新。 是的,如果你能详细说明这个update the entire users array in your state to make sure React can respond to the changes,那将是惊人的987654331@ 类不是 React 组件,我不能在该类中使用 useState 挂钩。也许通过适当的订阅事件可以实现某种存储? 我添加了一个通用示例,说明如何使用类方法更新 users 状态,但您可以使用克隆数组在第一步和第三步之间执行几乎任何操作。 【参考方案2】:

在 React 中重新渲染是通过在组件内调用 setState(或通过使用 hooks,但关键是您需要调用特定方法)或通过更改组件 props 引起的。这意味着您的状态的手动突变永远不会导致重新渲染 - 即使您有简单的组件并调用了

this.state.something = somethingElse;

不会发生重新渲染。同样适用于上下文。

对于您的情况,这意味着您不应改变 editingUser 状态,而是调用 setEditingUser 更改用户状态,例如:

const user =  ...ctx.editingUser ;
user.createScheduledPost("My post title", "The post content", (new Date).getTime() + 1 * 60 * 60);
ctx.setEditingUser(user);

我不确定您的内部结构,但如果同一用户也在 users 数组中,那么您需要通过调用 setUsers 方法来更新该部分状态,在该方法中您维护整个数组并且仅更新更改数据的单个用户 - 如果是这种情况,那么我会考虑重组应用程序,因为对于这种简单的状态更改,它已经变得复杂了。您还应该考虑使用redux、mobx 或其他一些状态管理库来代替反应上下文(我的个人建议)。

编辑

请你看一下这个:

在典型的 React 应用程序中,数据是自顶向下传递的(父级 child) 通过道具,但这对于某些类型的 许多人需要的道具(例如区域设置偏好,UI主题) 应用程序中的组件。上下文提供了一种共享方式 组件之间的此类值,而无需显式传递 贯穿树的每一层的道具。

如您所见,React 团队建议对许多组件中所需的一些全局首选项使用上下文。使用上下文的主要问题(在我看来)是您不编写 natural react 组件 - 它们不通过 props 接收依赖数据,而是从上下文 api 本身接收数据。这意味着如果不集成应用程序的上下文部分,您将无法重用您的组件。

虽然 redux 有类似的将状态保存在一个地方的概念,但它仍然通过 props 将该状态(及其更改)传播给组件,使您的组件不依赖于 redux、上下文或其他任何东西。

您可以坚持响应上下文并让整个应用程序使用它,但我只是说这样做不是最佳做法。

【讨论】:

以上是关于状态对象发生变化时更新上下文的主要内容,如果未能解决你的问题,请参考以下文章

上下文 api 状态发生变化,但在刷新浏览器之前不会反映

尽管状态发生了变化,但自定义钩子不会触发组件重新渲染

如果它们的状态在 React useEffect 中发生变化,如何区分上下文值

SwiftUI上下文菜单(Context Menu)内容不随对象状态刷新的解决

当 ref 的值发生变化时,Vue 不会更新模板 Vue 3

使用 bloc 更改屏幕