React Hooks 手把手體驗

React Hooks

  • Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性
  • 若是你在編寫函數組件並意識到須要向其添加一些 state,之前的作法是必須將其它轉化爲 class。如今你能夠在現有的函數組件中使用 Hook

解決的問題

  • 在組件之間複用狀態邏輯很難,可能要用到render props和高階組件,React 須要爲共享狀態邏輯提供更好的原生途徑,Hook 使你在無需修改組件結構的狀況下複用狀態邏輯
  • 難以理解的 class,包括難以捉摸的this
  • 複雜組件變得難以理解,Hook將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據)

注意事項

  • 只能在函數最外層調用 Hook。不要在循環、條件判斷或者子函數中調用。
  • 只能在 React 的函數組件中調用 Hook。不要在其餘 JavaScript 函數中調用

開始學習

不知道你是否已經熟練掌握官方基礎的hooks
做爲剛學習react的小白和你們一塊兒學習下,下面會以小例子的形式來實戰react

useState

  • 經過在函數組件裏調用它來給組件添加一些內部 state,React 會在重複渲染時保留這個 state
  • useState 會返回一對值:當前狀態和一個讓你更新它的函數,你能夠在事件處理函數中或其餘一些地方調用這個函數。它相似 class 組件的 this.setState,可是它不會把新的 state 和舊的 state 進行合併
  • useState 惟一的參數就是初始 state
  • 返回一個 state,以及更新 state 的函數
    • 在初始渲染期間,返回的狀態 (state) 與傳入的第一個參數 (initialState) 值相同
    • setState 函數用於更新 state。它接收一個新的 state 值並將組件的一次從新渲染加入隊列
const [state, setState] = useState(initialState);
複製代碼

咱們先來一個小例子看下效果吧ios

const Counter = (props: IParams) => {
  const [number, setNumber] = useState({
    sum: 0
  })
  const alertNumber = function () {
    setTimeout(() => {
      console.log(`value ${number.sum}`)
    }, 1500);
  }
  return (
    <div>
      <div>{number.sum}</div>
      {/* <button onClick={() => setNumber({ sum: number.sum + 1 })}>plus</button>
      <button onClick={() => setNumber({sum: number.sum - 1})}>minis</button> */}
      <button onClick={() => setNumber((state)=>({ sum: state.sum + 1 }))}>plus</button>
      <button onClick={() => setNumber((state) => ({ sum: state.sum - 1 }))}>minis</button>
      <button onClick={() => alertNumber()}>打印數據</button>
    </div>
  )
}

複製代碼

咱們能夠看到效果:
json

咱們操做添加和減小能夠修改 number
咱們修改 number值,能夠經過 setNumber傳入當前新對象,或者一個函數返回新對象值
咱們點擊打印數據的時候操做添加或減小,能夠發現打印數據的值和當前不同,因此咱們得出總結:

每次渲染都是獨立的閉包redux

  • 每一次渲染都有它本身的 Props and State
  • 每一次渲染都有它本身的事件處理函數
  • 咱們的組件函數每次渲染都會被調用,可是每一次調用中number值都是常量,而且它被賦予了當前渲染中的狀態值
  • 在單次渲染的範圍內,Props and State始終保持不變

咱們再來試下lazy新增的例子:axios

// lazy新增
const Counter1 = (props: IParams) => {
  const [number, setNumber] = useState({
    sum: 0
  })
  // 不使用函數更新 若是有異步的操做,伴隨着同步更新的操做會致使數據迴流
  const lazy = function () {
    setTimeout(() => {
      setNumber({
        sum: number.sum + 1
      })
    }, 1500);
  }
  const lazyUpdate = function () {
    setTimeout(() => {
      setNumber(state => ({
        sum: state.sum + 1
      }))
      setNumber(state => ({
        sum: state.sum + 1
      }))
    }, 1500);
  }
  return (
    <div>
      <div>{number.sum}</div>
      <button onClick={() => setNumber((state)=>({ sum: state.sum + 1 }))}>plus</button>
      <button onClick={lazy}>lazy</button>
      <button onClick={lazyUpdate}>lazyUpdate</button>
    </div>
  )
}

複製代碼

咱們點擊lazy新增以後再點擊plus新增 效果能夠看到,最新對象的sum值只加了1 api

