「React」25个自定义hooks(未来将继续更新)

259 阅读6分钟

1、useContextReducer

实现:

//先创建一个context 传递一个数组,这个数组中有两个元素,一个是对象,一个是回调函数
const Provider=({children})=>{
  const state=React.useState({});
  const value=React.useMemo(()=>state,[state[0]])
  return <Context.Provider value={value}>{children}</Context.Provider>
}
​
function useContextReducer(contextKey,reducer,initalState,initalAction){
    const [contextState,setContextState]=useContext(Context);
    let [state]=useReducer(reducer,initalState,initalAction);
​
    if(contextState[contextKey]!=null){
        state=contextState[contextKey]
    }
​
    const dispatch=action=>{
        setContextState(preState=>{
            Object.assign({},preState,{
            [contextKey]:reducer(preState[contextKey],action)
            })
        })
    }
    useEffect(()=>{
        if(contextState[contentKey]==null && state !=null){
            setContextState(preState=>{
                if(preState[contextKey]==null){
                    return Object.assign({},preState,{
                        [contentKey]:state
                    })
                }
                return preState;
            })
        }
    },[contextKey])
​
    return [state,dispatch];
}

案例:

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    default:
      return state;
  }
}
​
function useCounter() {
  const [count, dispatch] = useContextReducer("counter", reducer, 0);
  const increment = () => dispatch({ type: "INCREMENT" });
  return { count, increment };
}
​
function IncrementButton() {
  const { increment } = useCounter();
  return <button onClick={increment}>Increment</button>;
}
​
function Count() {
  const { count } = useCounter();
  return <div>{count}</div>;
}
​
function Demo() {
  return (
    <Provider>
      <Count />
      <IncrementButton />
    </Provider>
  );
}

2、useContextState

实现:

const Context = React.createContext([{}, () => {}]);
​
const Provider = ({ children }) => {
  const state = React.useState({});
  const value = React.useMemo(() => state, [state[0]]);
  return <Context.Provider value={value}>{children}</Context.Provider>;
};
​
function useContextState(contextKey, initialState) {
  const [contextState, setContextState] = React.useContext(Context);
​
  const state =
    contextState[contextKey] != null ? contextState[contextKey] : initialState;
​
  const setState = nextState =>
    setContextState(prevState =>
      Object.assign({}, prevState, {
        [contextKey]:
          typeof nextState === "function" ? nextState(prevState) : nextState
      })
    );
​
  React.useEffect(() => {
    if (contextState[contextKey] == null && state != null) {
      setContextState(prevState => {
        if (prevState[contextKey] == null) {
          return Object.assign({}, prevState, {
            [contextKey]: state
          });
        }
        return prevState;
      });
    }
  }, [contextKey]);
​
  return [state, setState];
}

案例:

function useCounter() {
  const [count, setCount] = useContextState("counter", 0);
  const increment = () => setCount(count + 1);
  return { count, increment };
}
​
function IncrementButton() {
  const { increment } = useCounter();
  return <button onClick={increment}>Increment</button>;
}
​
function Count() {
  const { count } = useCounter();
  return <div>{count}</div>;
}
​
function Demo() {
  return (
    <Provider>
      <Count />
      <IncrementButton />
    </Provider>
  );
}

3、useArray

实现:

const useArray = initial => {
  const [value, setValue] = useState(initial);
  return {
    value,
    setValue,
    add: useCallback(a => setValue(v => [...v, a]), []),
    clear: useCallback(() => setValue(() => []), []),
    removeById: useCallback(
      id => setValue(arr => arr.filter(v => v && v.id !== id)),
      []
    ),
    removeIndex: useCallback(
      index =>
        setValue(v => {
          v.splice(index, 1);
          return v;
        }),
      []
    )
  };
};

案例:

function Demo() {
  const todos = useArray(["Item 1"]);
  return todos.value.map(todo => <div>{todo}</div>);
}

4、useBoolean

实现:

const useBoolean = initial => {
  const [value, setValue] = useState(initial);
  return {
    value,
    setValue,
    toggle: useCallback(() => setValue(v => !v), []),
    setTrue: useCallback(() => setValue(true), []),
    setFalse: useCallback(() => setValue(false), [])
  };
};

案例:

function Demo() {
  const toggle = useBoolean(false);
  return toggle.value ? "On" : "Off";
}

5、useInput

实现:

const useInput = initial => {
  const isNumber = typeof initial === "number";
  const [value, setValue] = useState(initial);
  const onChange = useCallback(e => setValue(e.target.value), []);
​
  return {
    value,
    setValue,
    hasValue:
      value !== undefined &&
      value !== null &&
      (!isNumber ? value.trim && value.trim() !== "" : true),
    clear: useCallback(() => setValue(""), []),
    onChange,
    bindToInput: {
      onChange,
      value
    },
    bind: {
      onChange: setValue,
      value
    }
  };
};

案例:

function Demo() {
  const newTodo = useInput("");
  return (
    <input type="text" value={newTodo.value} onChange={newTodo.onChange} />
  );
}

6、useOnMount

实现:

const useOnMount = onMount =>
  useEffect(() => {
    onMount && onMount();
  }, []);

案例:

function Demo() {
  useOnMount(() => console.log("hello world"));
  return <div />;
}

7、useOnUnMount

实现:

const useOnUnmount = onUnmount =>
  useEffect(() => {
    return () => onUnmount && onUnmount();
  }, []);

案例:

function Demo() {
  // replace `<div></div>` below with `null` to see output in console
  useOnUnmount(() => console.log("goodbye world"));
  return <div />;
}

8、useActive

实现:

function useActive({ onChange, refEl }) {
  const [value, setValue] = useState(false);
  const handleMouseDown = () => {
    setValue(true);
    onChange(true);
  };
  const handleMouseUp = () => {
    setValue(false);
    onChange(false);
  };
  useEffect(() => {
    if (refEl && refEl.current) {
      refEl.current.addEventListener("mousedown", handleMouseDown);
      refEl.current.addEventListener("mouseup", handleMouseUp);
    }
    return () => {
      if (refEl && refEl.current) {
        refEl.current.removeEventListener("mousedown", handleMouseDown);
        refEl.current.removeEventListener("mouseup", handleMouseUp);
      }
    };
  }, []);
​
  return value;
}

案例:

function Demo() {
  const inputEl = useRef(null);
  const activeValue = useActive({ refEl: inputEl });
​
  return (
    <>
      <input ref={inputEl} type="text" />
      <div>{activeValue ? "Active" : "Nop"}</div>
    </>
  );
}

9、useInterval

实现:

function useInterval({ startImmediate, duration, callback }) {
  const [count, updateCount] = useState(0);
  const [intervalState, setIntervalState] = useState(
    startImmediate === undefined ? true : startImmediate
  );
  const [intervalId, setIntervalId] = useState(null);
​
  useEffect(() => {
    if (intervalState) {
      const intervalId = setInterval(() => {
        updateCount(count + 1);
        callback && callback();
      }, duration);
      setIntervalId(intervalId);
    }
​
    return () => {
      if (intervalId) {
        clearInterval(intervalId);
        setIntervalId(null);
      }
    };
  }, [intervalState, count]);
  return {
    intervalId,
    start: () => {
      setIntervalState(true);
    },
    stop: () => {
      setIntervalState(false);
    }
  };
}

案例:

function Demo() {
  const [time, setTime] = useState(null);
  const { start, stop } = useInterval({
    duration: 1000,
    startImmediate: false,
    callback: () => {
      setTime(new Date().toLocaleTimeString());
    }
  });
​
  return (
    <>
      <div>The time is now {time}</div>
      <button onClick={() => stop()}>Stop interval</button>
      <button onClick={() => start()}>Start interval</button>
    </>
  );
}

10、useMap

实现:

function useMap(initial) {
  const [mapValue, setMapValue] = useState(() => initial);
​
  return {
    values: mapValue,
    clear: () => setMapValue({}),
    reset: () => setMapValue(initial),
    set: (key, updater) => {
      setMapValue(prev =>
        Object.assign(prev, {
          [key]: typeof updater === "function" ? updater(prev[key]) : updater
        })
      );
    },
    get: key => mapValue[key],
    has: key => mapValue[key] != null,
    delete: key => setMapValue(({ [key]: deleted, ...prev }) => prev)
  };
}

案例:

function Demo() {
  const {
    set: setKey,
    get: getKey,
    has,
    delete: deleteKey,
    clear,
    reset,
    values
  } = useMap({ name: "PK", age: "30", occupation: "Reactor" });
​
  return JSON.stringify(values);
}

11、useToggle

实现:

const useToggle = initialValue => {
  const [value, setValue] = useState(initialValue);
  const toggler = useCallback(() => setValue(value => !value));
  return [value, toggler];
};

案例:

function Demo() {
  const [currentValue, toggleAway] = useToggle(true);
  return <div onClick={toggleAway}>{currentValue ? "🍎" : "🍏"}</div>;
}

12、useAsync

实现:

