React Hook 系列(一):完全搞懂react hooks 用法(長文慎點)

用心閱讀,跟隨codesandbox demo或運行源碼,你將熟悉react各類組件的優缺點及用法,完全熟悉react hook的用法,收益應該不小😀😀😀javascript

大綱:html

  • react 不一樣組件用法。
  • react hook 相比之前帶來什麼。
  • react hook 的用法。

React 組件的發展

1. 功能(無狀態)組件

Functional (Stateless) Component,功能組件也叫無狀態組件,通常只負責渲染。java

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
複製代碼

2. 類(有狀態)組件

Class (Stateful) Component,類組件也是有狀態組件,也能夠叫容器組件。通常有交互邏輯和業務邏輯。react

class Welcome extends React.Component {
	state = {
		name: ‘tori’,
	}
  componentDidMount() {
		fetch(…);
		…
	}
  render() {
    return (
		<> <h1>Hello, {this.state.name}</h1> <button onClick={() => this.setState({name: ‘007’})}>更名</button> </> ); } } 複製代碼

3. 渲染組件

Presentational Component,和功能(無狀態)組件相似。git

const Hello = (props) => {
  return (
    <div> <h1>Hello! {props.name}</h1> </div>
  )
}
複製代碼

**📢 總結: **github

  • 函數組件必定是無狀態組件,展現型組件通常是無狀態組件;
  • 類組件既能夠是有狀態組件,又能夠是無狀態組件;
  • 容器型組件通常是有狀態組件。
  • 劃分的原則歸納爲: 分而治之、高內聚、低耦合

經過以上組件之間的組合能實現絕大部分需求。json

4. 高階組件

Higher order components (HOC)redux

HOC 主要是抽離狀態,將重複的受控組件的邏輯抽離到高階組件中,以新的props傳給受控組件中,高階組件中能夠操做props傳入受控組件。 開源庫中常見的高階組件:Redux的connect, react-router的withRouter等等。api

Class HocFactory extends React.Component {
    constructor(props) {
      super(props)
    }
	// 操做props
	// …
    render() {
      const newProps = {…};
      return (Component) => <Component {…newProps} />;
    }
} 
Const Authorized = (Component) => (permission) => {
	return Class Authorized extends React.Component {
		…
		render() {
			const isAuth = ‘’;
			return isAuth ? <Component /> : <NoMatch />; } } } // 項目中涉及到的高階組件 // 主要做用是將全部action經過高階組件代理到component的Pro上。 import { bindActionCreators } from ‘redux’; import { connect } from ‘react-redux'; // 全部頁面action集合 import * as actions from './actions'; // 緩存actions, 避免render從新加載 let cachedActions; // action經過bindActionCreators綁定dispatch, const bindActions = (dispatch, ownProps) => { if (!cachedActions) { cachedActions = { dispatch, actions: bindActionCreators(actions, dispatch), }; } return cachedActions; }; const connectWithActions = ( mapStateToProps, mergeProps, options ) => (component) => connect( mapStateToProps, bindActions, mergeProps, options )(component); export default connectWithActions; // 相似還有log中間件樣子的等等。 複製代碼

HOC的不足

  • HOC產生了許多無用的組件,加深了組件層級,性能和調試受影響。
  • 多個HOC 同時嵌套,劫持props, 命名可能會衝突,且內部沒法判斷Props是來源於那個HOC。

5. Render Props

Render Props 你能夠把它理解成 JavaScript 中的回調函數數組

// 實現一個控制modal visible的高階組件
class ToggleVisible extends React.Component {
	state = {
		visible: false
	};
	toggle = () => {
		this.setState({visible: !this.state.visible});
	}
	render() {
		return (
		    <>{this.props.children({visible, toggle})}</>
		);
	}
}
//使用
const EditUser = () => (
	<ToggleVisible>
		{({visible, toggle}) => (<>
		    <Modal visible={visible}/>
		    <Button onClick={toggle}>打開/關閉modal</Button>
		</>)}
	</ToggleVisible>
)
複製代碼

📢 優勢

  • 組件複用不會產生多餘的節點,也就是不會產生多餘的嵌套。
  • 不用擔憂props命名問題。

6. 組合式組件(Compound Component)

子組件所須要的props在父組件會封裝好,引用子組件的時候就不必傳遞全部props了。組合組件核心的兩個方法是React.Children.map和React.cloneElement。

例以下面 子組件須要的click事件轉移到了父組件,經過父組件內部封裝到子組件上,ant-design的不少group組件用到了此方法。

參考文章