咱們點擊 lazyUpdate新增以後再點擊 plus新增 效果能夠看到,lazyUpdate和plus新增的操做都疊加上去了,最終的sum值爲3
咱們再試下一種場景,咱們先點擊 lazyUpdate新增以後再點擊 lazy新增 效果能夠看到,咱們的頁面會先顯示2,再顯示1
咱們來分析下結論:
第一場景咱們觸發了 lazy函數添加,異步函數內部用到的number是更新以前的,對象內sum值爲0,延遲1s以後加1了,sum值一秒後變爲1,咱們又觸發了同步的 setNumber((state)=>({ sum: state.sum + 1 }))最新的結果sum爲1,當前同步值和異步更新值都是1, 因此顯示不會變

第二個場景 咱們都是使用函數式更新去更新同步和異步的修改,咱們先調用了異步的lazyUpdate函數,可是新增的操做不是當即執行,是等1s後執行,在此期間,咱們又調用了同步的新增,這是同步直接執行,sum值變爲1,異步函數新增state函數觸發,拿到了最新的state新增以後就會使sum變爲3。
數組

第三種場景 經過上述2種場景,這種場景咱們也就清楚了,同時調用2個異步新增函數,確定會以最後觸發的異步函數爲結果,咱們觸發的時候lazy函數調用的時候,sum值是拿的初始化值0,異步接口調用完以後變爲1,而lazyUpdate函數先觸發,也是拿的初始化值去計算,新增完sum值會變爲2因此會出現先出現2,後出現1的狀況
瀏覽器

函數式更新緩存

  • 若是新的 state 須要經過使用先前的 state 計算得出,那麼能夠將函數傳遞給 setState。該函數將接收先前的 state,並返回一個更新後的值

覺得這樣就結束了麼?咱們再看下 惰性初始state
咱們來看個例子bash

const Counter2 = (props: IParams) => {
  const [obj, setObj] = useState(function() {
    return {
      sum: 0,
      title: '測試數據'
    }
  });
  return (
    <div>
      <div>{obj.title}: {obj.sum}</div>
      <button onClick={() => setObj((state) => ({ ...state, sum: state.sum + 1 }))}>plus</button>
      <button onClick={() => setObj((state) => ({ sum: state.sum + 1 }))}>plus</button>
      <button onClick={() => setObj(obj)}>state</button>
    </div>
  )
}

複製代碼

咱們能夠看到頁面上的展現

咱們觸發了第一個plus按鈕, 顯示正常對象中的sum值加1
咱們再觸發第二個plus按鈕, 返回了sum值加1頁面上顯示的title值丟失
因此咱們能得出結論:

  • initialState 參數只會在組件的初始渲染中起做用,後續渲染時會被忽略
  • 若是初始 state 須要經過複雜計算得到,則能夠傳入一個函數,在函數中計算並返回初始的 state,此函數只在初始渲染時被調用
  • 與 class 組件中的 setState 方法不一樣,useState 不會自動合併更新對象。你能夠用函數式的 setState 結合展開運算符來達到合併更新對象的效果

還有要補充的麼?真的沒有了,咱們看下一個吧

useCallback

useCallback能夠幫我作一些心梗優化,減小渲染次數

  • 把內聯回調函數及依賴項數組做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新

咱們來看個例子:

/**
 * useCallback 只有當依賴變化的時候函數纔會變化
 * 把內聯回調函數及依賴項數組做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新
 * @param props 
 */
let lastSetNumberBack:any;
let lastSetNameBack:any;
const Counter3 = (props: IParams) => {
  const [number, setNumber] = useState(0);
  const [name, setName] = useState('wp');
  const setNumberCallback = useCallback(() => setNumber((number + 1)), [number]);
  console.log('lastSetNumberBack === setNumberCallback', lastSetNumberBack === setNumberCallback);
  lastSetNumberBack = setNumberCallback;
  const setNameCallback = useCallback(() => setName(Date.now() + ''), [name]);
  console.log('lastSetNameBack === setNameCallback', lastSetNameBack === setNameCallback);
  lastSetNameBack = setNameCallback;

  return (
    <div>
      <div>{name}:{number}</div>
      <button onClick={setNumberCallback}>修改數字</button>
      <button onClick={setNameCallback}>修更名稱</button>
    </div>
  )
}

ReactDom.render(<Counter3 />, document.getElementById('root'));
複製代碼

咱們這邊設置了2個緩存變量來比對,判斷下當前的函數和以前的函數是否一致

經過展現內容能夠看到:

  • useCallback經過傳入依賴值來判斷當前回調函數是否須要更新
  • 若是當前的依賴未改動的話,useCallBack仍是返回以前的函數

說到依賴值的優化問題,咱們能夠再說下useMemo,useEffect最後再說

useMemo

  • 把建立函數和依賴項數組做爲參數傳入 useMemo,它僅會在某個依賴項改變時才從新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算
