React系列-自定義Hooks很簡單


React系列-Mixin、HOC、Render Props(上)

React系列-輕松學會Hooks(中)

React系列-自定義Hooks很簡單(下)

我們在第二篇文章中介紹了一些常用的hooks,接着我們繼續來介紹剩下的hooks吧

useReducer

作為useState 的替代方案。它接收一個形如(state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法。(如果你熟悉 Redux 的話,就已經知道它如何工作了。)

不明白Redux工作流的同學可以看看這篇Redux系列之分析中間件原理(附經驗分享)

為什么使用

官方說法: 在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較復雜且包含多個子值,或者下一個 state 依賴於之前的 state 等。並且,使用 useReducer 還能給那些會觸發深更新的組件做性能優化,因為你可以向子組件傳遞 dispatch 而不是回調函數 。

總結來說:

  • 如果你的state是一個數組或者對象等復雜數據結構

  • 如果你的state變化很復雜,經常一個操作需要修改很多state

  • 如果你希望構建自動化測試用例來保證程序的穩定性

  • 如果你需要在深層子組件里面去修改一些狀態(也就是useReducer+useContext代替Redux)

  • 如果你用應用程序比較大,希望UI和業務能夠分開維護

登錄場景

舉個例子????:

登錄場景

useState完成登錄場景

    function LoginPage() {
        const [name, setName] = useState(''); // 用戶名
        const [pwd, setPwd] = useState(''); // 密碼
        const [isLoading, setIsLoading] = useState(false); // 是否展示loading,發送請求中
        const [error, setError] = useState(''); // 錯誤信息
        const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登錄

        const login = (event) => {
            event.preventDefault();
            setError('');
            setIsLoading(true);
            login({ name, pwd })
                .then(() => {
                    setIsLoggedIn(true);
                    setIsLoading(false);
                })
                .catch((error) => {
                    // 登錄失敗: 顯示錯誤信息、清空輸入框用戶名、密碼、清除loading標識
                    setError(error.message);
                    setName('');
                    setPwd('');
                    setIsLoading(false);
                });
        }
        return ( 
            //  返回頁面JSX Element
        )
    }

useReducer完成登錄場景


    const initState = {
        name: '',
        pwd: '',
        isLoading: false,
        error: '',
        isLoggedIn: false,
    }
    function loginReducer(state, action) {
        switch(action.type) {
            case 'login':
                return {
                    ...state,
                    isLoading: true,
                    error: '',
                }
            case 'success':
                return {
                    ...state,
                    isLoggedIn: true,
                    isLoading: false,
                }
            case 'error':
                return {
                    ...state,
                    error: action.payload.error,
                    name: '',
                    pwd: '',
                    isLoading: false,
                }
            default: 
                return state;
        }
    }
    function LoginPage() {
        const [state, dispatch] = useReducer(loginReducer, initState);
        const { name, pwd, isLoading, error, isLoggedIn } = state;
        const login = (event) => {
            event.preventDefault();
            dispatch({ type: 'login' });
            login({ name, pwd })
                .then(() => {
                    dispatch({ type: 'success' });
                })
                .catch((error) => {
                    dispatch({
                        type: 'error'
                        payload: { error: error.message }
                    });
                });
        }
        return ( 
            //  返回頁面JSX Element
        )
    }

❗️我們的state變化很復雜,經常一個操作需要修改很多state,另一個好處是所有的state處理都集中到了一起,使得我們對state的變化更有掌控力,同時也更容易復用state邏輯變化代碼,比如在其他函數中也需要觸發登錄success狀態,只需要dispatch({ type: 'success' })。

筆者[狗頭]認為,暫時應該不會用useReducer替代useState,畢竟Redux的寫法實在是很繁瑣

復雜數據結構場景

剛好最近筆者的項目就碰到了復雜數據結構場景,可是並沒有用useReducer來解決,依舊采用useState,原因很簡單:方便

// 定義list類型
  export interface IDictList extends IList {
  extChild: {
    curPage: number
    totalSize: number
    size: number // pageSize
    list: IList[]
   }
 }
 const [list, setList] = useState<IDictList[]>([])
 
 const change=()=>{
   const datalist = JSON.parse(JSON.stringify(list)) // 拷貝對象 地址不同 不過這種寫法感覺不好 建議用reducers 應該封裝下reducers寫法
   const data = await getData()
      const { totalCount, pageSize, list } = data
      item.extChild.totalSize = totalCount
      item.extChild.size = pageSize
      item.extChild.list = list
      setList(datalist) // 改變
 }

看typescript寫的類型聲明就知道了這個list變量是個復雜的數據結構,需要經常需要改變添加extChild.list數組的內容,但是這種Array.prototype.push,是不會觸發更新,做過是通過const datalist = JSON.parse(JSON.stringify(list))。雖然沒有使用useReducer進行替代,筆者還是推薦大家試試

如何使用

const [state, dispatch] = useReducer(reducer, initialArg, init);

知識點合集

引用不變

useReducer返回的state跟ref一樣,引用是不變的,不會隨着函數組件的重新更新而變化,因此useReducer也可以解決閉包陷阱

const setCountReducer = (state,action)=>{
  switch(action.type){
    case 'add':
      return state+action.value
    case 'minus':
      return state-action.value
    default:
      return state
  }
}

const App = ()=>{
  const [count,dispatch] = useReducer(setCountReducer,0)
  useEffect(()=>{
    const timeId = setInterval(()=>{
      dispatch({type:'add',value:1})
    },1000)
    return ()=> clearInterval(timeId)
  },[])
  return (
    <span>{count}</span>
  )
}

把setCount改成useReducer的dispatch,因為useReducer的dispatch 的身份永遠是穩定的 —— 即使 reducer 函數是定義在組件內部並且依賴 props

useContext

,useContext肯定與React.createContext有關系的,接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider> 的 value prop 決定。

為什么使用

如果你在接觸 Hook 前已經對 context API 比較熟悉,那應該可以理解,useContext(MyContext) 相當於 class 組件中的 static contextType = MyContext 或者 <MyContext.Consumer>。簡單點說就是useContext是用來消費context API

如何使用

const value = useContext(MyContext);

知識點合集

useContext造成React.memo 無效

當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate❗️也會在組件本身使用 useContext 時重新渲染

舉個例子????:

// 創建一個 context
const Context = React.createContext()
// 用memo包裹
const Item = React.memo((props) => {
  // 組件一, useContext 寫法
  const count = useContext(Context);
  console.log('props', props)
  return (
    <div>{count}</div>
  )
})

const App = () => {
  const [count, setCount] = useState(0)
  return (
    <div>
      點擊次數: { count}
      <button onClick={() => { setCount(count + 1) }}>點我</button>
      <Context.Provider value={count}>
        <Item />
      </Context.Provider>
    </div>
  )
}

結果:

可以看到即使props沒有變化,一旦組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,此時Memo就失效了

Hooks替代Redux

有了useReduceruseContext以及React.createContext API,我們可以實現自己的狀態管理來替換Redux

實現react-redux

react-redux:React Redux is the official React binding for Redux. It lets your React components read data from a Redux store, and dispatch actions to the store to update data.

簡單理解就是連接組件和數據中心,也就是把React和Redux聯系起來,可以看看官方文檔或者看看阮一峰老師的文章,這里我們要去實現它最主要的兩個API

Provider 組件

Provider:組件之間共享的數據是 Provider 這個頂層組件通過 props 傳遞下去的,store必須作為參數放到Provider組件中去

利用React.createContext這個API,實現起來非常easy,react-redux本身就是依賴這個API的

const MyContext = React.createContext()

const MyProvider = MyContext.Provider

export default MyProvider // 導出

connect

connect:connect是一個高階組件,提供了一個連接功能,可用於將組件連接到store,它 提供了組件獲取 store 中數據或者更新數據的接口(mapStateToProps和mapStateToProps)的能力

connect([mapStateToProps], [mapStateToProps], [mergeProps], [options])

function connect(mapStateToProps, mapDispatchToProps) {
    return function (Component) {
        return function () {
            const {state, dispatch} = useContext(MyContext)
            const stateToProps = mapStateToProps(state)
            const dispatchToProps = mapDispatchToProps(dispatch)
            const props = {...props, ...stateToProps, ...dispatchToProps}
            return (
                <Component {...props} />
            )
        }
    }
}

export default connect // 導出

創建store

store: store對象包含所有數據。如果想得到某個時點的數據,就要對 Store 生成快照。這種時點的數據集合,就叫做 State。

利用useReducer來創建我們的store


 import React, { Component, useReducer, useContext } from 'react';
import { render } from 'react-dom';
import './style.css';

const MyContext = React.createContext()
const MyProvider = MyContext.Provider;

function connect(mapStateToProps, mapDispatchToProps) {
    return function (Component) {
        return function () {
            const {state, dispatch} = useContext(MyContext)
            const stateToProps = mapStateToProps(state)
            const dispatchToProps = mapDispatchToProps(dispatch)
            const props = {...props, ...stateToProps, ...dispatchToProps}

            return (
                <Component {...props} />
            )
        }
    }
}

function FirstC(props) {
    console.log("FirstC更新")
    return (
        <div>
             <h2>這是FirstC</h2>
            <h3>{props.books}</h3>
            <button onClick={()=> props.dispatchAddBook("Dan Brown: Origin")}>Dispatch 'Origin'</button>
        </div>
    )
}

function mapStateToProps(state) {
    return {
        books: state.Books
    }
}

function mapDispatchToProps(dispatch) {
    return {
        dispatchAddBook: (payload)=> dispatch({type: 'ADD_BOOK', payload})
    }
}

const HFirstC = connect(mapStateToProps, mapDispatchToProps)(FirstC)

function SecondC(props) {
   console.log("SecondC更新")
    return (
        <div>
            <h2>這是SecondC</h2>
            <h3>{props.books}</h3>
            <button onClick={()=> props.dispatchAddBook("Dan Brown: The Lost Symbol")}>Dispatch 'The Lost Symbol'</button>
        </div>
    )
}

function _mapStateToProps(state) {
    return {
        books: state.Books
    }
}

function _mapDispatchToProps(dispatch) {
    return {
        dispatchAddBook: (payload)=> dispatch({type: 'ADD_BOOK', payload})
    }
}

const HSecondC = connect(_mapStateToProps, _mapDispatchToProps)(SecondC)


function App () {
    const initialState = {
      Books: 'Dan Brown: Inferno'
    }

    const [state, dispatch] = useReducer((state, action) => {
      switch(action.type) {
        case 'ADD_BOOK':
          return { Books: action.payload }
        default:
          return state
      }
    }, initialState);
    return (
        <div>
            <MyProvider value={{state, dispatch}}>
                <HFirstC />
                <HSecondC />
            </MyProvider>
        </div>
    )
}

render(<App />, document.getElementById('root'));

結果:

嗯嗯????,我們就這樣實現了一個狀態管理

缺陷

  • 缺少時間旅行

  • 不支持中間件

  • 性能極差

可以看到上面的結果,一個狀態變化,所有組件都重新渲染,嗯嗯????,所以我們這是個demo玩玩而已,不要用於生產中

最后貼下Redux作者的回答:

useLayoutEffect

useLayoutEffect和useEffect一樣也是處理副作用,其函數簽名與 useEffect 相同,但它會在所有的 DOM 變更之后同步調用 effect。可以使用它來讀取 DOM 布局並同步觸發重渲染。在瀏覽器執行繪制之前,useLayoutEffect 內部的更新計划將被同步刷新。

❗️官方盡量推薦使用useEffect,因為useLayoutEffect,useLayoutEffect里面的callback函數會在DOM更新完成后立即執行,但是會在瀏覽器進行任何繪制之前運行完成,阻塞了瀏覽器的繪制

區別就是:useEffect是異步的,useLayoutEffect是同步的

為什么使用

解決一些閃爍場景

如何使用


useLayoutEffect(fn, []) // 接收兩個參數 一個是回調函數 另外一個是數組類型的參數(表示依賴)

知識點合集

⛽️暫無...

自定義hooks

自定義Hooks很簡單,利用官方提供的Hook我們可以把重用的邏輯抽離出來,也就是我們的自定義Hook,當你在一個項目中發現大量類似代碼,那就抽離成Hooks吧

❗️前面我們分析了Mixin,HOC,Render Props這些模式來實現狀態邏輯復用,這里的自定義hooks也是解決狀態邏輯復用問題的一種模式(????終於快完結了)

根據業務來說,我把自定義Hooks分為兩類,一類是自定義基礎Hooks,另一類是自定義業務Hooks

業務Hooks

比如我們多個頁面有相同的請求方法

// 以use開頭
export const useUserData = (category: string[], labelName?: string) => {
  const [baseTotal, setBaseTotal] = useState<number>(0)

  useEffect(() => {
    dealSearchTotal(category, labelName)
  }, [labelName, category])
  const dealSearchTotal = async (
  ) => {
    const data = await getUserData(curCategory, labelName)
    const { baseTotal, calculateTotal, basicTotal, extTotal } = data
    setBaseTotal(baseTotal)
  }
  // 最后return出想要的數據
  return [baseTotal, calculateTotal, basicTotal, extTotal]
}

❗️好如果你注意到你寫了重復代碼,抽離成自定義Hooks是沒問題的

基礎Hooks

基礎Hooks就是平時與業務無關的工具方法

useEffectOnce

該Hooks在函數組件只執行一次

const useEffectOnce = (effect) => {
  useEffect(effect, []);
};

export default useEffectOnce;

useMount

該Hook在組件掛載時調用

const useMount = (fn) => {
  useEffectOnce(() => {
    fn();
  });
};

export default useMount;

useUnmount

該Hook在組件銷毀時調用

const useUnmount = (fn: () => any): void => {
  const fnRef = useRef(fn);
  fnRef.current = fn;
  useEffectOnce(() => () => fnRef.current());
};

export default useUnmount;

usePrevious

獲取組件的state或者props的舊值

const usePrevious = (state): => {
  const ref = useRef();
  useEffect(() => {
    ref.current = state;
  });
  return ref.current;
};
export default usePrevious;

❗️其它參考Umi Hooks

最后


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM