effect 副作用相关

98 阅读3分钟

useUpdateEffect

useUpdateEffect 用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行。

import { useEffect, useRef } from 'react';

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isMounted = useRef(false);

  // 首次渲染后标记为已挂载
  useEffect(() => {
    isMounted.current = true;
    
    // 组件卸载时重置标志
    return () => {
      isMounted.current = false;
    };
  }, []);

  // 实际的 effect,只有在已挂载且依赖项变化时才执行
  useEffect(() => {
    if (isMounted.current) {
      return effect();
    }
  }, deps);
};

export default useUpdateEffect;

useUpdateLayoutEffect

useUpdateLayoutEffect 用法等同于 useLayoutEffect,但是会忽略首次执行,只在依赖更新时执行。

import { useLayoutEffect, useRef } from 'react';

const useUpdateLayoutEffect: typeof useLayoutEffect = (effect, deps) => {
  const isMounted = useRef(false);

  // 首次渲染后标记为已挂载
  useLayoutEffect(() => {
    isMounted.current = true;
    
    // 组件卸载时重置标志
    return () => {
      isMounted.current = false;
    };
  }, []);

  // 实际的 layout effect,只有在已挂载且依赖项变化时才执行
  useLayoutEffect(() => {
    if (isMounted.current) {
      return effect();
    }
  }, deps);
};

export default useUpdateLayoutEffect;

// 使用场景:DOM 同步测量和调整
import { useUpdateLayoutEffect } from 'ahooks';

function ResponsiveComponent({ items }) {
  const containerRef = useRef(null);
  
  // 需要在 DOM 更新后立即测量并调整样式
  useUpdateLayoutEffect(() => {
    const container = containerRef.current;
    if (container) {
      // 测量容器尺寸
      const { width } = container.getBoundingClientRect();
      
      // 根据宽度调整子元素样式(需同步执行避免闪烁)
      const children = container.querySelectorAll('.item');
      children.forEach(child => {
        child.style.flexBasis = width > 600 ? '50%' : '100%';
      });
    }
  }, [items]);
  
  return (
    <div ref={containerRef}>
      {items.map(item => (
        <div key={item.id} className="item">
          {item.content}
        </div>
      ))}
    </div>
  );
}

useAsyncEffect

useEffect 支持异步函数

import { useEffect, useRef } from 'react';

const useAsyncEffect = (
  effect: (isCanceled: () => boolean) => Promise<void | (() => void)>,
  deps?: React.DependencyList
) => {
  const isCanceledRef = useRef(false);
  
  useEffect(() => {
    isCanceledRef.current = false;
    
    // 执行异步效应
    const result = effect(() => isCanceledRef.current);
    
    // 返回清理函数
    return () => {
      isCanceledRef.current = true;
      
      // 如果 effect 返回了 Promise,处理其结果
      if (result instanceof Promise) {
        result.then(cleanup => {
          if (cleanup && !isCanceledRef.current) {
            cleanup();
          }
        });
      }
    };
  }, deps);
};

export default useAsyncEffect;

// 使用场景:异步数据获取
import { useAsyncEffect } from 'ahooks';
import { useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useAsyncEffect(async (isCanceled) => {
    setLoading(true);
    
    try {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      
      // 检查是否已取消,避免在组件卸载后设置状态
      if (!isCanceled()) {
        setUser(userData);
        setLoading(false);
      }
    } catch (error) {
      if (!isCanceled()) {
        console.error('Failed to fetch user:', error);
        setLoading(false);
      }
    }
  }, [userId]);
  
  if (loading) return <div>Loading...</div>;
  return <div>Hello, {user?.name}!</div>;
}

useDebounceFn

用来处理防抖函数的 Hook

import { useMemo, useRef } from 'react';
import debounce from 'lodash/debounce';
import type { DebounceSettings } from 'lodash';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';

export interface DebounceOptions extends DebounceSettings {
  wait?: number;
}

type Fn = (...args: any[]) => any;