class GroupButton extends React.PureComponent {
  state = {
    activeIndex: 0
  };
  render() {
    return (
      <> {React.Children.map(this.props.children, (child, index) => child.type ? React.cloneElement(child, { active: this.state.activeIndex === index, onClick: () => { this.setState({ activeIndex: index }); this.props.onChange(child.props.value); } }) : child )} </> ); } } // 用法 <GroupButton onChange={e => { console.log(「onChange」, e); }} > <Button value="red">red</Button> <Button value="yellow">yellow</Button> <Button value=「blue」>blue</Button> <Button value="white">white</Button> </GroupButton> 複製代碼

🙊 廢話結束,開始進入正題。。。

👍 React hooks

Hook 出現以前,組件之間複用狀態邏輯很難,解決方案(HOC、Render Props)都須要從新組織組件結構, 且代碼難以理解。在React DevTools 中觀察過 React 應用,你會發現由 providers,consumers,高階組件,render props 等其餘抽象層組成的組件會造成「嵌套地獄」。

組件維護愈來愈複雜,譬如事件監聽邏輯要在不一樣的生命週期中綁定和解綁,複雜的頁面componentDidMount包涵不少邏輯,代碼閱讀性變得不好。

class組件中的this難以理解,且class 不能很好的壓縮,而且會使熱重載出現不穩定的狀況。更多引子介紹參見官方介紹

因此hook就爲解決這些問題而來:

  • 避免地獄式嵌套,可讀性提升。
  • 函數式組件,比class更容易理解。
  • class組件生命週期太多太複雜,使函數組件存在狀態。
  • 解決HOC和Render Props的缺點。
  • UI 和 邏輯更容易分離。

下面逐一介紹官方提供的hook API。

1. useState

📢 函數組件有狀態了

const [state, setState] = useState(initialState); state爲變量,setState 修改 state值的方法, setState也是異步執行。

DEMO1

class this.setState更新是state是合併, useState中setState是替換。

function Example() {
  // 聲明一個叫 "count" 的 state 變量
  const [count, setCount] = useState(0);
	const [obj, setData] = useState();
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
  );
}
複製代碼

2. useEffect

📢 忘記生命週期,記住反作用