const useAsync = (fn, args) => {
  const [state, set] = useState({
    loading: true
  });
  const memoized = useCallback(fn, args);
​
  useEffect(() => {
    let mounted = true;
    const promise = memoized();
​
    promise.then(
      value => {
        if (mounted) {
          set({
            loading: false,
            value
          });
        }
      },
      error => {
        if (mounted) {
          set({
            loading: false,
            error
          });
        }
      }
    );
​
    return () => {
      mounted = false;
    };
  }, [memoized]);
​
  return state;
};

案例:

// Returns a Promise that resolves after one second.
const fn = () =>
  new Promise(resolve => {
    setTimeout(() => {
      resolve("RESOLVED");
    }, 1000);
  });
​
function Demo() {
  const { loading, value, error } = useAsync(fn);
​
  return (
    <div>{loading ? <div>Loading...</div> : <div>Value: {value}</div>}</div>
  );
}

13、useRaf

React动画钩子,强制组件在每个reaquestAnimationFrame上重新渲染,返回时间流逝的百分比

实现:

const useRaf = (ms = 1e12, delay = 0) => {
  const [elapsed, set] = useState(0);
​
  useEffect(() => {
    let raf, timerStop, start;
​
    const onFrame = () => {
      const time = Math.min(1, (Date.now() - start) / ms);
      set(time);
      loop();
    };
    const loop = () => {
      raf = requestAnimationFrame(onFrame);
    };
    const onStart = () => {
      timerStop = setTimeout(() => {
        cancelAnimationFrame(raf);
        set(1);
      }, ms);
      start = Date.now();
      loop();
    };
    const timerDelay = setTimeout(onStart, delay);
​
    return () => {
      clearTimeout(timerStop);
      clearTimeout(timerDelay);
      cancelAnimationFrame(raf);
    };
  }, [ms, delay]);
​
  return elapsed;
};

案例:

function Demo() {
  const elapsed = useRaf(5000, 1000);
​
  return <div>Elapsed: {elapsed}</div>;
}

14、useTimeout

在指定的毫秒数后返回true

实现:

const useTimeout = (ms = 0) => {
  const [ready, setReady] = useState(false);
​
  useEffect(() => {
    let timer = setTimeout(() => {
      setReady(true);
    }, ms);
​
    return () => {
      clearTimeout(timer);
    };
  }, [ms]);
​
  return ready;
};

案例:

function Demo() {
  const ready = useTimeout(2000);
  return <div>Ready: {ready ? "Yes" : "No"}</div>;
}

15、useLocalStorage

实现:

function useLocalStorage(key) {
  let localStorageItem;
  if (key) {
    localStorageItem = localStorage[key];
  }
  const [localState, updateLocalState] = useState(localStorageItem);
  function syncLocalStorage(event) {
    if (event.key === key) {
      updateLocalState(event.newValue);
    }
  }
  useEffect(() => {
    window.addEventListener("storage", syncLocalStorage);
    return () => {
      window.removeEventListener("storage", syncLocalStorage);
    };
  }, []);
  return localState;
}

案例:

function Demo() {
  let name = useLocalStorage("name"); // send the key to be tracked.
  return (
    <div>
      <h1>{name}</h1>
    </div>
  );
}

16、useNetworkStatus

实现:

function getConnection() {
  return (
    navigator.connection ||
    navigator.mozConnection ||
    navigator.webkitConnection
  );
}
​
function useNetworkStatus() {
  let [connection, updateNetworkConnection] = useState(getConnection());
​
  function updateConnectionStatus() {
    updateNetworkConnection(getConnection());
  }
  useEffect(() => {
    connection.addEventListener("change", updateConnectionStatus);
    return () => {
      connection.removeEventListener("change", updateConnectionStatus);
    };
  }, []);
​
  return connection;
}

案例:

function Demo() {
  let connection = useNetworkStatus();
  return (
    <div>
      <div>downlink: {connection.downlink}</div>
      <div>effectiveType: {connection.effectiveType}</div>
      <div>rtt: {connection.rtt}</div>
      <div>saveData: {connection.saveData ? "yes" : "no"}</div>
    </div>
  );
}

17、useOnlineStatus

实现:

function getOnlineStatus() {
  return typeof navigator !== "undefined" &&
    typeof navigator.onLine === "boolean"
    ? navigator.onLine
    : true;
}
​
function useOnlineStatus() {
  let [onlineStatus, setOnlineStatus] = useState(getOnlineStatus());
  function goOnline() {
    setOnlineStatus(true);
  }
  function goOffline() {
    setOnlineStatus(false);
  }
  useEffect(() => {
    window.addEventListener("online", goOnline);
    window.addEventListener("offline", goOffline);
    return () => {
      window.removeEventListener("online", goOnline);
      window.removeEventListener("offline", goOffline);
    };
  }, []);
​
  return onlineStatus;
}

