前言

本文主要编制了一份清单,列出了在项目中可能会需要用到的的七个Hooks。

1. useToggle

这个hook很常见,它用于在 true 和 false 之间切换布尔值。当我们想要显示/隐藏模式或打开/关闭侧边菜单时,它很有用。这个hook的基本版本如下所示:

实现v1

// useToggle.jsx
import { useState, useCallback } from 'react';

const useToggle = (initialValue = false) => {
  const [state, setState] = useState(initialValue);
  
  const toggle = useCallback(() => {
    setState((state) => !state);
  }, []);
  
  return [state, toggle];
};

export default useToggle;

案例1

import useToggle from './useToggle';
const App = () => {
  const [show, toggleShow] = useToggle();
  return (
    <Modal show={show} onClose={toggleShow}>
      <h1>Hello there</h1>
    </Modal>
  );
}

当我们想要显示/隐藏表中一行的模式时,可以稍微修改此hook以用于用例。我添加了一个 customToggle 方法,该方法将值设置为给定值,而不是切换先前的状态值。

实现v2

// useToggle.jsx modified

import { useState, useCallback } from 'react';

const useToggle = (initialValue = false) => {
  const [state, setState] = useState(initialValue);

  const toggle = useCallback(() => {
    setState((state) => !state);
  }, []);

  const customToggle = useCallback((value) => {
    setState(value);
  }, []);

  return [state, toggle, customToggle];
};

export default useToggle;

案例2

假设我们在一个表格中有一堆行,我们想提供一个删除行的选项。单击删除按钮应打开一个确认模式。
对于这种类型的功能,我们需要状态中的两个变量。首先,保存一个布尔值来确定是否显示删除确认模式,其次保存必须显示删除模式的行 ID。
使用这个hook,我们可以用一个状态变量来完成。这是如何做到的:

import useToggle from './useToggle';

// initial data
const rows = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Doe' }
];

const App = () => {
  // using hook
  const [delModal, toggleDelModal, setDelModalCustom] = useToggle();

  return (
    <div>
      <table>
        {rows.map(row => (
          <tr>
            <td>{row.id}</td>
            <td>{row.name}</td>
            <td onClick={() => setDelModalCustom(row.id)}>Delete</td>
          </tr>
        ))}
      </table>

      <DeleteModal
        show={Boolean(delModal)}
        onClose={toggleDelModal}
        itemToDelete={delModal} // it will hold the current row id
      >
        <h1>
          Are you sure you want to delete row with id {delModal}
        </h1>
      </DeleteModal>
    </div>
  );
}

2. usePageBottom

使用此hook,你可以确定用户是否已滚动到页面底部。非常适合无限滚动的应用程序,当用户滚动到页面底部时,你需要获取更多数据。

// usePageBottom.jsx

import { useState, useEffect } from 'react';

const usePageBottom = () => {
  const [reachedBottom, setReachedBottom] = useState(false);
  
  // event handler for determining if the user reached bottom
  const handleScroll = () => {
    const offsetHeight = document.documentElement.offsetHeight;
    const innerHeight = window.innerHeight;
    const scrollTop = document.documentElement.scrollTop;

    // if current scroll from bottom is less than equal to 10px
    const reachingBottom = offsetHeight - (innerHeight + scrollTop) <= 10;

    setReachedBottom(reachingBottom);
  };

  // effect for binding event listener on window scroll
  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, [handleScroll]);
  
  return reachedBottom;
}

案例

import usePageBottom from './usePageBottom';
const App = (props) => {
  // hook usage
  const scrolledBottom = usePageBottom();
  return (
    <div className="App" style={{ height: '150vh' }}>
      <h1>This is app</h1>
      <p>Scrolled to bottom {scrolledBottom}</p>
    </div>
  );
}

节流版本(可选)

减少像窗口滚动事件这样多次触发的事件的通知是最佳实践,我们可以使用节流设置事件侦听器。我正在使用 lodashthrottle;你也可以使用去debounce

// usePageBottom with throttle

import { useState, useEffect, useMemo } from 'react';
import { throttle } from 'lodash';

const usePageBottom = () => {
  const [reachedBottom, setReachedBottom] = useState(false);
  
  // event handler for determining if the user reached bottom
 const handleScroll = useMemo(() => {
    return throttle(() => {
      const offsetHeight = document.documentElement.offsetHeight;
      const innerHeight = window.innerHeight;
      const scrollTop = document.documentElement.scrollTop;

      // if current scroll from bottom is less than equal to 10px
      const reachingBottom = offsetHeight - (innerHeight + scrollTop) <= 10;
      
      setReachedBottom(reachingBottom);
    }, 1000);
  }, []);

  // effect for binding event listener on window scroll
  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, [handleScroll]);
  
  return reachedBottom;
}

3. useElementBottom

就像页面底部一样,我们也可以确定用户是否已经滚动到元素的底部。在这个hook中,传递了对元素的引用,因此我们可以使用元素的高度和当前滚动位置来确定元素是否滚动到底部。

// useElementBottom.jsx

import { useState, useEffect, useMemo } from 'react';
import { throttle } from 'lodash';

const useElementBottom = (element) => {
  const [reachedBottom, setReachedBottom] = useState(false);

  // event handler for determining if the user reached bottom
  const handleScroll = useMemo(() => {
    return throttle(() => {
      const { current } = element; // current holds the reference to element

      // if current scroll from bottom is less than equal to 10px
      const scrollBottom =
        current.scrollHeight - current.scrollTop - current.clientHeight;

      const reachingBottom = scrollBottom <= 10;
      setReachedBottom(reachingBottom);
    }, 1000);
  }, []);

  // effect for binding event listener on element scroll
  useEffect(() => {
    const { current } = element;
    current.addEventListener('scroll', handleScroll);

    return () => current.removeEventListener('scroll', handleScroll);
  }, []);

  return reachedBottom;
};

