React Hooks早知道

做者:docoder@毛豆前端前端

React Hooks

Notes

  • Examplenode

    function Example() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        document.title = `點擊了 ${count} 次`;
      });
      return (
        <div> <p>點擊了 {count} 次</p> <button onClick={() => setCount(count + 1)}> 點擊 </button> </div>
      );
    }
    複製代碼
  • React 16.8react

  • No Breaking Changesios

    • 可選的
    • 100% 向後兼容
    • 當即可用
  • Class 不會被移除git

  • Hook 被 React 團隊 指望成爲將來寫 React 的主要方式github

  • 除了不經常使用的 getDerivedStateFromError 和 componentDidCatch 在 hook 中尚未被等價實現(很快會添加),其餘幾乎能夠覆蓋全部使用 Class 的狀況web

  • 不影響對React的理解算法

  • 將來 Redux connect() 和 React Router 也會使用相似 useRedux() 或 useRouter() 的自定義 Hooks,固然如今的 API 用法也是兼容的npm

  • 對靜態類型支持更好,如 TypeScriptredux

  • 性能更好

    • 避免了 Class 的開銷
    • 重用邏輯無需高階組件,減小層級
  • 邏輯重用

    • render props
    • 高階組件
    • 重構,抽取通用邏輯,不用重寫層級結構
  • 擁抱 function, 無 Class

  • Thinking in Hooks

  • 不能在 Class 中使用

  • 建立本身的 Hooks

    • 每一次調用都會獲得一個徹底孤立的狀態
    • 能夠在同一個組件中使用兩次相同的自定義 Hook
    • 約定大於特性
      • 以 "use" 開頭
        • 只用於 linter plugin
        • react 並無依賴此來搜索 hook 或完成其餘功能特性
  • Hook 讓咱們根據代碼的做用進行拆分

    • 屢次使用,粒度更小
  • Hooks 只能在頂層調用

    • 不能在循環,條件語句,嵌套函數中調用

    • 保證其順序性和正確性

      • 保證每次 render 都按一樣的順序執行

      • 在多個 useState 和 useEffect 調用過程當中保證 state 的正確性

      • React 依賴 Hooks 的調用順序來確保 state 和 useState 的對應

        • 經過相似數組的方式實現,每次 render 是按數組 index 進行對應
        // ------------
        // First render
        // ------------
        useState('Mary')           // 1. Initialize the name state variable with 'Mary'
        useEffect(persistForm)     // 2. Add an effect for persisting the form
        useState('Poppins')        // 3. Initialize the surname state variable with 'Poppins'
        useEffect(updateTitle)     // 4. Add an effect for updating the title
        
        // -------------
        // Second render
        // -------------
        useState('Mary')           // 1. Read the name state variable (argument is ignored)
        useEffect(persistForm)     // 2. Replace the effect for persisting the form
        useState('Poppins')        // 3. Read the surname state variable (argument is ignored)
        useEffect(updateTitle)     // 4. Replace the effect for updating the title
        
        // ...
        複製代碼
    • linter plugin 會進行驗證

  • 只能在 React functions 中調用, 不能在普通的 JavaScript 函數中調用

    • React 的函數式組件
    • 自定義的 Hook
  • linter plugin

  • shouldComponentUpdate

    • React .memo wrap 一個 function component 會淺比較 props

      • 僅比較屬性,由於不存在 single state object to compare
      • 第二個參數接收一個自定義的 comparison function
      • 返回 true,update 將被跳過
      const Button = React.memo((props) => {
        // your component
      });
      複製代碼
    • useMemo

  • getDerivedStateFromProps

    function ScrollView({row}) {
      let [isScrollingDown, setIsScrollingDown] = useState(false);
      let [prevRow, setPrevRow] = useState(null);
    
      if (row !== prevRow) {
        // Row changed since last render. Update isScrollingDown.
        setIsScrollingDown(prevRow !== null && row > prevRow);
        setPrevRow(row);
      }
    
      return `Scrolling down: ${isScrollingDown}`;
    }
    複製代碼