function useDebounceFn<T extends Fn>(fn: T, options?: DebounceOptions) {
  const fnRef = useLatest(fn);
  
  const wait = options?.wait ?? 1000;

  const debouncedFn = useMemo(
    () =>
      debounce(
        ((...args: any[]) => {
          return fnRef.current(...args);
        }) as T,
        wait,
        options,
      ),
    [],
  );

  useUnmount(() => {
    debouncedFn.cancel();
  });

  return [debouncedFn, debouncedFn.cancel, debouncedFn.flush] as const;
}

export default useDebounceFn;

// 基本用法
const [debouncedFn, cancel, flush] = useDebounceFn(
  (value) => {
    console.log('防抖执行:', value);
  },
  { wait: 500 }
);

// 输入防抖
const handleChange = (e) => {
  debouncedFn(e.target.value);
};

// 手动取消
const handleCancel = () => {
  cancel();
};

// 立即执行
const handleFlush = () => {
  flush();
};

useDebounceEffect

为 useEffect 增加防抖的能力

import { useEffect } from 'react';
import useDebounceFn from '../useDebounceFn';
import type { DebounceOptions } from '../useDebounceFn';

function useDebounceEffect(
  effect: React.EffectCallback,
  deps?: React.DependencyList,
  options?: DebounceOptions,
) {
  const [run] = useDebounceFn(effect, options);

  useEffect(() => {
    run();
  }, deps);
}

export default useDebounceEffect;

// 使用场景:窗口尺寸变化防抖
useDebounceEffect(
  () => {
    console.log('Window resized');
    // 执行相关操作
  },
  [windowWidth, windowHeight],
  { wait: 300 }
);

useThrottleFn

用来处理函数节流的 Hook

import { useMemo } from 'react';
import throttle from 'lodash/throttle';
import type { ThrottleSettings } from 'lodash';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';

export interface ThrottleOptions extends ThrottleSettings {
  wait?: number;
}

type Fn = (...args: any[]) => any;

function useThrottleFn<T extends Fn>(fn: T, options?: ThrottleOptions) {
  const fnRef = useLatest(fn);

  const wait = options?.wait ?? 1000;

  const throttledFn = useMemo(
    () =>
      throttle(
        ((...args: any[]) => {
          return fnRef.current(...args);
        }) as T,
        wait,
        options,
      ),
    [],
  );

  useUnmount(() => {
    throttledFn.cancel();
  });

  return [throttledFn, throttledFn.cancel, throttledFn.flush] as const;
}

export default useThrottleFn;

// 基本用法
const [throttledFn, cancel, flush] = useThrottleFn(
  (value) => {
    console.log('Throttled:', value);
  },
  { wait: 500 }
);

// 滚动事件节流
const handleScroll = (e) => {
  throttledFn(e.target.scrollTop);
};

// 取消节流
const handleCancel = () => {
  cancel();
};

// 立即执行
const handleFlush = () => {
  flush();
};

useThrottleEffect

import { useEffect } from 'react';
import useThrottleFn from '../useThrottleFn';
import type { ThrottleOptions } from '../useThrottleFn';

function useThrottleEffect(
  effect: React.EffectCallback,
  deps?: React.DependencyList,
  options?: ThrottleOptions,
) {
  const [run] = useThrottleFn(effect, options);

  useEffect(() => {
    run();
  }, deps);
}

export default useThrottleEffect;

// 使用场景:防止频繁的搜索请求
useThrottleEffect(
  () => {
    if (searchKeyword) {
      fetchSearchResults(searchKeyword);
    }
  },
  [searchKeyword],
  { wait: 500 }
);

useDeepCompareEffect

用法与 useEffect 一致,但 deps 通过 react-fast-compare 进行深比较。

import { useEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';

function useDeepCompareEffect(
  effect: React.EffectCallback,
  deps: React.DependencyList,
) {
  const ref = useRef<React.DependencyList>();
  const signalRef = useRef<number>(0);

  if (!isEqual(ref.current, deps)) {
    ref.current = deps;
    signalRef.current += 1;
  }

  useEffect(effect, [signalRef.current]);
}

export default useDeepCompareEffect;

// 对象依赖的深度比较
const userData = { name: 'John', age: 30, address: { city: 'New York' } };

useDeepCompareEffect(() => {
  console.log('User data changed:', userData);
  // 发送请求或执行其他副作用
}, [userData]); // 即使内容相同但引用不同的对象也会正确比较

// 数组依赖的深度比较
const items = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];