interface ChildProps { 
  data: Record<string, number>,
  addNumber: Function
}
/**
 * 子類
 */
function Child(props: ChildProps) {
  console.log('child render');
  return (
    <div>
      <div>
        {props.data.number}
        <button onClick={()=>props.addNumber()}>+</button>
      </div>
    </div>
  )
}

/**
 * 父類
 */
function App() {
  const [number, setNumber] = useState(0);
  const [name, setName] = useState('wp');
  function changeObj(number: number) {
    console.log('===changeObj===')
    return { number }
  }
  const data = changeObj(number);
  return (
    <div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <Child addNumber={() => setNumber(number + 1)} data={data}/>
    </div>
  )
}

/**
 * 父類
 * 把建立函數和依賴項數組做爲參數傳入 useMemo,它僅會在某個依賴項改變時才從新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算
 */
let lastAddClick:Function;
let lastData:Record<string, number>;
function App1() {
  const [number, setNumber] = useState(0);
  const [name, setName] = useState('wp');
  const addNumber = useCallback(() => setNumber(number => number + 1), [number]);
  console.log('lastAddClick===addNumber', lastAddClick === addNumber)
  lastAddClick = addNumber;
  const data = useMemo(() => {
    console.log('===App1 useMemo===')
    return { number }
  }, [number]);
  console.log('data===lastData', data === lastData);
  lastData = data;
  
  return (
    <div>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <Child addNumber={()=>addNumber()} data={data}/>
    </div>
  )
}
複製代碼

咱們先使用App和App1組件來作比較
咱們看下App1組件調用的結果

咱們能夠看到,使用了 useMemo,咱們能夠像使用 useCallback同樣使用緩存,可是 useMemouseCallback的對象不同, useMemo服務的地方多是 一個變量,多是 一個dom元素,只要依賴值不變,對應的值就不會變

接下來,讓咱們來探索下useEffect吧

useEffect

  • 在函數組件主體內(這裏指在 React 渲染階段)改變 DOM、添加訂閱、設置定時器、記錄日誌以及執行其餘包含反作用的操做都是不被容許的,由於可能會產生莫名其妙的 bug 並破壞 UI 的一致性
  • 使用 useEffect 完成反作用操做。賦值給 useEffect 的函數會在組件渲染到屏幕以後執行。你能夠把 effect 看做從 React 的純函數式世界通往命令式世界的逃生通道
  • useEffect 就是一個 Effect Hook,給函數組件增長了操做反作用的能力。它跟 class 組件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具備相同的用途,只不過被合併成了一個 API

下面咱們經過useEffect來模擬class函數中的ComponentDidMount和ComponentWillUnmount

type StateParam = {
  num: number
}

/**
 * useEffect 使用
 * @param props 
 */
function Counter1(props: any) {
  const [state, setState] = useState({ num: 0 });
  useEffect(() => { 
    document.title = `當前數據${state.num}`
  }, [state])
  // 第二個參數設置爲[],則表示只會執行一次
  useEffect(() => { 
    const $time = setInterval(() => {
      setState(data => ({
        num: data.num + 1
      }))
    }, 1000);
    // 爲了防止內存泄露,effect的返回函數會在銷燬的時候執行
    // 爲防止內存泄漏,清除函數會在組件卸載前執行。另外,若是組件屢次渲染,則在執行下一個 effect 以前,上一個 effect 就已被清除
    return () => clearInterval($time);
  }, [])
  return <div>
    <div>{state.num}</div>
    <button onClick={() => { 
      setState(data => ({
        num: data.num + 1 
      }))
    }}>+</button>
  </div>
}
複製代碼

能夠根據結果來分析下:

  • useEffect根據依賴值的變化而調用
  • useEffect經過傳入 [] ,來實現只調用一次的函數,模擬了 componentDidMount ,咱們useEffect的反作用函數裏面調用了setInterval每秒執行+1操做,一直執行的話可能會致使內存泄露,那就須要調用class的ComponentWillUnmount去清除掉interval,好在useEffect提供給咱們回調函數給咱們清除反作用
  • 每次咱們從新渲染,都會生成新的 effect,替換掉以前的。某種意義上講,effect 更像是渲染結果的一部分 —— 每一個 effect 屬於一次特定的渲染

說到useEffect,咱們就會聯想到useReducer hook

useReducer

第一眼看上去,給人感受是能夠替代 redux 數據流是麼?用法真的是同樣同樣的,其實並非,後面會提到

  • useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法
  • 在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於以前的 state 等

咱們來看一下useReducer的使用

type StateParam = {
  number: number
}