Hooks

  • state hook

    • useState

      const [state, setState] = useState(initialState);
      複製代碼
    • 多個State,屢次使用useState

      • 數組解構,賦予狀態變量不一樣的名字

      • 在每一次渲染中以相同的順序被調用

      • initialState 沒必要須是對象,實際上不鼓勵是對象,根據實際數據相關性,進行分組和分離。這樣也更利於以後代碼重構,抽取相關邏輯成一個自定義 Hook

        //👎
        const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
        
        //👍改成以下:
        const [position, setPosition] = useState({ left: 0, top: 0 });
        const [size, setSize] = useState({ width: 100, height: 100 });
        
        //重構,自定義 Hook:
        function Box() {
          const position = useWindowPosition();
          const [size, setSize] = useState({ width: 100, height: 100 });
          // ...
        }
        
        function useWindowPosition() {
          const [position, setPosition] = useState({ left: 0, top: 0 });
          useEffect(() => {
            // ...
          }, []);
          return position;
        }
        複製代碼
    • state 僅在第一次 render 時被建立,以後只是修改使咱們獲得最新的 state

    • 無需像 useEffect 或 useCallback 那樣指定 依賴列表,由 React 來保證

      • 將來,React也會移除 useEffect 和 useCallback 的依賴列表,由於這徹底能夠經過算法自動解決
    • 函數式更新 (Functional updates)

      • 新值是經過以前的值計算而來

      • setCount 能夠接受一個函數,接受以前的 state, 返回新的 state

        function Counter({initialCount}) {
          const [count, setCount] = useState(initialCount);
          return (
            <> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </> ); } 複製代碼
    • state 更新了相同的值,不會進行 render 或 觸發 effect

      • Object.is() 算法,進行比較

      • useMemo

      • forceUpdate ?

        • 避免使用

        • hack

          const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
          
          function handleClick() {
            forceUpdate();
          }
          複製代碼
    • 懶初始化

      • useState 接受一個函數,返回 initialState,僅當初始 render 時被調用進行初始化,只調用一次

        const [state, setState] = useState(() => {
          const initialState = someExpensiveComputation(props);
          return initialState;
        });
        複製代碼
  • effect hook

    • useEffect

      useEffect(
        () => {
          // side effects (獲取數據、設置訂閱和手動更改 React 組件中的 DOM 等)
          return () => { // 可選
            // clean up
          }
        }
       	,
        [state, ...] // 可選,僅當 state (或 ...) 改變時,effect 才從新運行
      );
      複製代碼
    • 將類組件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 統一爲一個 API

      • 避免邏輯分散和重複書寫
        • 生命週期函數經常包含不相關的邏輯,同時相關的邏輯被拆分進不一樣的方法
    • 在每一次 render 後運行 effects ( 包括第一次 render )

      • React 保證每次運行 effects 以前 DOM 已經更新了
      • 在每次 render 的時候,都是新建立了一個函數傳遞給了 useEffect
        • 每次都是建立了新的 effect 替換以前的。
        • 每一個 effect 屬於一個特定的 render
        • 不用擔憂 effect 裏 state 過時的問題
    • 擁抱閉包,在函數做用域中,可方便訪問 state

    • 經過返回一個函數來 clean up

      • 添加和清理的邏輯能夠彼此靠近

      • 在下次運行 effect 以前清理上一次 render 中的 effect

        • 清理不在 unmount 調用一次,而是在每次 re-render 後調用

          • 避免在缺失 componentDidUpdate 時會產生的 bugs

            //當 friend 的屬性改變時,會產生依舊顯示以前 friend 的在線狀態的bug,尤爲當 unmounting 的時候,因爲 unsubscribe 一個錯誤的 friend id 會產生內存泄露甚至 crash
            	componentDidMount() {
                ChatAPI.subscribeToFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
            
            	componentWillUnmount() {
                ChatAPI.unsubscribeFromFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
            複製代碼
            //須要使用 componentDidUpdate
            	componentDidMount() {
                ChatAPI.subscribeToFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
            
              componentDidUpdate(prevProps) {
                // Unsubscribe 以前的 friend.id
                ChatAPI.unsubscribeFromFriendStatus(
                  prevProps.friend.id,
                  this.handleStatusChange
                );
                // Subscribe 新的 friend.id
                ChatAPI.subscribeToFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
            
              componentWillUnmount() {
                ChatAPI.unsubscribeFromFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
            
            複製代碼
        • 能夠有選擇的運行 effect,從而避免性能問題

          • useEffect 的第二個參數

            useEffect(() => {
              function handleStatusChange(status) {
                setIsOnline(status.isOnline);
              }
            
              ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
              return () => {
                ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
              };
            }, [props.friend.id]); // 僅當 props.friend.id 改變時 effect 才運行
            複製代碼
          • 僅在 mount 和 unmount 才運行 effect

            • 第二個參數傳空數組 []
          • 錯誤的指定第二個參數(常常是指過少配置), 會形成得不到最新的 props 或 state 的 bug

        • 把函數放到 useEffect 裏, 這樣更安全

          function Example({ someProp }) {
            function doSomething() {
              console.log(someProp);
            }
          
            useEffect(() => {
              doSomething();
            }, []); // 🔴 This is not safe (it calls `doSomething` which uses `someProp`)
          }
          複製代碼
          function Example({ someProp }) {
            useEffect(() => {
              function doSomething() {
                console.log(someProp);
              }
          
              doSomething();
            }, [someProp]); // ✅ OK (our effect only uses `someProp`)
          }
          
          function Example({ someProp }) {
            useEffect(() => {
              function doSomething() {
                console.log('hello');
              }
          
              doSomething();
            }, []); // ✅ OK in this example because we don't use *any* values from component scope
          }
          複製代碼
        • 不能將函數放到 effect 裏

          • 確保函數中沒有引用 props 或 state

          • 純計算函數,將該函數返回結果做爲 effect 的依賴

          • 用 useCallback wrap 函數 (確保函數在依賴不變的狀況下,自己不變),再做爲 effect 依賴

            function ProductPage({ productId }) {
              // ✅ Wrap with useCallback to avoid change on every render
              const fetchProduct = useCallback(() => {
                // ... Does something with productId ...
              }, [productId]); // ✅ All useCallback dependencies are specified
            
              return <ProductDetails fetchProduct={fetchProduct} />; } function ProductDetails({ fetchProduct }) useEffect(() => { fetchProduct(); }, [fetchProduct]); // ✅ All useEffect dependencies are specified // ... } 複製代碼
        • effect 依賴變更太頻繁

          • 函數式更新 (Functional updates)
          function Counter() {
            const [count, setCount] = useState(0);
          
            useEffect(() => {
              const id = setInterval(() => {
                setCount(count + 1); // This effect depends on the `count` state
              }, 1000);
              return () => clearInterval(id);
            }, []); // 🔴 Bug: `count` is not specified as a dependency
          
            return <h1>{count}</h1>;
          }
          複製代碼
          function Counter() {
            const [count, setCount] = useState(0);
          
            useEffect(() => {
              const id = setInterval(() => {
                setCount(c => c + 1); // ✅ This doesn't depend on `count` variable outside. The identity of the setCount function is guaranteed to be stable so it’s safe to omit.
              }, 1000);
              return () => clearInterval(id);
            }, []); // ✅ Our effect doesn't use any variables in the component scope
          
            return <h1>{count}</h1>;
          }
          複製代碼
          const BookEntryList = props => {
            const [pending, setPending] = useState(0);
            const [booksJustSaved, setBooksJustSaved] = useState([]);
          
            useEffect(() => {
              const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
          
              ws.onmessage = ({ data }) => {
                let packet = JSON.parse(data);
                if (packet._messageType == "initial") {
                  setPending(packet.pending);
                } else if (packet._messageType == "bookAdded") {
                  setPending(pending - 1 || 0);
                  setBooksJustSaved([packet, ...booksJustSaved]);
                } else if (packet._messageType == "pendingBookAdded") {
                  setPending(+pending + 1 || 0);
                } else if (packet._messageType == "bookLookupFailed") {
                  setPending(pending - 1 || 0);
                  setBooksJustSaved([
                    {
                      _id: "" + new Date(),
                      title: `Failed lookup for ${packet.isbn}`,
                      success: false
                    },
                    ...booksJustSaved
                  ]);
                }
              };
              return () => {
                try {
                  ws.close();
                } catch (e) {}
              };
            }, []);
          
            //...
          };
          複製代碼
          function scanReducer(state, [type, payload]) {
            switch (type) {
              case "initial":
                return { ...state, pending: payload.pending };
              case "pendingBookAdded":
                return { ...state, pending: state.pending + 1 };
              case "bookAdded":
                return {
                  ...state,
                  pending: state.pending - 1,
                  booksSaved: [payload, ...state.booksSaved]
                };
              case "bookLookupFailed":
                return {
                  ...state,
                  pending: state.pending - 1,
                  booksSaved: [
                    {
                      _id: "" + new Date(),
                      title: `Failed lookup for ${payload.isbn}`,
                      success: false
                    },
                    ...state.booksSaved
                  ]
                };
            }
            return state;
          }
          const initialState = { pending: 0, booksSaved: [] };
          
          const BookEntryList = props => {
            const [state, dispatch] = useReducer(scanReducer, initialState);
          
            useEffect(() => {
              const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
          
              ws.onmessage = ({ data }) => {
                let packet = JSON.parse(data);
                dispatch([packet._messageType, packet]); // The identity of the dispatch function from useReducer is always stable
              };
              return () => {
                try {
                  ws.close();
                } catch (e) {}
              };
            }, []);
          
            //...
          };
          複製代碼
        • exhaustive-deps ESLint

    • 異步請求數據

      function SearchResults() {
        const [data, setData] = useState({ hits: [] });
        const [query, setQuery] = useState('react');
      
        useEffect(() => { // 在 useEffect 裏直接使用 async function 是不被容許的,由於 useEffect function 必需要返回一個清理 function 或 nothing。
          let ignore = false;
      
          async function fetchData() { //須要在 useEffect function 裏使用 async function
            const result = await axios('https://hn.algolia.com/api/v1/search?query=' + query);
            if (!ignore) setData(result.data); //當組件 unmount 時,阻止其設置 state
          }
      
          fetchData();
          return () => { ignore = true; }
        }, [query]); // [query] 阻止形成循環,僅當 query 改變時,effect 才執行
      
        return (
          <>
            <input value={query} onChange={e => setQuery(e.target.value)} />
            <ul>
              {data.hits.map(item => (
                <li key={item.objectID}>
                  <a href={item.url}>{item.title}</a>
                </li>
              ))}
            </ul>
          </>
        );
      }
      複製代碼
    • 關注點分離,屢次使用 useEffect

      • 根據代碼的做用拆分紅多個 effect
    • useEffect 不會阻塞瀏覽器渲染

      • componentDidMountcomponentDidUpdate 會阻塞
      • 提供阻塞版本 useLayoutEffect ,來知足同步調用計算元素尺寸等問題
  • other hooks

    • useContext

    • useReducer

      • 只是對 local state 進行 redux 化,沒有造成 store 和 公用的 state 樹
      const [state, dispatch] = useReducer(reducer, initialArg, init);
      複製代碼
      const initialState = {count: 0};
      
      function reducer(state, action) {
        switch (action.type) {
          case 'increment':
            return {count: state.count + 1};
          case 'decrement':
            return {count: state.count - 1};
          default:
            throw new Error();
        }
      }
      
      function Counter({initialState}) {
        const [state, dispatch] = useReducer(reducer, initialState);
        return (
          <> Count: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 複製代碼
      • Pass down a dispatch function
      const TodosDispatch = React.createContext(null);
      
      function TodosApp() {
        // Note: `dispatch` won't change between re-renders
          const [todos, dispatch] = useReducer(todosReducer);
      
          return (
              <TodosDispatch.Provider value={dispatch}>
                  <DeepTree todos={todos} />
              </TodosDispatch.Provider>
          );
      }
      複製代碼
      function DeepChild(props) {
        // If we want to perform an action, we can get dispatch from context.
          const dispatch = useContext(TodosDispatch);
      
          function handleClick() {
              dispatch({ type: 'add', text: 'hello' });
          }
      
          return (
              <button onClick={handleClick}>Add todo</button>
          );
      }
      複製代碼
    • useRef

      • 相似實例變量

        function Timer() {
          const intervalRef = useRef();
        
          useEffect(() => {
            const id = setInterval(() => {
              // ...
            });
            intervalRef.current = id; // current 能夠被賦任何值,相似類中的實例變量
            return () => {
              clearInterval(intervalRef.current);
            };
          });
        
          // ...
          function handleCancelClick() {
            clearInterval(intervalRef.current);
          }
          // ...
        }
        複製代碼
      • 獲取 previous state

        function Counter() {
          const [count, setCount] = useState(0);
          const prevCountRef = useRef();
          useEffect(() => {
            prevCountRef.current = count;
          });
          const prevCount = prevCountRef.current;
        
          return <h1>Now: {count}, before: {prevCount}</h1>;
        }
        複製代碼
      • 自定義 Hook,usePrevious

        • 可能以後會提供開箱即用的官方實現
        function Counter() {
          const [count, setCount] = useState(0);
          const prevCount = usePrevious(count); // 使用自定義的 Hook
          return <h1>Now: {count}, before: {prevCount}</h1>;
        }
        // 自定義 Hook
        function usePrevious(value) {
          const ref = useRef();
          useEffect(() => {
            ref.current = value;
          });
          return ref.current;
        }
        複製代碼
      • 在一些異步回掉中,讀取最新 state

        // 首先點擊 Show alert, 而後點擊 Click me
        function Example() {
          const [count, setCount] = useState(0);
        
          function handleAlertClick() {
            setTimeout(() => {
              alert('You clicked on: ' + count); //讀取的是點擊 Show alert 時的 count,不是最新值
            }, 3000);
          }
        
          return (
            <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={handleAlertClick}> Show alert </button> </div>
          );
        }
        複製代碼
        // 用 useRef 修改:
        function Example() {
          const [count, setCount] = useState(0);
          
          //使用 useRef
          const countRef = useRef();
          countRef.current = count;
          
          function handleAlertClick() {
            setTimeout(() => {
              alert('You clicked on: ' + countRef.current); //經過 ref 訪問 count,讀取的是最新值
            }, 3000);
          }
        
          return (
            <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={handleAlertClick}> Show alert </button> </div>
          );
        }
        複製代碼
      • 懶加載

        • useRef 不像 useState 那樣接收函數

          function Image(props) {
            // ⚠️ IntersectionObserver is created on every render
            const ref = useRef(new IntersectionObserver(onIntersect));
            // ...
          }
          複製代碼
          function Image(props) {
            const ref = useRef(null);
          
            // ✅ IntersectionObserver is created lazily once
            function getObserver() {
              let observer = ref.current;
              if (observer !== null) {
                return observer;
              }
              let newObserver = new IntersectionObserver(onIntersect);
              ref.current = newObserver;
              return newObserver;
            }
          
            // When you need it, call getObserver()
            // ...
          }
          複製代碼
    • useCallback(fn, deps)

      • 返回一個帶緩存的 callback

      • 僅當所需依賴 deps (數組) 中元素改變時,才執行,不然返回緩存的值

      • 等價於 useMemo(() => fn, deps)

      • exhaustive-deps ESLint

        // 使用 Callback Refs, 而不用 useRef,這樣當 ref 改變時,能夠獲知
        function MeasureExample() {
          const [height, setHeight] = useState(0);
        
          const measuredRef = useCallback(node => {
            if (node !== null) {
              setHeight(node.getBoundingClientRect().height);
            }
          }, []); // [] 確保 ref callback 不會改變,re-renders 時,不會被重複調用執行
        
          return (
            <> <h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2> </> ); } 複製代碼
        // 抽取自定義 Hook
        function MeasureExample() {
          const [rect, ref] = useClientRect();
          return (
            <> <h1 ref={ref}>Hello, world</h1> {rect !== null && <h2>The above header is {Math.round(rect.height)}px tall</h2> } </> ); } function useClientRect() { const [rect, setRect] = useState(null); const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; } 複製代碼
    • useMemo(() => fn, deps)

      • 返回一個帶緩存的值,避免消耗性能的計算重複執行

        const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
        複製代碼
        • 僅當 a,b 改變時,纔會從新調用 computeExpensiveValue
        • 每次 render 時,傳給 useMemo 的函數會執行,不要把一些反作用放到裏面
      • 性能優化

        function Parent({ a, b }) {
          // Only re-rendered if `a` changes:
          const child1 = useMemo(() => <Child1 a={a} />, [a]);
          // Only re-rendered if `b` changes:
          const child2 = useMemo(() => <Child2 b={b} />, [b]);
          return (
            <> {child1} {child2} </> ) } 複製代碼
    • useImperativeHandle(ref, createHandle, [deps])

      • 當使用 ref 來給父組件提供實例時,用來提供自定義的方法屬性

      • 儘可能避免使用

      • 應該與 forwardRef 一塊兒使用

        function FancyInput(props, ref) {
          const inputRef = useRef();
          useImperativeHandle(ref, () => ({
            focus: () => {
              inputRef.current.focus();
            }
          }));
          return <input ref={inputRef} />; } FancyInput = forwardRef(FancyInput); 複製代碼
        function Parent() {
          const fancyInputRef = useRef();
          return (
            <>
              <FancyInput ref={fancyInputRef} />
              <button onClick={()=>{
                  fancyInputRef.current.focus()
              }}>Focus</button>
            </>
          )
        }
        
        複製代碼
相關文章
相關標籤/搜索