useDeepCompareEffect(() => {
  console.log('Items changed:', items);
}, [items]);

useDeepCompareLayoutEffect

用法与 useLayoutEffect 一致,但 deps 通过 react-fast-compare 进行深比较。

import { useLayoutEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';

function useDeepCompareLayoutEffect(
  effect: React.EffectCallback,
  deps: React.DependencyList,
) {
  const ref = useRef<React.DependencyList>();
  const signalRef = useRef<number>(0);

  if (!isEqual(ref.current, deps)) {
    ref.current = deps;
    signalRef.current += 1;
  }

  useLayoutEffect(effect, [signalRef.current]);
}

export default useDeepCompareLayoutEffect;

// 需要同步执行的 DOM 操作
const styleConfig = { color: 'red', fontSize: 16 };

useDeepCompareLayoutEffect(() => {
  // 同步执行,避免页面闪烁
  document.getElementById('myElement')?.setAttribute('style', JSON.stringify(styleConfig));
}, [styleConfig]);

// 需要精确控制时机的布局计算
const layoutData = { width: 100, height: 200 };

useDeepCompareLayoutEffect(() => {
  // 在浏览器绘制前完成布局计算
  calculateAndSetLayout(layoutData);
}, [layoutData]);

useInterval

一个可以处理 setInterval 的 Hook

import { useEffect, useRef } from 'react';
import useLatest from './useLatest';

export interface Options {
  immediate?: boolean;
}

function useInterval(
  fn: () => void,
  delay: number | undefined,
  options?: Options,
) {
  const fnRef = useLatest(fn);

  useEffect(() => {
    if (typeof delay !== 'number' || delay < 0) return;
    
    if (options?.immediate) {
      fnRef.current();
    }
    
    const timer = setInterval(() => {
      fnRef.current();
    }, delay);

    return () => {
      clearInterval(timer);
    };
  }, [delay, options?.immediate]);
}

export default useInterval;

// 使用场景:带立即执行的定时器
useInterval(() => {
  console.log('立即执行一次,之后每2秒执行一次');
}, 2000, { immediate: true });

useRafInterval

用 requestAnimationFrame 模拟实现 setInterval,API 和 useInterval 保持一致,好处是可以在页面不渲染的时候停止执行定时器,比如页面隐藏或最小化等。

import { useEffect, useRef } from 'react';
import useLatest from '../useLatest';
import { isBrowser } from '../utils/dom';

export interface Options {
  immediate?: boolean;
}

function useRafInterval(
  fn: () => void,
  delay: number | undefined,
  options?: Options,
) {
  const fnRef = useLatest(fn);
  const timerRef = useRef<number | null>(null);

  useEffect(() => {
    if (!isBrowser) {
      return;
    }
    
    if (typeof delay !== 'number' || delay < 0) return;

    if (options?.immediate) {
      fnRef.current();
    }

    const loop = () => {
      timerRef.current = requestAnimationFrame(() => {
        fnRef.current();
        loop();
      });
    };

    timerRef.current = window.setTimeout(() => {
      loop();
    }, delay);

    return () => {
      if (timerRef.current) {
        cancelAnimationFrame(timerRef.current);
      }
      clearTimeout(timerRef.current as unknown as number);
    };
  }, [delay, options?.immediate]);

  const clear = () => {
    if (timerRef.current) {
      cancelAnimationFrame(timerRef.current);
      clearTimeout(timerRef.current as unknown as number);
    }
  };

  return clear;
}

export default useRafInterval;


// 动画更新
function AnimatedComponent() {
  const [progress, setProgress] = useState(0);
  
  useRafInterval(() => {
    setProgress(p => Math.min(p + 0.01, 1));
  }, 16); // 约60fps
  
  return (
    <div style={{ width: `${progress * 100}%` }}>
      加载中...
    </div>
  );
}

// 实时数据更新
function RealTimeChart() {
  const dataRef = useRef([]);
  
  useRafInterval(() => {
    // 更新图表数据
    updateChart(dataRef.current);
  }, 1000);
  
  return <canvas ref={canvasRef} />;
}


useTimeout

一个可以处理 setTimeout 计时器函数的 Hook

import { useEffect, useRef } from 'react';
import useLatest from './useLatest';

function useTimeout(fn: () => void, delay: number | undefined) {
  const fnRef = useLatest(fn);

  useEffect(() => {
    if (typeof delay !== 'number' || delay < 0) return;
    
    const timer = setTimeout(() => {
      fnRef.current();
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [delay]);
}

export default useTimeout;

// 显示提示信息并在几秒后自动隐藏
const [visible, setVisible] = useState(true);

useTimeout(() => {
  setVisible(false);
}, 3000);

// 防止误触的确认操作
const [confirming, setConfirming] = useState(false);

const handleDelete = () => {
  if (!confirming) {
    setConfirming(true);
    useTimeout(() => {
      setConfirming(false);
    }, 3000);
  } else {
    // 执行删除操作
    performDelete();
  }
};

useRafTimeout

用 requestAnimationFrame 模拟实现 setTimeout,API 和 useTimeout 保持一致,好处是可以在页面不渲染的时候不触发函数执行,比如页面隐藏或最小化等。

import { useEffect, useRef } from 'react';
import useLatest from '../useLatest';
import { isBrowser } from '../utils/dom';

function useRafTimeout(fn: () => void, delay: number | undefined) {
  const fnRef = useLatest(fn);
  const timerRef = useRef<number | null>(null);

  useEffect(() => {
    if (!isBrowser) {
      return;
    }
    
    if (typeof delay !== 'number' || delay < 0) return;

    const loop = () => {
      timerRef.current = requestAnimationFrame(() => {
        fnRef.current();
      });
    };

    timerRef.current = window.setTimeout(() => {
      loop();
    }, delay);

    return () => {
      if (timerRef.current) {
        cancelAnimationFrame(timerRef.current);
      }
      clearTimeout(timerRef.current as unknown as number);
    };
  }, [delay]);

  const clear = () => {
    if (timerRef.current) {
      cancelAnimationFrame(timerRef.current);
      clearTimeout(timerRef.current as unknown as number);
    }
  };

  return clear;
}

export default useRafTimeout;

// 基本用法
const clear = useRafTimeout(() => {
  console.log('在下一动画帧执行');
}, 1000);

// 动画相关操作
useRafTimeout(() => {
  // 在浏览器重绘前执行动画更新
  updateAnimationState();
}, 16); // 约一帧的时间

// 手动清除
const clearTimeout = useRafTimeout(() => {
  performAction();
}, 3000);

// 在需要时清除
const handleCancel = () => {
  clearTimeout();
};

useLockFn

用于给一个异步函数增加竞态锁,防止并发执行。

import { useRef, useCallback } from 'react';

function useLockFn<P extends any[] = any[], V extends any = any>(
  fn: (...args: P) => Promise<V> | V,
) {
  const lockRef = useRef(false);

  return useCallback(
    async (...args: P) => {
      if (lockRef.current) return;
      lockRef.current = true;
      
      try {
        const ret = await fn(...args);
        return ret;
      } catch (e) {
        throw e;
      } finally {
        lockRef.current = false;
      }
    },
    [fn],
  );
}

export default useLockFn;

// 防止重复提交
const submit = useLockFn(async (data) => {
  const result = await api.submit(data);
  return result;
});

// 防止重复请求
const fetchData = useLockFn(async (id) => {
  const data = await api.getData(id);
  setData(data);
  return data;
});

// 处理用户连续点击
const handleClick = useLockFn(async () => {
  setLoading(true);
  try {
    await performAsyncOperation();
  } finally {
    setLoading(false);
  }
});

useUpdate

useUpdate 会返回一个函数,调用该函数会强制组件重新渲染

import { useCallback, useState } from 'react';

function useUpdate() {
  const [, setState] = useState({});

  return useCallback(() => setState({}), []);
}

export default useUpdate;

// 基本用法
function MyComponent() {
  const update = useUpdate();
  
  const handleClick = () => {
    // 执行某些不改变状态的操作
    doSomething();
    // 强制组件重新渲染
    update();
  };
  
  return (
    <div>
      <p>当前时间: {Date.now()}</p>
      <button onClick={handleClick}>强制更新</button>
    </div>
  );
}