function reducer(prevState: StateParam, action: any) {
  switch (action.type) {
    case 'INCREMENT':
      return {number: prevState.number + 1}
      break;
    case 'DECREMENT':
      return {number: prevState.number - 1}
      break;
    default:
      return prevState;
      break;
  }
}
/**
 * useReducer使用
 */
function App() {
  const initState: StateParam = { number: 0 };
  const [state, dispath] = useReducer(reducer, initState)
  return (
    <div>
      <div>number: {state.number}</div>
      <button onClick={() => dispath({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispath({ type: 'DECREMENT' })}>-</button>
    </div>
  )
}
複製代碼

像極了redux的用法是否是

useReducerredux概念同樣,會返回一個 state 值和 dispatch 值, state 用來顯示狀態, dispatch 調用 action ,派發給 reducer 處理結果
都說 useReducer 能夠替代 useEffect 了,那咱們來用 useReducer 實現個 useEffect

/**
 * 經過useReducer來重寫useState鉤子函數
 */
type StatePram = Record<string, any>;
function useState(initState: StatePram) {
  const [state, dispatch] = useReducer((prevState: StatePram, action: any) => action, initState);
  function setState(data: StatePram) {
    return dispatch(data)
  }
  return [state, setState];
}

function App1() {
  const initState: StateParam = { number: 0 };
  // const [state, dispath] = useReducer(reducer, initState)
  const [obj, setObj] = useState(initState)
  return (
    <div>
      <div>number: {obj.number}</div>
      <button onClick={() => setObj({ number: obj.number + 1 })}>+</button>
      <button onClick={() => setObj({ number: obj.number - 1 })}>-</button>
    </div>
  )
}
複製代碼

效果和上面同樣

使用 useReducer 如何重寫 useEffect 鉤子函數呢? 首先 useReducer 返回的是state和dispatch, 咱們經過 setState函數 去調用 dispatch函數 ,觸發咱們的 reducer ,咱們這邊的 reducer函數 是接收到 action 返回 action 值作爲最新狀態值,從而實現了 useEffect 的功能
事實擺在面前,useReducer牛皮? 確實這麼牛麼?咱們後續會分析

useContext

  • 接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值
  • 當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider> 的 value prop 決定
  • 當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值
  • useContext(MyContext) 至關於 class 組件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是讓你可以讀取 context 的值以及訂閱 context 的變化。你仍然須要在上層組件樹中使用 <MyContext.Provider> 來爲下層組件提供 context

使用例子

type StateParam = {
  number: number
}

function reducer(prevState: StateParam, action: any) {
  switch (action.type) {
    case 'INCREMENT':
      return {number: prevState.number + 1}
      break;
    case 'DECREMENT':
      return {number: prevState.number - 1}
      break;
    default:
      return prevState;
      break;
  }
}
const AppContext: Context<ContextParam> = React.createContext({});
/**
 * useReducer
 */
function App() {
  const initState: StateParam = { number: 0 };
  const [state, dispath] = useReducer(reducer, initState)
  return (
    <AppContext.Provider value={{ state, dispath }}>
      <Child />
    </AppContext.Provider>
  )
}

interface ContextParam { 
  state?: any,
  dispath?: Function
}

/**
 * useContext
 */
function Child() {
  const { state, dispath } = useContext<ContextParam>(AppContext)
  return (
    <div>
      <div>number: {state.number}</div>
      <button onClick={() => dispath({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispath({ type: 'DECREMENT' })}>-</button>
    </div>
  )
}
複製代碼

上面咱們講到了 useReducer 用法,那麼怎麼和 redux 同樣共享操做和狀態呢, redux 怎麼作的呢? redux 也是經過 react-redux Provider 提供 store ( state, dispatch, subscribe ), connect 封裝了子組件, 封裝了 subscribe 和 unsubcribe 調用刷新 state ,讓子組件能夠調用到 store 的 state 和 dispatch 功能。那咱們函數組件使用hook來實現確定是相似了,咱們須要經過 createContext 建立個上下文,經過 Provider 去把 useReducer 返回的 state 和 dispatch 操做向下傳,子組件經過 useContext 至關於 consumer 消耗父級傳下來的數據,從而實現改動父級狀態值
實現了相似於redux的功能是否是?
react hooks試圖替換redux的方案具體可見 juejin.im/post/5eec5e…

useRef

  • useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue)
  • 返回的 ref 對象在組件的整個生命週期內保持不變

咱們先來看個例子

type IParam = Record<string, any>;
function Parent(props: IParam) {
  const [number, setNumber] = useState(0);
  const inputRef:React.MutableRefObject<HTMLInputElement> = useRef();
  const getFocus = () => {
    inputRef.current.focus();
  }
  return <>
    <Child inputRef={inputRef} getFocus={getFocus}/>
    <div>
      <div>
        {number}
        <button onClick={()=>setNumber(number + 1 )}>+</button>
      </div>
    </div>
  </>
}

function Child(props: IParam) {
  // const inputRef:React.MutableRefObject<HTMLInputElement> = useRef();
  // const getFocus = () => {
  //   inputRef.current.focus();
  // }
  const { inputRef, getFocus } = props;
  return <>
    <input type="text" ref={inputRef}/>
    <button onClick={ getFocus }>觸發焦點</button>
  </>
}
複製代碼

咱們能夠經過 useRef 建立一個 ref 對象,而後操做當前對象demo
這種方式看着怪怪的,向下傳遞ref怎麼處理呢?
forwardRef

  • 將ref從父組件中轉發到子組件中的dom元素上
  • 子組件接受props和ref做爲參數

修改下代碼再來看下

function Child(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}
Child = forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  const inputRef = useRef();
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>得到焦點</button>
      </>
  )
}
複製代碼

除了這種操做,偷偷告訴一個騷操做,若是你想使用一個不可變值,從上到下保持統一,也可使用useRef使用

const count = useRef(0);
// 使用變量
const runCount = count.current;
複製代碼

useLayoutEffect

  • 其函數簽名與 useEffect 相同,但它會在全部的 DOM 變動以後同步調用 effect
  • 可使用它來讀取 DOM 佈局並同步觸發重渲染
  • 在瀏覽器執行繪製以前useLayoutEffect內部的更新計劃將被同步刷新
  • 儘量使用標準的 useEffect 以免阻塞視圖更新

例子以下:

type IParam = Record<string, any>;
function App(props: IParam) {
  const [color, setColor] = useState('red')
  useEffect(() => {
    console.log('當前顏色:', color);
  }, [color]);
  useLayoutEffect(() => { 
    alert(color);
  })
  return (
    <div style={{ background: color, padding:10 }}>
      <div>
        {color}內容數據
      </div>
      <div>
        <button onClick={() => setColor('red')}>紅色</button>
        <button onClick={() => setColor('yellow')}>黃色</button>
        <button onClick={() => setColor('blue')}>藍色</button>
      </div>
    </div>
  )
}
複製代碼

咱們點擊切換顏色的時候會發現 useLayoutEffect 會先執行,先打印出內容,阻塞渲染,而後再執行 useEffect 執行變量變化

自定義hook

  • 有時候咱們會想要在組件之間重用一些狀態邏輯
  • 自定義 Hook 可讓你在不增長組件的狀況下達到一樣的目的
  • Hook 是一種複用狀態邏輯的方式,它不復用 state 自己
  • 事實上 Hook 的每次調用都有一個徹底獨立的 state
  • 自定義 Hook 更像是一種約定,而不是一種功能。若是函數的名字以 use 開頭,而且調用了其餘的 Hook,則就稱其爲一個自定義 Hook

umiuseRequest鉤子 ,功能酷似 axios ,咱們這邊就模擬個簡單的 xhr 請求的 useRequest hook

function useReqeust(url: string) {
  const limit = 5;
  const [data, setData] = useState([]);
  const [offset, setOffset] = useState(0);
  function loadMore() {
    const fetchUrl = `${url}?offset=${offset}&limit=${limit}`
    fetch(fetchUrl).then(response => response.json()).then(resData => { 
      setData([
        ...data,
        ...resData
      ]);
      setOffset(offset + resData.length);
    })
  }
  useEffect(() => loadMore(), []);
  return [data, loadMore];
}

/**
 * useReqeust調用
 */
function App() {
  const [list, loadMore] = useReqeust('http://localhost:8000/api/userList');
  if (!list || !list.length) {
    return <div>loading...</div>
  }
  return <>
    <ul>
      {
        list && (list as Array<any>).map((item, index) => (
          <li key={index}>
            {item.id}:{item.name}
          </li>
        ))
      }
    </ul>
    <div><button onClick={() => (loadMore as Function)()}>loadmore</button></div>
  </>
}
複製代碼

咱們在本身寫的 useRequest 裏面調用了useStateuseEffect 功能,返回了請求後數據 data,還有獲取更多的 loadMore 方法

結束語

你們能夠發揮本身的想法,切合本身的業務場景拆分出 tool hooks 或者組件 hooks 你們有什麼想討論的,能夠發評論區,看到會及時回覆~~~

相關文章
相關標籤/搜索