useEffect(() =>  {// Async Action}, ?[dependencies]); // 第二參數非必填
複製代碼

DEMO2

function Hook2() {
  const [data, setData] = useState();
  useEffect(() => {
    console.log("useEffect");
  });
  return (
    <div> {(() => { console.log("render"); return null; })()} <p>data: {JSON.stringify(data)}</p> </div>
  );
}
複製代碼

執行結果:

結論:

  • useEffect 是在render以後生效執行的。

DEMO3

import React, { useState, useEffect, useRef } from 「react」;
function Demo3() {
  const [data, setData] = useState();
  useEffect(() => {
    console.log("useEffect—[]」);
    fetch(「https://www.mxnzp.com/api/lottery/common/latest?code=ssq」)
      .then(res => res.json())
      .then(res => {
        setData(res);
      });
  }, []);

  useEffect(() => {
    console.log("useEffect ---> 無依賴");
  });

  useEffect(() => {
    console.log(「useEffect 依賴data: data發生了變化」);
  }, [data]);

  return (
    <div>
      <p>data: {JSON.stringify(data)}</p>
    </div>
  );
}
export default Demo3;
複製代碼

執行結果:

結論:

  • effect在render後按照先後順序執行。
  • effect在沒有任何依賴的狀況下,render後每次都按照順序執行。
  • effect內部執行是異步的。
  • 依賴[]能夠實現相似componentDidMount的做用,但最好忘記生命週期, 只記反作用。

DEMO4

import React, { useState, useEffect, useRef } from "react";

function Demo4() {
  useEffect(() => {
    console.log(「useEffect1」);
    const timeId = setTimeout(() => {
      console.log(「useEffect1-setTimeout-2000」);
    }, 2000);
    return () => {
      clearTimeout(timeId);
    };
  }, []);
  useEffect(() => {
    console.log("useEffect2");
    const timeId = setInterval(() => {
      console.log("useEffect2-setInterval-1000");
    }, 1000);
    return () => {
      clearInterval(timeId);
    };
  }, []);
  return (
    <div> {(() => { console.log(「render」); return null; })()} <p>demo4</p> </div>
  );
}
export default Demo4;
複製代碼

執行結果:

結論:

  • effect回調函數是按照前後順序同時執行的。
  • effect的回調函數返回一個匿名函數,至關於componentUnMount的鉤子函數,通常是remove eventLisenter, clear timeId等,主要是組件卸載後防止內存泄漏。

綜上所述,useEffect 就是監聽每當依賴變化時,執行回調函數的存在函數組件中的鉤子函數。

3. useContext

跨組件共享數據的鉤子函數

const value = useContext(MyContext);
// MyContext 爲 context 對象(React.createContext 的返回值) 
// useContext 返回MyContext的返回值。
// 當前的 context 值由上層組件中距離當前組件最近的<MyContext.Provider> 的 value prop 決定。
複製代碼

DEMO5

import React, { useContext, useState } from 「react」;
const MyContext = React.createContext();
function Demo5() {
  const [value, setValue] = useState("init」);
  console.log(「Demo5」);
  return (
    <div>
      {(() => {
        console.log("render");
        return null;
      })()}
      <button onClick={() => {
        console.log('click:更新value')
        setValue(`${Date.now()}_newValue`)
      }}>
        改變value
      </button>
      <MyContext.Provider value={value}>
        <Child1 />
        <Child2 />
      </MyContext.Provider>
    </div>
  );
}

function Child1() {
  const value = useContext(MyContext);
  console.log(「Child1-value」, value);
  return <div>Child1-value: {value}</div>;
}

function Child2(props) {
  console.log(‘Child2’)
  return <div>Child2</div>;
}
複製代碼

執行結果:

結論:

  • useContext 的組件總會在 context 值變化時從新渲染, 因此<MyContext.Provider>包裹的越多,層級越深,性能會形成影響。

  • <MyContext.Provider> 的 value 發生變化時候, 包裹的組件不管是否訂閱content value,全部組件都會重新渲染。

  • demo中child2 不該該rerender, 如何避免沒必要要的render?*
    使用React.memo優化。

const Child2 = React.memo((props) => {
  return <div>Child2</div>;
})
複製代碼

執行結果:

注意: 默認狀況下React.memo只會對複雜對象作淺層對比,若是你想要控制對比過程,那麼請將自定義的比較函數經過第二個參數傳入來實現。 參考連接

4. useRef

傳送門

const refContainer = useRef(initialValue);
複製代碼
  • useRef 返回一個可變的 ref 對象, 和自建一個 {current: …} 對象的惟一區別是,useRef 會在每次渲染時返回同一個 ref 對象, 在整個組件的生命週期內是惟一的。
  • useRef 能夠保存任何可變的值。其相似於在 class 中使用實例字段的方式。 總結:
  • useRef 能夠存儲那些不須要引發頁面從新渲染的數據
  • 若是你刻意地想要從某些異步回調中讀取 /最新的/ state,你能夠用 一個 ref 來保存它,修改它,並從中讀取。

5. useReducer

const [state, dispatch] = useReducer(reducer, initialState);
複製代碼

reducer就是一個只能經過actionstate從一個過程轉換成另外一個過程的純函數; useReducer就是一種經過(state,action) => newState的過程,和redux工做方式同樣。數據流: dispatch(action) => reducer更新state => 返回更新後的state

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() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); } 複製代碼

官方推薦如下場景須要useReducer更佳:

  • state 邏輯較複雜且包含多個子值, 能夠集中處理。
  • 下一個 state 依賴於以前的 state 。
  • 想更穩定的構建自動化測試用例。
  • 想深層級修改子組件的一些狀態,使用 useReducer 還能給那些會觸發深更新的組件作性能優化,由於 你能夠向子組件傳遞 dispatch 而不是回調函數
  • 使用reducer有助於將讀取與寫入分開。 DEMO6
const fetchReducer = (state, action) => {
  switch (action.type) {
    case 「FETCH_INIT":
      return {
        ...state,
        loading: true,
        error: false
      };
    case 「FETCH_SUCCESS」:
      return {
        ...state,
        loading: false,
        error: false,
        data: action.payload
      };
    case "FETCH_FAIL":
      return {
        …state,
        loading: false,
        error: true
      };
    default:
      throw new Error();
  }
};

function Demo6() {
  const [state, dispatch] = useReducer(fetchReducer, {
    loading: false,
    error: false,
    msg: "",
    data: {}
  });

  const getData = useCallback(async () => {
    try {
      dispatch({ type: "FETCH_INIT" });
      const response = await fetch(
        "https://www.mxnzp.com/api/lottery/common/latest?code=ssq"
      );
      const res = await response.json();

      if (res.code) {
        dispatch({ type: "FETCH_SUCCESS", payload: res.data });
      } else {
        dispatch({ type: 「FETCH_FAIL」, payload: res.msg });
      }
    } catch (error) {
      dispatch({ type: 「FETCH_FAIL」, payload: error });
    }
  }, []);

  useEffect(() => {
    getData();
  }, [getData]);

  return (
    <Loading loading={state.loading}>
      <p>開獎號碼: {state.data.openCode}</p>
    </Loading>
  );
}
複製代碼

demo6useReducer處理了多個能夠用useState實現的邏輯,包括loading, error, msg, data

useContext 和 useReducer模擬redux管理狀態

import React, { useReducer, useContext } from 「react」;

const ModalContext = React.createContext();

const visibleReducer = (state, action) => {
  switch (action.type) {
    case 「CREATE」:
      return { ...state, ...action.payload };
    case "EDIT":
      return { ...state, ...action.payload };
    default:
      return state;
  }
};
function Demo7() {
  const initModalVisible = {
    create: false,
    edit: false
  };
  const [state, dispatch] = useReducer(visibleReducer, initModalVisible);

  return (
    <ModalContext.Provider value={{ visibles: state, dispatch }}> <Demo7Child /> </ModalContext.Provider> ); } function Demo7Child() { return ( <div> Demo7Child <Detail /> </div> ); } function Detail() { const { visibles, dispatch } = useContext(ModalContext); console.log("contextValue", visibles); return ( <div> <p>create: {`${visibles.create}`}</p> <button onClick={() => dispatch({ type: "CREATE", payload: { create: true } })} > 打開建立modal </button> </div> ); } export default Demo7; 複製代碼

邏輯很清晰的抽離出來,context value中的值不須要在組件中透傳,即用即取DEMO7

注意 React 會確保 dispatch 函數的標識是穩定的,而且不會在組件從新渲染時改變。這就是爲何能夠安全地從 useEffect 或 useCallback 的依賴列表中省略 dispatch。

6. useCallback

語法:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
複製代碼

返回一個 memoized 回調函數。

useCallback解決了什麼問題?先看DEMO8

import React, { useRef, useEffect, useState, useCallback } from 「react」;

function Child({ event, data }) {
  console.log("child-render");
  // 第五版
  useEffect(() => {
    console.log(「child-useEffect」);
    event();
  }, [event]);
  return (
    <div> <p>child</p> {/* <p>props-data: {data.data && data.data.openCode}</p> */} <button onClick={event}>調用父級event</button> </div>
  );
}

const set = new Set();

function Demo8() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState({});

  // 初版
  // const handle = async () => {
  // const response = await fetch(
  // "https://www.mxnzp.com/api/lottery/common/latest?code=ssq"
  // );
  // const res = await response.json();
  // console.log("handle", data);
  // setData(res);
  // };

  // 第二版
  // const handle = useCallback(async () => {
  // const response = await fetch(
  // 「https://www.mxnzp.com/api/lottery/common/latest?code=ssq"
  // );
  // const res = await response.json();
  // console.log(「handle」, data);
  // setData(res);
  // });

  // 第三版
  // const handle = useCallback(async () => {
  // const response = await fetch(
  // 「https://www.mxnzp.com/api/lottery/common/latest?code=ssq」
  // );
  // const res = await response.json();
  // setData(res);
  // console.log(「useCallback」, data);
  // // eslint-disable-next-line react-hooks/exhaustive-deps
  // }, []);

  // // 第四版
  // const handle = useCallback(async () => {
  // const response = await fetch(
  // 「https://www.mxnzp.com/api/lottery/common/latest?code=ssq"
  // );
  // const res = await response.json();
  // setData(res);
  // console.log(「parent-useCallback", data);
  // // eslint-disable-next-line react-hooks/exhaustive-deps
  // }, []);

  // 第五版
  const handle = useCallback(async () => {
    const response = await fetch(
      "https://www.mxnzp.com/api/lottery/common/latest?code=ssq"
    );
    const res = await response.json();
    setData(res);
    console.log("parent-useCallback", data);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [count]);
  set.add(handle);

  console.log(「parent-render====>」, data);
  return (
    <div> <button onClick={e => { setCount(count + 1); }} > count++ </button> <p>set size: {set.size}</p> <p>count:{count}</p> <p>data: {data.data && data.data.openCode}</p> <p>-------------------------------</p> <Child event={handle} /> </div> ); } export default Demo8; 複製代碼

結論:

  • 初版:每次render,handle都是新的函數,且每次都能拿到最新的data。
  • 第二版:用useCallback包裹handle,每次render, handle也是新的函數,且每次都能拿到最新的data, 和一版效果同樣, 因此不建議這麼用。
  • 第三版:useCallback假如第二個參數deps,handle會被memoized, 因此每次data都是第一次記憶時候的data(閉包)。
  • 第四版: useCallback依賴count的變化,每當useCallback 變化時,handle會被從新memoized。
  • 第五版:每當count變化時,傳入子組件的函數都是最新的,因此致使child的useEffect執行。 總結:
  • useCallback將返回一個記憶的回調版本,僅在其中一個依賴項已更改時才更改。
  • 當將回調傳遞給依賴於引用相等性的優化子組件以防止沒必要要的渲染時,此方法頗有用。
  • 使用回調函數做爲參數傳遞,每次render函數都會變化,也會致使子組件rerender, useCallback能夠優化rerender。 疑問:如何優化子組件沒必要要的渲染?

7. useMemo

語法: const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);; 返回一個 memoized 值,和useCallback同樣,當依賴項發生變化,纔會從新計算 memoized 的值,。 useMemo和useCallback不一樣之處是:它容許你將 memoized 應用於任何值類型(不只僅是函數)。 DEMO9

import React, { useState, useMemo } from 「react」;

function Demo9() {
  const [count, setCount] = useState(0);
  const handle = () => {
    console.log(「handle」, count);
    return count;
  };

  const handle1 = useMemo(() => {
    console.log("handle1", count);
    return count;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handle2 = useMemo(() => {
    console.log(「handle2」, count);
	  // 大計算量的方法
    return count;
  }, [count]);

  console.log("render-parent");

  return (
    <div> <p> demo9: {count} <button onClick={() => setCount(count + 1)}>++count</button> </p> <p>-------------------</p> <Child handle={handle1} /> </div> ); } function Child({ handle }) { console.log("render-child"); return ( <div> <p>child</p> <p>props-data: {handle}</p> </div> ); } export default Demo9; 複製代碼

總結:

  • useMemo 會在render前執行。
  • 若是沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值。
  • useMemo用於返回memoize,防止每次render時大計算量帶來的開銷。
  • 使用useMemo優化需謹慎, 由於優化自己也帶來了計算,大多數時候,你不須要考慮去優化沒必要要的從新渲染

其餘Hook

1. useImperativeHandle

// ref:須要傳遞的ref
// createHandle: 須要暴露給父級的方法。
// deps: 依賴
useImperativeHandle(ref, createHandle, [deps])
複製代碼

useImperativeHandle 應當與forwardRef一塊兒使用。先看DEMO10

import React, {
  useRef,
  forwardRef,
  useImperativeHandle,
  useEffect,
  useState
} from "react";

const Child = forwardRef((props, ref) => {
  const inputEl = useRef();
  const [value, setVal] = useState("");
  // 初版
  // useImperativeHandle(ref, () => {
  //   console.log("useImperativeHandle");
  //   return {
  //     value,
  //     focus: () => inputEl.current.focus()
  //   };
  // });

  // 第二版
  useImperativeHandle(
    ref,
    () => {
      console.log(「useImperativeHandle");
      return {
        value,
        focus: () => inputEl.current.focus()
      };
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    []
  );

  return (
    <input
      ref={inputEl}
      onChange={e => setVal(e.target.value)}
      value={value}
      {...props}
    />
  );
});
function Demo10() {
  const inputEl = useRef(null);

  useEffect(() => {
    console.log(「parent-useEffect」, inputEl.current);
    inputEl.current.focus();
  }, []);

  function click() {
    console.log("click:", inputEl.current);
    inputEl.current.focus();
  }
  console.log(「Demo10」, inputEl.current);
  return (
    <div>
      <Child ref={inputEl} />
      <button onClick={click}>click focus</button>
    </div>
  );
}
export default Demo10;
複製代碼

結論:

  • useImperativeHandle在當前組件render後執行。
  • 初版:沒有deps,每當rerender時,useImperativeHandle都會執行, 且能拿到 state中最新的值, 父組件調用傳入的方法也是最新。
  • 第二版: 依賴[],每當rerender時,useImperativeHandle不會執行,且不會更新到父組件。
  • 第三版:依賴傳入的state值 [value], 達到想要的效果。

2. useDebugValue

不經常使用, 只能在React Developer Tools看到,詳見官方傳送門

DEMO11

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(false);
  // 在開發者工具中的這個 Hook 旁邊顯示標籤
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? "Online" : "Offline");
  return isOnline;
}
function Demo11() {
  const isOnline = useFriendStatus(567);
  return <div>朋友是否在線:{isOnline ? "在線" : "離線"}</div>;
}
複製代碼
image-20191212232215094

3. useLayoutEffect

不多用,與 useEffect 相同,但它會在全部的 DOM 變動以後同步調用 effect, 詳見官方傳送門


📢 最後

歡迎交流,,,😀🍻🍻😀

下期預告

《自定義 Hook 在項目中的實踐》

相關閱讀:

舒適提示:**多伸懶腰,對身體好

相關文章
相關標籤/搜索