React Hook起飛指南

React Hook起飛指南

做者:元瀟 方凳雅集出品javascript

16.8目前放出來了10個內置hook,但僅僅基於如下兩個API,就能作不少事情。因此這篇文章不會講不少API,也不會講API的基本用法,只把這兩個能作的事情講清楚,閱讀全文大概5-10分鐘。java

  • 狀態管理:useState
  • 反作用管理:useEffect

這兩個api就是hook世界裏的鐮刀和錘子,看似簡單的兩個api實際上所表明的,是相比之前大相徑庭的一種新的編程模型。react

前言:已經有了class component,爲何又來了一個hook?

Dan在他的博客上提到:編程

咱們知道組件和自上而下的數據流能夠幫助咱們將大型UI組織成小型,獨立,可重用的部分。 可是,咱們常常沒法進一步破壞複雜組件,由於邏輯是有狀態的,沒法提取到函數或其餘組件中。而hook讓咱們能夠將組件內部的邏輯組織成可重用的隔離單元。

因此,一句話總結hook帶來的變革就是:將可複用的最小單元從組件層面進一步細化到邏輯層面。redux

基於這一點優點,在後面咱們看能夠看到,基於hook開發的應用裏的全部組件都不會隨業務增加而變得臃腫,由於在hook的世界裏,狀態邏輯和UI是解耦的。UI只須要消費最終計算出來的狀態數據,hook只關注狀態的計算和改變。api

1、有狀態的函數

useState組件是有狀態的:緩存

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

函數是無狀態的:ide

const Example = props => {
  const { count, onClick } = props;
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={onClick}>
        Click me
      </button>
    </div>
  );
}

hooks是有狀態的函數:函數

import { useState } from 'react';
const Example = () => {
    const [count, setCount] = useState(0);
    return (
      <div>
        <p>You clicked {count} times</p>
        <button onClick={() => setCount(count + 1)}>
          Click me
        </button>
      </div>
    );
}

Think: useState生產出來的setter在更新state的時候不會合並,這點不一樣於傳統class組件的setState方法,爲何這樣設計?測試

// Don't do such like this ↓
const [data, setData] = useState({ count: 0, name: 'zby' });
useEffect(() => {
  // data: { count: 0, name: 'zby' } -> { count: 0 }
  setData({ count: 1 });
}, []);

咱們的應用都是從小到大發展起來的,初始充分的組件劃分和狀態設計是保證應用後續可維護性的重要一環,由於隨着應用的擴增,組件不免變得臃腫。因此有時咱們也從一開始就一步到位引入redux之類的狀態管理。

但如今,在咱們的「純函數」組件裏,每一個useState都會生產出一對兒state和stateSetter,咱們無需考慮更多的狀態樹的設計和組件的劃分設計,邏輯代碼直接從根組件寫起,漸進式開發變得可行

所謂「漸進式」開發:歸納應用的發展路徑大體能夠分爲如下3個階段:

1. 前期farm:
只須要把相關 state 組合到幾個獨立的 state 變量便可應付絕大多數狀況;

2.中期gank:當組件的狀態逐漸變得多起來時,咱們能夠很輕鬆地將狀態的更新交給reducer來管理(詳情在下文第二章展開);

3.大後期團戰:不光狀態多了,狀態的邏輯也愈來愈複雜的時候,咱們能夠幾乎0成本的將繁雜的狀態邏輯代碼抽離成自定義hook解決問題(詳情在下文第三章展開);

基於hook,咱們的開發過程,變得比以往更有彈性。因此這樣的漸進式的開發變得可行且高效。

2、 高度靈活的redux,純粹、無依賴

上文說道,當組件的狀態逐漸變得多起來時,咱們能夠很天然的將狀態的更新交給reducer來管理。

不一樣於真正的redux,在實際應用中,hook帶來了一種更加靈活和純粹的模式。如今咱們能夠用10行代碼實現一個全局的redux,也能夠用2行代碼隨時隨地實現一個局部的redux。

A:10行代碼實現一個全局redux:

import React from 'react';
const store = React.createContext(null);
export const initialState = { name: '元瀟' };
export function reducer(state, action) {
  switch (action.type) {
    case 'changeName': return { ...state, name: action.payload };
    default: throw new Error('Unexpected action');
  }
}
export default store;

Provider根組件掛上便可

import React, { useReducer } from 'react';
import store, { reducer, initialState } from './store';
function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
     <store.Provider value={{ state, dispatch }}>
      <div/>
     </store>
  )
}

子組件調用

import React, { useContext } from 'react';
import store from './store';
function Child() {
  const { state, dispatch } = useContext(store);
  ...
}

B:隨時隨地實現一個局部redux

import React, { useReducer } from 'react';
const initialState = { name: '元瀟' };
function reducer(state, action) {
  switch (action.type) {
    case 'changeName': return { ...state, name: action.payload };
    default: throw new Error('Unexpected action');
  }
}
function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);
  ...
}
  • useState的本質是useReducer的一個語法糖,感興趣能夠閱讀一下hooks的類型定義和實現。

3、自定義hook

上上文說道,當組件發展到必定程度,不光是狀態多了,狀態的邏輯也愈來愈複雜的時候,咱們能夠幾乎0成本的將繁雜的狀態邏輯代碼抽離成自定義hook解決問題。

當咱們想在兩個函數之間共享邏輯時,咱們會把它提取到第三個函數中。而組件和 hook 都是函數,因此也一樣適用這種方式。不一樣的是,hook 是有狀態的函數,它能實現以往純函數所不能作到的更高級別的複用——狀態邏輯的複用。

且先看下面兩個demo示例

A:class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: undefined
    };
  }
  componentDidMount() {
    service.getInitialCount().then(data => {
      this.setState({ count: data });
    });
    service.getInitialName().then(data => {
      this.setState({ name: data });
    });
  }
  componentWillUnmount() {
    service.finishCounting().then(() => {
      alert('計數完成');
    });
  }
  addCount = () => {
    this.setState({ count: this.state.count + 1 });
  };
  handleNameChange = name => {
    this.setState({ name });
  };
  render() {
    const { count, name } = this.state;
    return (
      <div>
        <div>
          <p>You clicked {count} times</p>
          <button onClick={this.addCount}>Click me</button>
        </div>
        <Input value={name} onChange={this.handleNameChange} />
      </div>
    );
  }
}
B:function useCount(initialValue) {
  const [count, setCount] = useState(initialValue);
  useEffect(() => {
    service.getInitialCount().then(data => {
      setCount(data);
    });
    return () => {
      service.finishCounting().then(() => {
        alert('計數完成');
      });
    };
  }, []);
  function addCount() {
    setCount(c => c + 1);
  }
  return { count, addCount };
}
function useName(initialValue) {
  const [name, setName] = useState(initialValue);
  useEffect(() => {
    service.getInitialName().then(data => {
      setName(data);
    });
  }, []);
  function handleNameChange(value) {
    setName(value);
  }
  return { name, handleNameChange };
}
const App = () => {
  const { count, addCount } = useCount(0);
  const { name, setName } = useName();
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={addCount}>Click me</button>
      <Input value={name} onChange={setName} />
    </div>
  );
};

A有2個狀態:count和name,還有與之相關的一票兒邏輯,散落在組件的生命週期和方法裏。雖然咱們能夠將組件的state和變動action抽成公共的,但涉及到反作用的action,到最終仍是繞不開組件的生命週期。但一個組件的生命週期只有一套,不可避免的會出現一些徹底不相干的邏輯寫在一塊兒。如此一來,便沒法實現徹底的狀態邏輯複用。

在B中,咱們將count相關的邏輯和name相關的邏輯經過自定義hook,封裝在獨立且封閉的邏輯單元裏。以往class組件的生命週期在這裏不復存在。生命週期是和UI強耦合的一個概念,雖然易於理解,但它自然距離數據很遙遠。而hook以一種相似rxjs模式的數據流訂閱實現了組件的反作用封裝,這樣的好處就是咱們只須要關心數據。因此hook所帶來的,毫不僅僅只是簡化了state的定義與包裝。

這個動畫,很好的展現了A->B先後相關聯的狀態邏輯的組織方式變化。

業務實戰記錄:

這是一個商品詳情頁用到的SKU選擇組件

咱們使用hook將原有的class組件進行重構,這個重構的過程就是一個狀態邏輯的抽取過程。

咱們將組件核心的2個狀態和相關的邏輯,抽到了2個獨立的自定義hook中(見下圖)。這樣作的好處很明顯,咱們的組件只須要去消費這兩個hook產出的value和function,狀態的維護和更新細節,已經被封裝在hook裏。

const { specPath, handleSpecPathChange } = useSkuSpecPath({
    defaultSpecPath,
    dataSource
  });
const { skus, handleSkuAmountChange } = useSkuAmount({
  defaultSelectedSkus,
  dataSource
});

在這裏提出一個自定義hook的設計範式:

const { state, handleChange, others } = useCustomHook(config, dependency?);

其中config聲明瞭hook所須要的數據,多是內部useState的初始值,也多是結構化的數據,總結起來就是這個hook的配置

dependency一般只有hook內使用了useEffect、useCallback這類API,須要咱們聲明依賴的時候須要傳入。

左邊是重構後的代碼(脫敏代碼,領會精神就好),原先核心的通用邏輯被抽到useSkuSpecPath和useSkuAmount這兩個hook裏後,這個組件變成了它們的調用方。後續無論UI組件如何變化和擴展,只要符合它們的接口格式約定,就能夠隨時隨地地複用這些邏輯,很快地實現一個新的sku選擇組件。

總結:自定義hook實現了狀態邏輯與UI分離,經過合理抽象自定義hook,可以實現很是高級別的業務邏輯抽象複用。

推薦一個網站,裏面收集了一些有意思的自定義hook

4、將來引用Dan的一句話:

hook能夠涵蓋class組件的全部使用場景,同時在抽取、測試和重用代碼方面提供了更大的靈活性。這就是爲何Hooks表明了咱們對React將來的願景。

react16.8以上的應用裏,你們能夠立馬用起來了。

彩蛋

hook原理: Not magic,just array

let hooks, i;
function useState() {
  i++;
  if (hooks[i]) {
    // 再次渲染時
    return hooks[i];
  }
  // 第一次渲染
  hooks.push(...);
}
// 準備渲染
i = -1;
hooks = fiber.hooks || [];
// 調用組件
Component();
// 緩存 Hooks 的狀態
fiber.hooks = hooks;
相關文章
相關標籤/搜索