案例:

function Demo() {
  let onlineStatus = useOnlineStatus();
  return (
    <div>
      <h1>You are {onlineStatus ? "Online" : "Offline"}</h1>
    </div>
  );
}

18、useComponentSize

获取组件的实际大小

实现:

function getSize(el) {
  if (!el) {
    return {};
  }
​
  return {
    width: el.offsetWidth,
    height: el.offsetHeight
  };
}
​
function useComponentSize(ref) {
  let [ComponentSize, setComponentSize] = useState(getSize(ref.current));
​
  function handleResize() {
    if (ref && ref.current) {
      setComponentSize(getSize(ref.current));
    }
  }
​
  useLayoutEffect(() => {
    handleResize();
    window.addEventListener("resize", handleResize);
​
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);
​
  return ComponentSize;
}

案例:

function Demo() {
  let ref = useRef(null);
  let size = useComponentSize(ref);
  // size == { width: 100, height: 200 }
  let { width, height } = size;
  let imgUrl = `https://via.placeholder.com/${width}x${height}`;
​
  return (
    <div ref={ref} style={{ width: "100%", height: "100%" }}>
      <img src={imgUrl} />
    </div>
  );
}

18、useWindowMousePosition

获取鼠标在全局中的位置

实现:

function useWindowMousePosition() {
  let [WindowMousePosition, setWindowMousePosition] = useState({
    x: null,
    y: null
  });
​
  function handleMouseMove(e) {
    setWindowMousePosition({
      x: e.pageX,
      y: e.pageY
    });
  }
​
  useEffect(() => {
    window.addEventListener("mousemove", handleMouseMove);
​
    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
    };
  });
​
  return WindowMousePosition;
}

案例:

function Demo() {
  let { x, y } = useWindowMousePosition();
  return (
    <div style={{ width: "100%", height: "100%" }}>
      <pre>{JSON.stringify({ x, y })}</pre>
    </div>
  );
}

19、useWindowSize

实现:

function getSize() {
  return {
    innerHeight: window.innerHeight,
    innerWidth: window.innerWidth,
    outerHeight: window.outerHeight,
    outerWidth: window.outerWidth
  };
}
​
function useWindowSize() {
  let [windowSize, setWindowSize] = useState(getSize());
​
  function handleResize() {
    setWindowSize(getSize());
  }
​
  useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);
​
  return windowSize;
}

案例:

function Demo() {
  const size = useWindowSize();
  return <div>{size.innerWidth}</div>;
}

20、useWindowScrollPosition

滚动条的滚动位置

实现:

// We need to import lodash throttle if we want to throttle our scroll events
// import throttle from 'lodash.throttle';const useWindowScrollPosition = (options = {}) => {
  const { throttleMs = 100 } = options;
  const [scroll, setScroll] = React.useState({
    x: window.pageXOffset,
    y: window.pageYOffset
  });
​
  const handle = throttle(() => {
    setScroll({
      x: window.pageXOffset,
      y: window.pageYOffset
    });
  }, throttleMs);
​
  React.useEffect(() => {
    window.addEventListener("scroll", handle);
​
    return () => {
      window.removeEventListener("scroll", handle);
    };
  }, []);
​
  return scroll;
};

案例:

function Demo() {
  const { x, y } = useWindowScrollPosition();
​
  return (
    <div style={{ width: "100%", height: "100%" }}>
      <pre>{JSON.stringify({ x, y })}</pre>
    </div>
  );
}

21、useIntersectionObserver

IntersectionObserver提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

当一个IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

实现:

const useIntersectionObserver = (target, root) => {
  const [isIntersecting, setIntersecting] = React.useState(false);
​
  React.useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting !== isIntersecting) {
          setIntersecting(entry.isIntersecting);
        }
      },
      {
        rootMargin: "0px",
        root: root.current
      }
    );
    if (target.current) {
      observer.observe(target.current);
    }
    return () => {
      observer.unobserve(target.current);
    };
  }, []);
​
  return isIntersecting;
};

案例:

function Demo() {
  const targetRef = useRef(null);
  const isIntersecting = useIntersectionObserver(
    targetRef,
    document.querySelector("body")
  );
  return (
    <div className="App" ref={targetRef}>
      <h2>{isIntersecting ? "Component is visible" : "Component is hidden"}</h2>
    </div>
  );
}

22、usePromise

简化了React组件内部的promise处理

实现:

const usePromise = (fn, { resolve = false, resolveCondition = [] } = {}) => {
  const [data, setData] = useState();
  const [isLoading, setLoading] = useState(resolve);
  const [lastUpdated, setLastUpdated] = useState();
  const [error, setError] = useState();
​
  const request = (...args) => {
    /*
    Using isValid guard, in order to prevent the cleanup warning.
    */
    let isValid = true;
    setLoading(true);
​
    fn(...args)
      .then(result => {
        if (!isValid) return;
​
        setData(result);
        setLastUpdated(Date.now());
      })
      .catch(err => {
        if (!isValid) return;
​
        setError(err);
      })
      .finally(() => {
        if (!isValid) return;
​
        setLoading(false);
      });
​
    /*
    When component will be unmounted, isValid will become false and state setter
    functions will not be envoked on unmounted component.
    */
    return () => {
      isValid = false;
    };
  };
​
  if (resolve) {
    useEffect(request, resolveCondition);
  }
​
  return {
    request,
    data,
    isLoading,
    lastUpdated,
    error
  };
};

案例:

const Demo = () => {
  const { isLoading, data } = usePromise(fetchMovies, { resolve: true });
​
  return isLoading ? (
    <div>Loading...</div>
  ) : (
    <ul>
      {data.map(movie => (
        <li key={movie.id}>{movie.title}</li>
      ))}
    </ul>
  );
};
​
const fetchMovies = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, title: "The Godfather" },
        { id: 2, title: "The Dark Knight" },
        { id: 3, title: "Fight Club" }
      ]);
    }, 1000);
  });

23、useThrottle

实现:

const useThrottle = (value, limit) => {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastRan = useRef(Date.now());
​
  useEffect(() => {
    const handler = setTimeout(function() {
      if (Date.now() - lastRan.current >= limit) {
        setThrottledValue(value);
        lastRan.current = Date.now();
      }
    }, limit - (Date.now() - lastRan.current));
​
    return () => {
      clearTimeout(handler);
    };
  }, [value, limit]);
​
  return throttledValue;
};

案例:

function Demo() {
  const [text, setText] = useState("Hello");
  const throttledText = useThrottle(text, 1000);
  return (
    <div>
      <input
        defaultValue={"Hello"}
        onChange={e => {
          setText(e.target.value);
        }}
      />
      <p>Actual value: {text}</p>
      <p>Throttled value: {throttledText}</p>
    </div>
  );
}

24、useDraggable

实现:

function useDraggable(el) {
  const [{ dx, dy }, setOffset] = useState({ dx: 0, dy: 0 });
  useEffect(() => {
    const handleMouseDown = event => {
      const startX = event.pageX - dx;
      const startY = event.pageY - dy;
      const handleMouseMove = event => {
        const newDx = event.pageX - startX;
        const newDy = event.pageY - startY;
        setOffset({ dx: newDx, dy: newDy });
      };
      document.addEventListener("mousemove", handleMouseMove);
      document.addEventListener(
        "mouseup",
        () => {
          document.removeEventListener("mousemove", handleMouseMove);
        },
        { once: true }
      );
    };
    el.current.addEventListener("mousedown", handleMouseDown);
    return () => {
      el.current.removeEventListener("mousedown", handleMouseDown);
    };
  }, [dx, dy]);
​
  useEffect(() => {
    el.current.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
  }, [dx, dy]);
}

案例:

function Demo() {
  const el = useRef();
  useDraggable(el);
​
  const style = {
    border: "3px solid cyan",
    margin: "40px auto 0 auto",
    width: "100px",
    height: "100px"
  };
​
  return <div ref={el} style={style} />;
}

25、useFoucs

实现:

const useFocus = (ref, defaultState = false) => {
  const [state, setState] = useState(defaultState);
​
  useEffect(() => {
    const onFocus = () => setState(true);
    const onBlur = () => setState(false);
    ref.current.addEventListener("focus", onFocus);
    ref.current.addEventListener("blur", onBlur);
​
    return () => {
      ref.current.removeEventListener("focus", onFocus);
      ref.current.removeEventListener("blur", onBlur);
    };
  }, []);
​
  return state;
};

案例:

function Demo() {
  const ref = useRef();
  const focused = useFocus(ref);
​
  return (
    <div className={`form-field${focused && " is-focused"}`}>
      <input type="text" ref={ref} placeholder="focus on me" />
      {focused && (
        <span className="tip">
          This trivial example could easily be achieved with{" "}
          <code>input:focus + .tip</code> but managing styles on a parent
          element is where this hook shines.
        </span>
      )}
    </div>
  );
}