在React中,是否有一种方法可以将可变类实例对象存储在状态中?

21

React状态不应直接被改变。但是,如果状态是一个应该使用自己的方法可变的类实例。有没有其他方法而不必深度克隆并使用新参数重新实例化对象?

一般而言:在维护父组件状态(通过props/context传递)的属性的同时,在子组件中使用创建的类对象的React方式是什么?

示例类

class Car{
    constructor(data){
        this.data = data
    }
    changeColor = (newcolor) => this.data.color = newcolor
}

样例父组件

const App = ({data}) => {
 const [car, setCar] = useState(new Car(data))
  return (
    <div>
      <CarViewer car={car} />
    </div>
  );
};

样品子组件

const CarViewer = ({car}) => {
  return (
    <div>
      The color is: {car.data.color}
    <button onClick={()=>car.changeColor("blue")}>Change color to blue </button>
    </div>
  );
};

1
这的实际例子会是什么? - Prateek Thapa
3
我认为这对于任何使用状态类实例中的一组方法和相应属性的用例都是实用的。 例如:一个具有内置缓存功能并具有访问window.location.state的数据获取类。它通过上下文/props传递给其他使用此实例的组件。我认为OOP可以是一种很好的方法(无论是类还是原型注释),以在您的应用程序中带来秩序,并且我在思考是否可能在“react方式”中使用它,当实例应该是有状态的时候。 - elMeroMero
我发现这个链接很有用 https://dev59.com/wlQK5IYBdhLWcg3wdPiI - Mayowa Daniel
2个回答

10

我已经困扰了几年,关于这个问题,以下是我的理解。我不断地完善这些想法,因此请持保留态度,如果感觉错误或不清楚,请毫不犹豫地进行评论或澄清。同时,像任何通用的或基于模式的解决方案一样,它并不总是值得付出成本或适合某种情况。

组件仅涉及数据下传和事件上传

这里的基本前提是,任何给定组件的实现代码应仅使用来自其他代码的纯数据和回调来工作。所谓“来自其他代码”,指的是prop、钩子的输出或纯计算的结果。所谓“纯数据”,是指任何可变实例都应该被“投影”为不可变的子集,以供组件消耗,或者至少被视为不透明,使得可变的细节被忽略。

简而言之:甚至不要让组件“知道”可变模型的存在!让其他事物为它们做点简单的事情。组件应专注于呈现给定的数据,并通过回调向上报告事件。

对于你的示例,这意味着既不应该让 App 也不应该让 CarViewer 直接操作 Car

使用Hooks处理模型

那么,基于这个规则,像 Car 这样的模型如何成为组件可以使用的东西呢?Props和纯函数无法解决问题,只能将其推回;毕竟,数据仍然必须来自某处。这只留下了hooks。

但是,“使用hook”过于模糊,没有实际意义。我们需要知道a)首先如何获得您手头的模型,b)如何将其转换为组件允许使用的内容。

维护模型实例

模型从哪里来,我们如何保留它?归根结底,这始于业务逻辑:使用构造函数或工厂函数构建新模型,或从一些外部提供者(如DI容器)中提取共享实例。在你的例子中,这是 new Car(data)

一旦实例可用,使用 useRef 存储它。不要将其存储在 useState 中,因为您不会直接渲染该值,并且将需要以其他方式考虑更改。

确保钩子恰好在需要时创建(并清理!)模型。具体细节因用例而异,但此处是有关管理非状态实例的提示,考虑到计划中的更改react挂载行为。

对于您的简单示例,我只需创建一个如下的钩子:

function useCar(initialData) {
    const carRef = useRef();
    if (!carRef.current) {
        carRef.current = new Car(initialData);
    }
    return carRef.current;
}

维护模型预测

好了,现在您最终拥有了一个包含访问详情的模型实例。组件如何在不违反主要规则的情况下使用它呢?一个钩子必须将相关的模型值和函数投影到不可变的计算子值中,并将其存储在状态中。诀窍在于每当投影值发生更改时,需要使用重新计算的投影更新此状态。这里有两个常用的选项。

