一个API将React中的useState状态值同步化
为什么要做这样一件事
上一次写React
已经是6年前的事了,直到现在还是没习惯它的响应式属性修改处理。加上新版本的发布,所以稍作改动,让这个响应式用起来变得更加丝滑。
先看一个代码片段
function ExampleComponent() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
console.log("count >>", count); // 这里拿到的值为上一次的
};
return (
<div>
<p>Count: {count}</p>
<button className="the-btn blue" onClick={increment}>Increment</button>
</div>
);
}
注意看打印后面的注释;然后再看下面的场景片段
import { useState } from "react";
/** 是否触底加载更多 */
function onBottom(param: { load: () => void }) {}
let count = 10;
/** 模拟接口请求列表数据 */
function getList() {
count = count + 10;
const list = new Array(10).fill(0).map((_, index) => ({ id: index + count, item: `item-${index + count}` }));
return new Promise<{ list: typeof list }>(function(resolve) {
setTimeout(function() {
resolve({ list });
}, 500)
})
}
/** 列表组件 */
export default function List() {
const [state, setState] = useState({
hasMore: true,
loading: false,
list: new Array(10).fill(0).map((_, index) => ({ id: index, item: `item-${index}` }))
});
onBottom({
async load() {
if (state.loading || !state.hasMore) return;
setState({
...state,
loading: true,
});
const res = await getList()
const clone: typeof state = JSON.parse(JSON.stringify(state));
clone.list = clone.list.concat(res.list);
clone.loading = false;
clone.hasMore = clone.list.length < 100;
setState(clone);
}
})
return (
<section>
{
state.list.map(item => (
<div
style={{ height: "20vh", backgroundColor: "orange", marginBottom: "20px" }}
key={item.id}
>{item.item}</div>
))
}
<div>
{
state.loading ? "加载中..." : (!state.hasMore ? "数据已全部加载完" : "滚动到底部加载更多")
}
</div>
</section>
)
}
这个时候上面的代码就会有问题了,因为在onBottom
函数的实现中,load
的执行时机是不确定的,这个时候如果用state.loading || state.hasMore
去判断是否发起请求则产生意外的行为,理由和上面的代码片段注释所描述那样;在修改后马上获取到的值是上一次的,所以导致逻辑出错,那么这个时候通常的做法就是单独声明变量来记录同步修改的值,从而和响应式变量分开操作。
实现核心API
我不想用第三方库(懒得看各种库的文档),同时还想继续使用useState
,有没有一种可替代的方案或者基于useState
封装的方案呢?没错,就是使用Object.defineProperty
,因为要保证修改的值在书写的代码片段中同步获取到最新值,同时还要为响应式,所以可以使用该API进行绑定操作;在getter
和setter
中只需做两件事就可以实现同步读取和修改,get的时候读取的是绑定到的值,该值始终是同步修改,而set则修改同步值,还调用了setState
去修改响应式的值,这样就实现了“既要又要”了
实现代码片段
用法我只使用一种,就是基于对象,其他类型不需要处理,这样就简单很多了;isType()
和DeepReadonly
请看 type.d.ts,utils.ts 这里不做详细说明。
/**
* 返回两个对象,`state`为新的响应对象,可以直接通过修改值而自动进行`setState`,并且它在修改后获取到的值是实时同步的; `ref`则为`[ref, setRef] = useState()`中的第一个值(原始响应对象)。
* @param target
*/
export function useObjectState<T extends object>(target: T) {
const [value, setValue] = useState(target);
/**
* 设置对象监听
* @param target
*/
function setData(target: T) {
for (const key in target) {
let value = target[key];
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
// console.log(`set ${key}`);
value = newValue;
setValue(JSON.parse(JSON.stringify(setRef)));
isType(target[key], "object") && setData(newValue);
}
});
isType(target[key], "object") && setData(target[key] as T);
}
}
const setRef = JSON.parse(JSON.stringify(value)) as T;
setData(setRef);
return {
state: setRef,
ref: value as DeepReadonly<T>,
}
}
这里返回两个对象的理由是:state
作为同步响应数据对象,可以修改和获取值,通常用到的只有该值;而ref
则是提供一个原始响应对象,在一些特殊场景会需要获取上一次的值,行为和useState
的第一个参数行为一致。
注意这两个对象值写在节点标签上的效果是一致的,看下面代码片段
import { useObjectState } from "@/hooks/common";
interface TheState {
id: number;
loading: boolean;
info: {
size: number;
desc: string;
before?: boolean
}
}
export default function About() {
const { state, ref } = useObjectState<TheState>({
id: 0,
loading: false,
info: {
size: 10,
desc: "this is a object value"
}
});
function countId() {
state.id++;
console.log("原始响应数据 >>", ref.id, "新的响应数据 >>", state.id);
}
function changeInfo() {
if (state.info.before) {
state.info = {
size: 999,
desc: "remove before"
}
} else {
state.info = {
size: 17,
desc: "the ref.info is changed !",
before: true
}
}
// console.log(ref);
// console.log(ref.info, state.info);
}
function changeDesc() {
state.info.desc = "desc is changed !";
}
return (
<div>
<button className="the-btn blue" onClick={countId}>ref.id: { ref.id }; state.id: { state.id }</button>
<br /><br />
<div>
<button className="the-btn green" onClick={changeDesc}>修改 desc 值</button>
<button className="the-btn green" onClick={changeInfo}>修改整个 info</button>
</div>
<br /><br />
<div>{JSON.stringify(state, null, 4)}</div>
</div>
)
}
改造之前的代码
现在不再需要写烦人的setState
了!
/** 列表组件 */
export default function List() {
const { state } = useObjectState({
hasMore: true,
loading: false,
list: new Array(10).fill(0).map((_, index) => ({ id: index, item: `item-${index}` }))
});
onBottom({
async load() {
if (state.loading || !state.hasMore) return;
state.loading = true;
const res = await getList()
state.list = state.list.concat(res.list);
state.loading = false;
state.hasMore = state.list.length < 100;
}
})
return (
<section>
{
state.list.map(item => (
<div
style={{ height: "20vh", backgroundColor: "orange", marginBottom: "20px" }}
key={item.id}
>{item.item}</div>
))
}
<div>
{
state.loading ? "加载中..." : (!state.hasMore ? "数据已全部加载完" : "滚动到底部加载更多")
}
</div>
</section>
)
}
转载自:https://juejin.cn/post/7392898777047941139