前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >这些 hook 更优雅的管理你的状态

这些 hook 更优雅的管理你的状态

作者头像
GopalFeng
发布2022-08-01 20:13:03
9360
发布2022-08-01 20:13:03
举报
文章被收录于专栏:前端杂货铺-Gopal

本文是深入浅出 ahooks 源码系列文章的第十二篇,这个系列的目标主要有以下几点:

  • 加深对 React hooks 的理解。
  • 学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。
  • 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。

今天我们来聊聊 ahooks 中那些可以帮助我们更优雅管理我们 state(状态)的那些 hook。一些比较特殊的,比如 cookie/localStorage/sessionStorage,useUrlState等,我们已经单独拿出来细讲了,感兴趣可以看看笔者的历史文章。

useSetState

管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致。

先来了解一下可变数据和不可变数据的含义和区别如下:

  • 可变数据(mutable)即一个数据被创建之后,可以随时进行修改,修改之后会影响到原值。
  • 不可变数据(Immutable) 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。

我们知道,React Function Components 中的 State 是不可变数据。所以我们经常需要写类似如下的代码:

代码语言:javascript
复制
setObj((prev) => ({
  ...prev,
  name: 'Gopal',
  others: {
    ...prev.others,
    age: '27',
  }
}));

通过 useSetState,可以省去对象扩展运算符操作这个步骤,即:

代码语言:javascript
复制
setObj((prev) => ({
  name: 'Gopal',
  others: {
    age: '27',
  }
}));

其内部实现也比较简单,如下所示:

  • 调用设置值方法的时候,会根据传入的值是否为函数。如果是函数,则入参为旧状态,输出新的状态。否则直接作为新状态。这个符合 setState 的使用方法。
  • 使用对象拓展运算符,返回新的对象,保证原有数据不可变。
代码语言:javascript
复制
const useSetState = <S extends Record<string, any>>(
  initialState: S | (() => S),
): [S, SetState<S>] => {
  const [state, setState] = useState<S>(initialState);

  // 合并操作,并返回一个全新的值
  const setMergeState = useCallback((patch) => {
    setState((prevState) => {
      // 新状态
      const newState = isFunction(patch) ? patch(prevState) : patch;
      // 也可以通过类似 Object.assign 的方式合并
      // 对象拓展运算符,返回新的对象,保证原有数据不可变
      return newState ? { ...prevState, ...newState } : prevState;
    });
  }, []);

  return [state, setMergeState];
};

可以看到,其实就是将对象拓展运算符的操作封装到内部。

还有其他更优雅的方式?我们可以使用 use-immer[1]

useImmer(initialState) 非常类似于 useState。该函数返回一个元组,元组的第一个值是当前状态,第二个是 updater 函数,它接受一个 immer producer 函数或一个值作为参数。

使用如下:

代码语言:javascript
复制
const [person, updatePerson] = useImmer({
  name: "Michel",
  age: 33
});

function updateName(name) {
  updatePerson(draft => {
    draft.name = name;
  });
}

function becomeOlder() {
  updatePerson(draft => {
    draft.age++;
  });
}

当向更新函数传递一个函数的时候,draft 参数可以自由地改变,直到 producer 函数结束,所做的改变将是不可变的,并成为下一个状态。这更符合我们的使用习惯,可以通过 draft.xx.yy 的方式更新我们对象的值。

useBoolean 和 useToggle

这两个都是特殊情况下的值管理。

useBoolean,优雅的管理 boolean 状态的 Hook。

useToggle,用于在两个状态值间切换的 Hook。

实际上,useBoolean 又是 useToggle 的一个特殊使用场景。

先看 useToggle。

  • 这里使用了 typescript 函数重载声明入参和出参类型,根据不同的入参会返回不同的结果。比如第一个入参为 boolean 布尔值,则返回一个元组,第一项为 boolean 值,第二个为更新函数。优先级从上到下依次变低。
  • 入参可能有两个值,第一个为默认值(认为是左值),第二个是取反之后的值(认为是右值),可以不传,不传的时候,则直接根据默认值取反 !defaultValue
  • toggle 函数。切换值,也就是上面的左值和右值的转换。
  • set。直接设置值。
  • setLeft。设置默认值(左值)。
  • setRight。如果传入了 reverseValue, 则设置为 reverseValue。否则设置为 defautValue 的取反值。
代码语言:javascript
复制
// TS 函数重载的使用
function useToggle<T = boolean>(): [boolean, Actions<T>];

function useToggle<T>(defaultValue: T): [T, Actions<T>];

function useToggle<T, U>(defaultValue: T, reverseValue: U): [T | U, Actions<T | U>];

function useToggle<D, R>(
  // 默认值
  defaultValue: D = false as unknown as D,
  // 取反
  reverseValue?: R,
) {
  const [state, setState] = useState<D | R>(defaultValue);

  const actions = useMemo(() => {
    const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;

    // 切换 state
    const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
    // 修改 state
    const set = (value: D | R) => setState(value);
    // 设置为 defaultValue
    const setLeft = () => setState(defaultValue);
    // 如果传入了 reverseValue, 则设置为 reverseValue。 否则设置为 defautValue 的反值
    const setRight = () => setState(reverseValueOrigin);

    return {
      toggle,
      set,
      setLeft,
      setRight,
    };
    // useToggle ignore value change
    // }, [defaultValue, reverseValue]);
  }, []);

  return [state, actions];
}

