elM*_*ero 7 javascript reactjs react-hooks use-state
不应直接改变 React 状态。但是,如果状态是一个类的实例,它应该是可变的,它有自己的方法。除了必须深度克隆并使用新参数重新实例化对象之外,还有其他方法吗?
一般而言:在父组件中创建的类对象在子组件中使用,同时在父状态(通过道具/上下文传递)保持其属性的反应方式是什么?
示例类
class Car{
constructor(data){
this.data = data
}
changeColor = (newcolor) => this.data.color = newcolor
}
Run Code Online (Sandbox Code Playgroud)
示例父组件
const App = ({data}) => {
const [car, setCar] = useState(new Car(data))
return (
<div>
<CarViewer car={car} />
</div>
);
};
Run Code Online (Sandbox Code Playgroud)
示例子组件
const CarViewer = ({car}) => {
return (
<div>
The color is: {car.data.color}
<button onClick={()=>car.changeColor("blue")}>Change color to blue </button>
</div>
);
};
Run Code Online (Sandbox Code Playgroud)
The*_*uck 13
几年来我一直在为同样的问题而苦苦挣扎,这就是我想到的。我不断完善这些想法,所以要持保留态度,不要害怕批评或澄清任何感觉错误或不清楚的地方。此外,与任何通用或基于模式的解决方案一样,它并不总是值得付出成本或适合具体情况。
这里的基本前提是任何给定组件的实现代码都应该专门根据来自其他代码的纯数据和回调来工作。我所说的“来自其他代码”是指 prop、hook 的输出或纯计算的结果。我所说的“纯数据”是指任何可变实例都应该“投影”到一个不可变的子集中以供组件使用,或者至少被视为不透明,以便忽略可变细节。
简而言之:甚至不要让组件“知道”可变模型!让其他事情让他们变得简单。组件应该专注于渲染提供给它的数据并通过回调报告事件。
对于您的示例,这意味着既不App也不CarViewer应该直接扰乱 a Car。
那么根据这个规则,模型如何Car变成可供组件使用的东西呢?Props 和纯函数并不能解决问题,只能将问题推倒;毕竟,数据仍然必须来自某个地方。那就只剩下钩子了。
但“使用钩子”太模糊了,没有意义。我们需要知道a)如何首先获得模型,b)如何将其变成组件可以使用的东西。
模型从何而来,我们如何保存它?最终,它从业务逻辑开始:使用构造函数或工厂函数构建新模型,或者从某些外部提供程序(例如 DI 容器)提取共享实例。在您的示例中,这是new Car(data).
一旦实例可用,将其存储为useRef. 不要将其存储,useState因为您没有直接渲染该值,并且无论如何都需要以其他方式考虑更改。
确保钩子准确地在需要时创建(并清理!)模型。详细信息根据用例而有所不同,但这里有一个线程讨论根据计划的更改来管理非状态实例以反应安装行为。
对于您的简单示例,我只需创建一个像这样的钩子:
function useCar(initialData) {
const carRef = useRef();
if (!carRef.current) {
carRef.current = new Car(initialData);
}
return carRef.current;
}
Run Code Online (Sandbox Code Playgroud)
好的,您终于有了一个模型实例,其中的访问详细信息封装在一个钩子中。组件如何在不违反主要规则的情况下使用它?钩子必须将相关模型值和函数投影到不可变的计算子值中并将其存储在状态中。诀窍在于,每当投影值发生变化时,就需要使用重新计算的投影来更新此状态。有两个通用选项。
完整的选项是让您的模型发布/发出更改事件,您可以在每次发生时监听并重新计算投影。我喜欢用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 };
}
Run Code Online (Sandbox Code Playgroud)
值得注意的是,我让这个钩子将汽车作为参数,这样它就不会关心直接访问或存储它。您需要第三个钩子来将两者组合起来,或者您可以将两者组合成一个钩子以实现更简单的情况。
我认为你需要做的是改变你在反应状态中存储类的心理模型,并尝试像这样的不同模型,这是更多的反应方式:
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>
);
};
Run Code Online (Sandbox Code Playgroud)
编辑:根据您的评论,我认为您需要的是这样的自定义钩子:
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
};
}
Run Code Online (Sandbox Code Playgroud)