export default useElementBottom;

案例

import useElementBottom from './useElementBottom';
const App = (props) => {
  const element = useRef();
  // hook usage
  const scrolledBottom = useElementBottom(element);
  return (
    <div ref={element} style={{ height: '150vh' }}>
      <h1>This is app</h1>
      <p>Scrolled to bottom {scrolledBottom}</p>
    </div>
  );
}

4. usePrevious

我们可以制作一个自定义hook来获取 prop 或 state 的先前值。使用 React 类组件,可以使用 componentDidUpdate 生命周期来获取之前的 prop 和 state 值。对于功能组件,我们可以使用自定义hook来完成,如下所示:

// usePrevious.jsx

import { useEffect, useRef } from 'react';

const usePrevious = (value) => {
  const ref = useRef();

  // store current value in ref
  useEffect(() => {
    ref.current = value;
  }, [value]);

  // return previous value (happens before update in useEffect above)
  return ref.current;
};

export default usePrevious;

我们使用 useRef hook来存储先前的值。我们的hook在 useEffect 中更新引用对象之前返回先前的值。

import usePrevious from './usePrevious';

const App = () => {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  
  return (
    <div>
      <h1>Now: {count}, before: {prevCount}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

5. useWindowSize

这个hook返回窗口的宽度和高度。我们在调整窗口大小时设置了一个受限制的 set 事件侦听器,以便我们每次都能获得正确的宽度和高度。

// useWindowSize

import { useState, useEffect, useCallback } from 'react';
import { throttle } from 'throttle';

const useWindowSize = () => {
  const [size, setSize] = useState({
    innerWidth: window.innerWidth,
    innerHeight: window.innerHeight,
  });

  // throttled set listener that runs on window resize
  const throttledSetResizeListner = useCallback(
    throttle(() => {
      // set size in state
      setSize({
        innerWidth: window.innerWidth,
        innerHeight: window.innerHeight,
      });
    }, 1000),
    []
  );

  // effect for binding resize event on window
  useEffect(() => {
    // add throtelled set window resize event listener
    window.addEventListener('resize', throttledSetResizeListner);

    // remove throttled set window resize event listener
    return () =>
      window.removeEventListener('resize', throttledSetResizeListner);
  }, [throttledSetResizeListner]);

  return size;
};

export default useWindowSize;

案例

当你想根据窗口的宽度或高度在你的 JS 代码中做一些事情时,它很有用。

import useWindowSize from './useWindowSize';
const App = () => {
  const { innerWidth, innerHeight } = useWindowSize();
  return (
    <div>
      {innerWidth <= 768 ?
        'I am on small screen' : 'I am on large screen'}
    </div>
  );
}

6. useEventListener

使用此hook可以轻松地将事件侦听器绑定到窗口或窗口元素。在hook的effect中,我们检查元素是否是来自 useRef 的引用。如果没有,我们将事件侦听器添加到全局窗口对象。

import { useEffect } from 'react';

const useEventListener = (event, handler, referencedElement) => {
  // effect for binding event handler to the element
  useEffect(() => {
    const element = referencedElement?.current || window;

    const isSupported = element && element.addEventListener;

    if (!isSupported) return;

    // bind event to the element
    element.addEventListener(event, handler);

    return () => element.removeEventListener(event, handler);
  }, [referencedElement, event, handler]);

  return;
};

export default useEventListener;
import useEventListener from './useEventListener';

const App = (props) => {
  const element = useRef();
  useEventListener('mouseover', handler, element);
  const handler => () => {
    console.log('Event triggered');
  }
  return (
    <div ref={element}>
      <h1>This is app</h1>
    <div>
  );
}

我们还可以使用相同的hook将事件添加到窗口

import useEventListener from './useEventListener';
const App = (props) => {
  // adding event listener on window scroll
  useEventListener('scroll', handler);
  const handler => () => {
    console.log('Event triggered');
  }
  return (
    <div>
      <h1>This is app</h1>
    <div>
  );
}

7. useLocalStorageState

有时,可能需要在本地存储和状态之间同步一个值。这个hook的作用完全一样。我们正在使用 useState 并检查指定的键是否在本地存储中具有某个值,以便我们可以将其用作初始值。这允许我们在页面刷新时保持值与状态同步。

// useLocalStorageState

import { useState, useEffect } from 'react';

const useLocalStorageState = (key, defaultValue) => {
  const [value, setValue] = useState(() => {
    let val;

    try {
      // if there is a value in local storage for given key, set it as initial state
      val = JSON.parse(localStorage.getItem(key) || String(defaultValue));
    } catch (error) {
      // otherwise, set default value as initial state
      val = defaultValue;
    }

    return val;
  });

  // effect to update local storage when state changes
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [value]);

  return [value, setValue];
};

export default useLocalStorageState;

当值更新时,会运行一个effect ,更新本地存储中的值。

案例

假设你想在本地存储中存储用户主题首选项

import useLocalStorageState from './useLocalStorageState';
const App = (props) => {
  // setting default theme to light
  const [theme, setTheme] = useLocalStorageState('theme', 'light');
  return (
    <div>
      <p>Current theme is {theme}</p>
      <button onClick={() => setTheme('dark')}>
        Change theme to dark
      </button>
    </div>
  );
}

当你点击更改主题按钮时,状态和本地存储中的值都会更新。页面刷新后,你将获得存储在本地存储中的最后一个值。

结语

感谢你阅读到最后。我希望这些自定义hook对你有用。