而 useBoolean 是对 useToggle 的一个使用。如下,比较简单,不细说

代码语言:javascript
复制
export default function useBoolean(defaultValue = false): [boolean, Actions] {
  const [state, { toggle, set }] = useToggle(defaultValue);

  const actions: Actions = useMemo(() => {
    const setTrue = () => set(true);
    const setFalse = () => set(false);
    return {
      toggle,
      set: (v) => set(!!v),
      setTrue,
      setFalse,
    };
  }, []);

  return [state, actions];
}

usePrevious

保存上一次状态的 Hook。

其原理,是每次状态变更的时候,比较值有没有发生变化,变更状态:

  • 维护两个状态 prevRef(保存上一次的状态)和 curRef(保存当前状态)。
  • 状态变更的时候,使用 shouldUpdate 判断是否发生变化,默认通过 Object.is 判断。开发者可以自定义 shouldUpdate 函数,并决定什么时候记录上一次状态。
  • 状态发生变化,更新 prevRef 的值为上一个 curRef,并更新 curRef 为当前的状态。
代码语言:javascript
复制
const defaultShouldUpdate = <T>(a?: T, b?: T) => !Object.is(a, b);
function usePrevious<T>(
  state: T,
  shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,
): T | undefined {
  // 使用了 useRef 的特性,一直保持引用不变
  // 保存上一次值
  const prevRef = useRef<T>();
  // 当前值
  const curRef = useRef<T>();

  // 自定义是否更新上一次的值
  if (shouldUpdate(curRef.current, state)) {
    prevRef.current = curRef.current;
    curRef.current = state;
  }

  return prevRef.current;
}

useRafState

只在 requestAnimationFrame callback 时更新 state,一般用于性能优化。

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

假如你的操作是比较频繁的,就可以通过这个 hook 进行性能优化。

  • 重点看 setRafState 方法,它执行的时候,会取消上一次的 setRafState 操作。重新通过 requestAnimationFrame 去控制 setState 的执行时机
  • 另外在页面卸载的时候,会直接取消操作,避免内存泄露
代码语言:javascript
复制
function useRafState<S>(initialState?: S | (() => S)) {
  const ref = useRef(0);
  const [state, setState] = useState(initialState);

  const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
    cancelAnimationFrame(ref.current);
    ref.current = requestAnimationFrame(() => {
      setState(value);
    });
  }, []);

  // unMount 的时候,去除监听
  useUnmount(() => {
    cancelAnimationFrame(ref.current);
  });

  return [state, setRafState] as const;
}

useSafeState

用法与 React.useState 完全一样,但是在组件卸载后异步回调内的 setState 不再执行,避免因组件卸载后更新状态而导致的内存泄漏。

代码如下:

  • 在更新的时候,通过 useUnmountedRef 判断如果组件卸载,则停止更新。
代码语言:javascript
复制
function useSafeState<S>(initialState?: S | (() => S)) {
  // 判断是否卸载
  const unmountedRef = useUnmountedRef();
  const [state, setState] = useState(initialState);
  const setCurrentState = useCallback((currentState) => {
    // 如果组件卸载,则停止更新
    if (unmountedRef.current) return;
    setState(currentState);
  }, []);

  return [state, setCurrentState] as const;
}

useUnmountedRef 这个我们之前提过,简单回顾下,其实就是在 hook 的返回值中标记组件为已卸载。

代码语言:javascript
复制
const useUnmountedRef = () => {
  const unmountedRef = useRef(false);
  useEffect(() => {
    unmountedRef.current = false;
    // 如果已经卸载,则会执行 return 中的逻辑
    return () => {
      unmountedRef.current = true;
    };
  }, []);
  return unmountedRef;
};

useGetState

给 React.useState 增加了一个 getter 方法,以获取当前最新值。

其实现如下:

  • 其实就是通过 useRef 记录最新的 state 的值,并暴露一个 getState 方法获取到最新的。
代码语言:javascript
复制
function useGetState<S>(initialState?: S) {
  const [state, setState] = useState(initialState);
  // useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
  // 使用 useRef 处理 state
  const stateRef = useRef(state);
  stateRef.current = state;
  const getState = useCallback(() => stateRef.current, []);

  return [state, setState, getState];
}

这在某一些情况下,可以避免 React 的闭包陷阱。如官网例子:

代码语言:javascript
复制
const [count, setCount, getCount] = useGetState<number>(0);

useEffect(() => {
  const interval = setInterval(() => {
    console.log('interval count', getCount());
  }, 3000);

  return () => {
    clearInterval(interval);
  };
}, []);

假如这里不使用 getCount(),而是直接使用 count,是获取不到最新的值的。

总结与思考

React 的 function Component 的状态管理还是比较灵活,我们可以针对一些场景进行封装和优化,从而更优雅的管理我们的 state 状态,希望 ahooks 这些封装能对你有所帮助。

参考资料

[1]

use-immer: https://github.com/immerjs/use-immer

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端杂货铺 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • useSetState
  • useBoolean 和 useToggle
  • usePrevious
  • useRafState
  • useSafeState
  • useGetState
  • 总结与思考
    • 参考资料
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档