This article is the twelfth of a series of articles that explain the source code of ahoos in simple language, which has been organized into document- address . I think it's not bad, give a star to support it, thanks.

Today we're going to talk about those hooks in ahoos that can help us manage our state more gracefully. Some of the more special ones, such as cookie/localStorage/sessionStorage, useUrlState, etc., we have already taken them out separately. If you are interested, you can read the author's historical articles.

useSetState

Hooks that manage the state of the object type are basically the same as this.setState of the class component.

Let's first understand the meaning and difference between mutable data and immutable data as follows:

  • Mutable data means that after a data is created, it can be modified at any time, and the modification will affect the original value.
  • Immutable data is data that, once created, cannot be changed. Any modification or addition or deletion of the ---0f0df247ce52c341847daaca2ca95750 Immutable object returns a new Immutable object.

We know that State in React Function Components is immutable data. So we often need to write code like the following:

 setObj((prev) => ({
  ...prev,
  name: 'Gopal',
  others: {
    ...prev.others,
    age: '27',
  }
}));

With useSetState, the operation of the object spread operator can be omitted, namely:

 setObj((prev) => ({
  name: 'Gopal',
  others: {
    age: '27',
  }
}));

Its internal implementation is also relatively simple, as follows:

  • When calling the set value method, it will be based on whether the incoming value is a function. If it is a function, the input parameter is the old state and the output is the new state. Otherwise directly as the new state. This is in line with the usage of setState.
  • Use the object spread operator to return a new object, ensuring that the original data is immutable.
 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];
};

As you can see, it actually encapsulates the operation of the object expansion operator inside.

Is there any other more elegant way? We can use use-immer

useImmer(initialState) is very similar to useState . The function returns a tuple, the first value of the tuple is the current state, and the second is the updater function, which accepts a immer producer function or a value as an argument.

Use as follows:

 const [person, updatePerson] = useImmer({
  name: "Michel",
  age: 33
});

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

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

When passing a function to the update function, the draft parameters can be changed freely until the producer function finishes, the changes made will be immutable and become the next state. This is more in line with our usage habits, and we can update the value of our object by draft.xx.yy .

useBoolean and useToggle

Both of these are special-case value management.

useBoolean, an elegant Hook for managing boolean state.

useToggle, a Hook to toggle between two state values.

In fact, useBoolean is a special use case of useToggle.

Look at useToggle first.

  • Here, the typescript function overloading is used to declare the input and output parameter types, and different results will be returned according to different input parameters. For example, the first input parameter is a boolean boolean value, then a tuple is returned, the first item is the boolean value, and the second is the update function. Priority decreases from top to bottom.
  • The input parameter may have two values, the first is the default value (considered to be an lvalue), and the second is the value after negation (considered to be an rvalue), which can be omitted. Invert the value !defaultValue .
  • toggle function. Switch value, that is, the conversion of the above lvalue and rvalue.
  • set. Set the value directly.
  • setLeft. Set the default value (lvalue).
  • setRight. If reverseValue is passed in, it is set to reverseValue. Otherwise set to the negated value of defautValue.
 // 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];
}

And useBoolean is a use of useToggle. As follows, relatively simple, not elaborate

 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 that saves the last state.

The principle is that every time the state changes, whether the comparison value has changed, change the state:

  • Maintain two states prevRef (save the last state) and curRef (save the current state).
  • When the state changes, use shouldUpdate to judge whether there is a change, and the default is Object.is to judge. Developers can customize the shouldUpdate function and decide when to record the last status.
  • When the state changes, update the value of prevRef to the previous curRef, and update curRef to the current state.
 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

Only update state when requestAnimationFrame callback, generally used for performance optimization.

window.requestAnimationFrame() Tell the browser that you want to perform an animation, and ask the browser to call the specified callback function to update the animation before the next repaint. This method needs to pass in a callback function as a parameter, the callback function will be executed before the browser's next repaint.

If your operations are frequent, you can use this hook for performance optimization.

  • Focus on the setRafState method. When it is executed, it will cancel the last setRafState operation. Re-control the execution timing of setState through requestAnimationFrame .
  • In addition , when the page is unloaded, the operation will be canceled directly to avoid memory leaks .
 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

The usage is exactly the same as React.useState, but the setState in the asynchronous callback is no longer executed after the component is unloaded, avoiding memory leaks caused by updating the state after the component is unloaded.

code show as below:

  • When updating, it is judged by useUnmountedRef that if the component is unmounted, the update will be stopped.
 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;
}

We mentioned useUnmountedRef before, but to briefly review, it actually marks the component as unmounted in the return value of the hook.

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

useGetState

Added a getter method to React.useState to get the current latest value.

It is implemented as follows:

  • In fact, it records the latest state value through useRef, and exposes a getState method to get the latest.
 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];
}

This, in some cases, avoids React's closure trap. Such as the official website example:

 const [count, setCount, getCount] = useGetState<number>(0);

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

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

If getCount() is not used here, but count is used directly, the latest value cannot be obtained.

Summary and thinking

The state management of React's function Component is relatively flexible. We can encapsulate and optimize some scenarios to manage our state more elegantly. I hope these encapsulations of ahoos can help you.

This article has been included in the personal blog , welcome to pay attention~


Gopal
366 声望77 粉丝