React Hooks:初探·實踐

前言

這篇文章主要介紹了React Hooks的一些實踐用法和場景,遵循我我的一向的思(tao)路(是什麼-爲何-怎麼作)html

是什麼

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.vue

簡單來講,上面這段官腔大概翻(xia)譯(shuo)就是告訴咱們class可以作到的老子用hooks基本能夠作到,放棄抵抗吧,少年!react

其實按照我本身的見解:React Hooks是在函數式組件中的一類以use爲開頭命名的函數。 這類函數在React內部會被特殊對待,因此也稱爲鉤子函數。編程

  • 函數式組件

Hooks只能用於Function Component, 其實這麼說不嚴謹,我更喜歡的說法是建議只在於Function Component使用Hooksredux

Should I use Hooks, classes, or a mix of both?
excuse me?

  • use開頭

React 約定,鉤子一概使用use前綴命名,便於識別,這沒什麼可說的,要被特殊對待,就要服從必定的規則api

  • 特殊對待

Hooks做爲鉤子,存在與每一個組件相關聯的「存儲器單元」的內部列表。 它們只是咱們能夠放置一些數據的JavaScript對象。 當你像使用useState()同樣調用Hook時,它會讀取當前單元格(或在第一次渲染時初始化它),而後將指針移動到下一個單元格。 這是多個useState()調用每一個get獨立本地狀態的方式數組

爲何

解決爲何要使用hooks的問題,我決定從hooks解決了class組件的哪些痛點和hooks更符合react的組件模型兩個方面講述。性能優化

1. class組件不香嗎?

class組件它香,可是暴露的問題也很多。Redux 的做者 Dan Abramov總結了幾個痛點:bash

  • Huge components that are hard to refactor and test.
  • Duplicated logic between different components and lifecycle methods.
  • Complex patterns like render props and higher-order components.

第一點:難以重構和測試的巨大組件。 若是讓你在一個代碼行數300+的組件里加一個新功能,你不慌嗎?你嘗試過註釋一行代碼,結果就跑不了或者邏輯錯亂嗎?若是須要引入redux或者定時器等那就更慌了~~網絡

第二點:不一樣組件和生命週期方法之間的邏輯重複。 這個難度不亞於蜀道難——難於上青天!固然對於簡單的邏輯可能經過HOCrender props來解決。可是這兩種解決辦法有兩個比較致命的缺點,就是模式複雜和嵌套。

第三點:複雜的模式,好比render props和 HOC。 不得不說我在學習render props的時候不由發問只有在render屬性傳入函數纔是render props嗎?好像我再任意屬性(如children)傳入函數也能實現同樣的效果; 一開始使用HOC的時候打開React Develops Tools一看,Unknown是什麼玩意~看着一層層的嵌套,我也是無能爲力。

以上這三點均可以經過Hooks來解決(瘋狂吹捧~)

2. hooks更符合React的編程模型

咱們知道,react強調單向數據流和數據驅動視圖,說白了就是組件和自上而下的數據流能夠幫助咱們將UI分割,像搭積木同樣實現頁面UI。這裏更增強調組合而不是嵌套,class並不能很完美地詮釋這個模型,可是hooks配合函數式組件卻能夠!函數式組件的純UI性配合Hooks提供的狀態和反作用能夠將組件隔離成邏輯可複用的獨立單元,邏輯分明的積木他不香嗎!

真香

怎麼作

別問,問就是文檔,若是不行的話,請熟讀並背誦文檔...

可是(萬事萬物最怕But), 既然是實踐,就得僞裝實踐過,下面就說說本人的簡單實踐和想法吧。

1. 轉變心智模型

jQuery
我認爲學習Hooks的主要成本不在於api的學習,而是在於 心智模型的轉變~就像是當年react剛出時,jQuery盛行的時代,這也須要時間去理解這種基於virtual DOM的心智模型。出於本能,咱們總喜歡在新事物的身上尋找舊事物的共同點,這種慣性思惟應該批判性地對待(上升到哲學層面了,趕忙迴歸正題....),若是你在學習的過程當中也有過把class組件的那套搬到Hooks,那麼恭喜你,你可能會陷入無限的wtf····, 下面舉幾個例子

  1. state一把梭
// in class component
class Demo extends React.Component {
 constructor(props) {
   super(props)
   this.state = {
     name: 'Hello',
     age: '18',
     rest: {},
   }
 }
 ...
}

// in function component
function Demo(props) {
 const initialState = {
   name: 'Hello',
   age: '18',
   rest: {},
 }
 const [state, setState] = React.useState(initialState)
 ...
}
複製代碼
  1. 嘗試模擬生命週期
// 這麼實現很粗糙,能夠配合useRef和useCallback,但即便這樣也不徹底等價於componentDidMount
function useDidMount(handler){
  React.useEffect(()=>{
      handler && handler()
  }, [])
}
複製代碼
  1. 在useEffect使用setInterval有時會事與願違
// count更新到1就不動了
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  ...
}
複製代碼

其實,在class component環境下思考問題更像是在特定的時間點作特定的事情,例如咱們會在constructor中初始化state,會在組件掛載後(DidMount)請求數據等,會在組件更新後(DidUpdate)處理狀態變化的邏輯,會在組件卸載前(willUnmount)清除一些反作用

然而在hooks+function component環境下思考問題應該更趨向於特定的功能邏輯,以功能爲一個單元去思考問題會有一種豁然開朗的感受。例如改變document的title、網絡請求、定時器... 對於hooks,只是爲了實現特定功能的工具而已

你會發現大部分你想實現的特定功能都是有反作用(effect)的,能夠負責任的說useEffect是最干擾你心智模型的Hooks, 他的心智模型更接近於實現狀態同步,而不是響應生命週期事件。還有一個可能會影響你的就是每一次渲染都有它本身的資源,具體表現爲如下幾點

  • 每一次渲染都有它本身的Props 和 State:當咱們更新狀態的時候,React會從新渲染組件。每一次渲染都能拿到獨立的狀態值,這個狀態值是函數中的一個常量(也就是會說,在任意一次渲染中,props和state是始終保持不變的)
  • 每一次渲染都有它本身的事件處理函數:和props和state同樣,它們都屬於一次特定的渲染,即使是異步處理函數也只能拿到那一次特定渲染的狀態值
  • 每個組件內的函數(包括事件處理函數,effects,定時器或者API調用等等)會捕獲某次渲染中定義的props和state(建議在分析問題時,將每次的渲染的props和state都常量化)

2. 所謂Hooks實踐

useState —— 相關的狀態放一塊兒

  • 不要全部state一把梭,能夠寫多個useState,基本原則是相關的狀態放一塊兒
  • setXX的時候建議使用回調的形式setXXX(xxx => xxx...)
  • 管理複雜的狀態能夠考慮使用useReducer(如狀態更新依賴於另外一個狀態的值)
// 實現計數功能
 const [count, setCount] = React.useState(0);
 setCount(count => count + 1)
 
// 展現用戶信息
const initialUser = {
  name: 'Hello',
  age: '18',
}
const [user, setUser] = React.useState(initialUser)
複製代碼

useEffect —— 不接受欺騙的反作用

  • 不要對依賴數組撒謊,effect中用到的全部組件內的值都要包含在依賴中。這包括props,state,函數等組件內的任何東西
  • 不要濫用依賴數組項, 讓Effect自給自足
  • 經過返回一個函數來清除反作用,在從新渲染後纔會清除上一次的effects
// 修改上面count更新到1就不動了,方法1
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);
  ...
}
// 修改上面count更新到1就不動了,方法2( 與方法1的區別在哪裏 )
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  ...
}
複製代碼

關於useEffect, 牆裂推薦Dan Abramov的A Complete Guide to useEffect,一篇支稱整篇文章架構的深度好文!

useReducer —— 強大的狀態管理機制

  • 把組件內發生了什麼(actions)和狀態如何響應並更新分開表述,是Hooks的做弊模式
/** 修改需求:每秒不是加多少能夠由用戶決定,能夠看做不是+1,而是+step*/

// 方法1
function Counter() {
  const [count, setCount] = React.useState(0);
  const [step, setStep] = React.useState(1);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count => count + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);
  ...
}
// 方法2( 與方法1的區別在哪裏 )
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { ...state, count: count + step };
  } else if (action.type === 'step') {
    return { ...state, step: action.step };
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  ...
}
複製代碼

useCallback —— FP裏使用函數的好搭檔

說這個以前,先說一說若是你要在FP裏面使用函數,你要先要思考有替代方案嗎?

方案1: 若是這個函數沒有使用組件內的任何值,把它提到組件外面去定義

方案2:若是這個函數只是在某個effect裏面用到,把它定義到effect裏面

若是沒有替代方案,就是useCallback出場的時候了。

  • 返回一個 memoized 回調, 不要對依賴數組撒謊
// 場景1:依賴組件的query
function Search() {
  const [query, setQuery] = React.useState('hello');
  
  const getFetchUrl = React.useCallback(() => {
    return `xxxx?query=${query}`;
  }, [query]);  

  useEffect(() => {
    const url = getFetchUrl();
  }, [getFetchUrl]); 
  ...
}

// 場景2:做爲props
function Search() {
   const [query, setQuery] = React.useState('hello');

  const getFetchUrl = React.useCallback(() => {
    return `xxxx?query=${query}`;
  }, [query]);  

  return <MySearch getFetchUrl={getFetchUrl} />
}

function MySearch({ getFetchUrl }) {
  useEffect(() => {
    const url = getFetchUrl();
  }, [getFetchUrl]); 
  ...
}
複製代碼

useRef —— 有記憶功能的可變容器

  • 返回一個可變的 ref 容器對象,其 .current 屬性被初始化爲傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變,也就是說會在每次渲染時返回同一個 ref 對象
  • 當 ref 對象內容發生變化時,useRef 並不會通知你。變動 .current 屬性不會引起組件從新渲染
  • 能夠在ref.current 屬性中保存一個可變值的「盒子「。常見使用場景:存儲指向真實DOM / 存儲事件監聽的句柄 / 記錄Function Component在某次渲染的值( eg:上一次state/props,定時器id.... )
// 存儲不變的引用類型
const { current: stableArray } = React.useRef( [1, 2, 3] )
<Comp arr={stableArray} />

// 存儲dom引用
const inputEl = useRef(null);
<input ref={inputEl} type="text" />

// 存儲函數回調
const savedCallback = useRef();
useEffect(() => {
    savedCallback.current = callback;
}
複製代碼

useMemo —— 記錄開銷大的值

// 此栗子來自文檔
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼

useContext —— 功能強大的上下文

  • 接收一個 context (React.createContext 的返回值)並返回該 context 的當前值,當前的 context 值由上層組件中最早渲染的 <MyContext.Provider value={value}> 的 value決定
  • 當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值,若是從新呈現組件很是昂貴,那麼能夠經過使用useMemo來優化它
// 此栗子來自文檔
const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}
複製代碼

彩蛋

說是彩蛋,實際上是補充說明~~

1. 一條重要的規則(代碼不規範,親人兩行淚)

hooks除了要以use開頭,還有一條很很很很重要的規則,就是hooks只容許在react函數的頂層被調用(這裏牆裂推薦Hooks必備神器eslint-plugin-react-hooks)

考慮到出於研(gang)究(jing)精神的你可能會問,爲何不能這麼用,我偏要的話呢?若是我是hooks開發者,我會堅決果斷地說出門右轉,有請下一位開發者!固然若是你想知道爲何這麼約定地話,仍是值得探討一下的。其實這個規則就是保證了組件內的全部hooks能夠按照順序被調用。那麼爲何順序這麼重要呢,不能夠給每個hooks加一個惟一的標識,這樣不就能夠隨心所欲了嗎?我之前一直都這麼想過直到Dan給了我答案,簡單點說就是爲了hooks最大的閃光點——custom-hooks

2. custom-hooks

給個人感受就是custom-hooks是一個真正詮釋了React的編程模型的組合的魅力。你能夠不看好它,但它確實有過人之處,至少它呈現出思想讓我越想越上頭~~以致於vue3.0也借鑑了他的經驗,推出了Vue Hooks。反手推薦一下react conf 2018的custom-hooks。

魅力

// 修改頁面標題
function useDocumentTitle(title) {
  useEffect (() => {
    document.title = title;
  }, [title]);
}

// 使用表單的input
function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  return {
    value,
    onChange: handleChange
  };
}
複製代碼

寫在最後

最後拋出兩個討論的小問題。

  1. React Hooks沒有缺點嗎?

    • 確定是有的,給我最直觀的感覺就是使人又愛又恨的閉包
    • 不斷地重複渲染會帶來必定的性能問題,須要人爲的優化
  2. 上面說了寫了不少的setInterval的代碼,能夠考慮封裝成一個custom-hooks?

    • 能夠考慮封裝成useInterva,關於封裝仍是牆裂推薦Dan的 Making setInterval Declarative with React Hooks
    • 若是有一堆特定的功能hooks,是否是徹底能夠經過組裝各類hooks完成業務邏輯的開發,例如網絡請求、綁定事件監聽等

本人能力有限,若是有哪裏說得不對的地方,歡迎批評指正!

對不聽系列

真的真的最後,怕你錯過,再次安利Dan Abramov的A Complete Guide to useEffect,一篇支稱整篇文章架構的深度好文!

相關文章
相關標籤/搜索