第一种完整的选项是让您的模型发布/发出更改事件,以便您可以监听并在每次更改发生时重新计算投影。我喜欢使用 rxjs 来实现这一点。不幸的是,这需要将更改事件考虑在内来设计模型,但如果更改可能来自于视图层次之外(例如来自于推送通知),那么这可能是必要的。

如果无法依赖模型通知您有关更改的信息,则必须拦截自己的更改。为此,您可以包装已投影更改的函数,以便它们直接在更新状态后更新状态。下面是示例代码:

function useCarProjection(car) {
    const [projection, setProjection] = useState(projectCar(car));
    return {
        ...projection,
        // each change function modifies the car AND updates the projection
        changeColor: (newColor) => {
            car.changeColor(newColor);
            setProjection(projectCar(car));
        },
    };
}

function projectCar(car) {
    // include any data your components need
    return { color: car.data.color };
}

值得注意的是,我将这个钩子作为参数传递汽车,以便它不必直接访问或存储汽车。您需要第三个钩子来组合这两个钩子,或者您可以将两个钩子合并为一个钩子,以简化情况。


1
这可能更适合我的任务。我的任务是处理身份验证和登录。我的项目要求我提供一个“主体”(实际人员),该主体可能有一个或多个“角色”实例附加到它上面。任何角色都可以是“已登录”或“已注销”。这个模型很容易建模为实例和类。我花了几周时间尝试在React中解决useState,useEffect和useCallback的行为。我将尝试这种方法,看看它是否有所帮助。 - Tom Stambaugh
@TomStambaugh,这听起来像是从复杂的模型到基于视图的简化状态集进行“投影”的好理由。每当角色情况发生变化(signIn/Out)时,您可以发布一种“事件”,并让组件监听该事件。这可以通过rxjs实现,甚至只需在模型保留要在更改时调用的函数列表即可。 - TheRubberDuck

9
我认为你需要改变存储类在 React 状态中的思维模式,并尝试使用更符合 React 的模式,如下所示:
const CarViewer = ({ carData, changeColor }) => {
  return (
    <div>
      The color is: {carData.color}
      <button onClick={() => changeColor("blue")}>Change color to blue</button>
    </div>
  );
};

const App = ({ data }) => {
  const [carData, setCarData] = useState(data);

  const changeColor = (newcolor) =>
    setCarData((data) => ({ ...data, color: newcolor }));

  return (
    <div>
      <CarViewer carData={carData} changeColor={changeColor} />
    </div>
  );
};

编辑:根据您的评论,我认为您需要像这样的自定义钩子:


const App = ({ data }) => {
  const { carData, changeColor } = useCar(data);
  
  return (
    <div>
      <CarViewer carData={carData} changeColor={changeColor} />
    </div>
  );
};

function useCar(defaultData = {}) {
  const [carData, setCarData] = useState(defaultData);

  const changeColor = (newcolor) =>
    setCarData((data) => ({ ...data, color: newcolor }));

  return {
    carData,
    changeColor,
    //... your other methods
  };
}

3
想象一下,类"Car"是一个更加复杂的类,有数十个方法和自己的属性,这个类会被在其他地方反复使用。 - elMeroMero
1
@elMeroMero,这正是“自定义钩子”发挥作用的地方。 - Taghi Khavari
3
谢谢,我喜欢这个方法。但是我还在思考,有没有一种“react方式”来处理有状态的类实例/使用类语法,而不必在每个更改其属性的类方法中使用setState处理程序? - elMeroMero
但是,如果我直接在我的组件中使用它,它的状态数据将受到该组件的限制。我希望它的状态能够被所有使用它的组件共享。这就是为什么我首先将它放入状态中的原因。 - elMeroMero
6
“在处理有状态的类实例/使用类语法时,是否存在“React方式”?”简单回答是“没有”,因为React无法察觉Car对象的内部变化并在必要时重新渲染CarViewer。当你调用useState(new Car(data))时,你的状态只是存储对那个Car对象的引用。只要引用保持不变,组件就不会更新。因此,你需要让Car的方法返回一个新的实例,然后使用新实例调用setState - Linda Paiste
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接