likes
comments
collection
share

一个API将React中的useState状态值同步化

作者站长头像
站长
· 阅读数 49

一个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进行绑定操作;在gettersetter中只需做两件事就可以实现同步读取和修改,get的时候读取的是绑定到的值,该值始终是同步修改,而set则修改同步值,还调用了setState去修改响应式的值,这样就实现了“既要又要”了

实现代码片段

用法我只使用一种,就是基于对象,其他类型不需要处理,这样就简单很多了;isType()DeepReadonly请看 type.d.tsutils.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
评论
请登录