React Hooks + TypeScript 實戰記錄

React Hooks

什麼是 Hooks

  • React 一直都提倡使用函數組件,可是有時候須要使用 state 或者其餘一些功能時,只能使用類組件,由於函數組件沒有實例,沒有生命週期函數,只有類組件纔有。
  • HooksReact 16.8 新增的特性,它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。
  • 若是你在編寫函數組件並意識到須要向其添加一些 state ,之前的作法是必須將其它轉化爲 class 。如今你能夠直接在現有的函數組件中使用 Hooks
  • use 開頭的 React API 都是 Hooks

Hooks 解決了哪些問題?

  • 狀態邏輯難複用
    • 在組件之間複用狀態邏輯很難,可能要用到 render props (渲染屬性)或者 HOC(高階組件),但不管是渲染屬性,仍是高階組件,都會在原先的組件外包裹一層父容器(通常都是 div 元素),致使層級冗餘
  • 趨向複雜難以維護
    • 在生命週期函數中混雜不相干的邏輯(如:在 componentDidMount 中註冊事件以及其餘的邏輯,在 componentWillUnmount 中卸載事件,這樣分散不集中的寫法,很容易寫出 Bug )。
    • 類組件中處處都是對狀態的訪問和處理,致使組件難以拆分紅更小的組件。
  • this 指向問題
    • 父組件給子組件傳遞函數時,必須綁定 this

Hooks 優點

  • 能優化類組件的三大問題
  • 能在無需修改組件結構的狀況下複用狀態邏輯(自定義 Hooks )
  • 能將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據)
  • 反作用的關注點分離
    • 反作用指那些沒有發生在數據向視圖轉換過程當中的邏輯,如 Ajax 請求、訪問原生 DOM 元素、本地持久化緩存、綁定/解綁事件、添加訂閱、設置定時器、記錄日誌等。以往這些反作用都是寫在類組件生命週期函數中的。

經常使用 Hooks

useState

  1. React 假設當咱們屢次調用 useState 的時候,要保證每次渲染時它們的調用順序是不變的。
  2. 經過在函數組件裏調用它來給組件添加一些內部 stateReact 會 在重複渲染時保留這個 state
  3. useState 惟一的參數就是初始 state
  4. useState 會返回一個數組:一個 state ,一個更新 state 的函數
  5. 在初始化渲染期間,返回的狀態 state 與傳入的第一個參數 initialState 值相同。 咱們能夠在事件處理函數中或其餘一些地方調用更新 state 的函數。它相似 class 組件的 this.setState,可是它不會把新的 state 和舊的 state 進行合併,而是直接替換。

使用方法

const [state, setState] = useState(initialState);
複製代碼

舉個例子react

import React, { useState } from 'react';

function Counter() {
  const [counter, setCounter] = useState(0);

  return (
    <> <p>{counter}</p> <button onClick={() => setCounter(counter + 1)}>counter + 1</button> </> ); } export default Counter; 複製代碼

每次渲染都是一個獨立的閉包

  • 每一次渲染都有它本身的 Props 和 State
  • 每一次渲染都有它本身的事件處理函數
  • 當點擊更新狀態的時候,函數組件都會從新被調用,那麼每次渲染都是獨立的,取到的值不會受後面操做的影響

舉個例子ios

function Counter() {
  const [counter, setCounter] = useState(0);
  function alertNumber() {
    setTimeout(() => {
      // 只能獲取到點擊按鈕時的那個狀態
      alert(counter);
    }, 3000);
  }
  return (
    <> <p>{counter}</p> <button onClick={() => setCounter(counter + 1)}>counter + 1</button> <button onClick={alertNumber}>alertCounter</button> </> ); } 複製代碼

函數式更新

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

舉個例子github

function Counter() {
  const [counter, setCounter] = useState(0);

  return (
    <> <p>{counter}</p> <button onClick={() => setCounter(counter => counter + 10)}> counter + 10 </button> </> ); } 複製代碼

惰性初始化

  • initialState 參數只會在組件的初始化渲染中起做用,後續渲染時會被忽略
  • 若是初始 state 須要經過複雜計算得到,則能夠傳入一個函數,在函數中計算並返回初始的 state ,此函數只在初始渲染時被調用

舉個例子ajax

function Counter4() {
  console.log('Counter render');

  // 這個函數只在初始渲染時執行一次,後續更新狀態從新渲染組件時,該函數就不會再被調用
  function getInitState() {
    console.log('getInitState');
    // 複雜的計算
    return 100;
  }

  let [counter, setCounter] = useState(getInitState);

  return (
    <> <p>{counter}</p> <button onClick={() => setCounter(counter + 1)}>+1</button> </> ); } 複製代碼

useEffect

  • effect(反作用):指那些沒有發生在數據向視圖轉換過程當中的邏輯,如 ajax 請求、訪問原生dom 元素、本地持久化緩存、綁定/解綁事件、添加訂閱、設置定時器、記錄日誌等。
  • 反作用操做能夠分兩類:須要清除的和不須要清除的。
  • 原先在函數組件內(這裏指在 React 渲染階段)改變 dom 、發送 ajax 請求以及執行其餘包含反作用的操做都是不被容許的,由於這可能會產生莫名其妙的 bug 並破壞 UI 的一致性
  • useEffect 就是一個 Effect Hook,給函數組件增長了操做反作用的能力。它跟 class 組件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具備相同的用途,只不過被合併成了一個 API
  • useEffect 接收一個函數,該函數會在組件渲染到屏幕以後才執行,該函數有要求:要麼返回一個能清除反作用的函數,要麼就不返回任何內容
  • 與 componentDidMount 或 componentDidUpdate 不一樣,使用 useEffect 調度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快。大多數狀況下,effect 不須要同步地執行。在個別狀況下(例如測量佈局),有單獨的 useLayoutEffect Hook 供你使用,其 API 與 useEffect 相同。

使用方法

const App => () => {
  useEffect(()=>{})
  // 或者
  useEffect(()=>{},[...])
  return <></> } 複製代碼

使用 class 組件實現修改標題

在這個 class 中,咱們須要在兩個生命週期函數中編寫重複的代碼,這是由於不少狀況下,咱們但願在組件加載和更新時執行一樣的操做。咱們但願它在每次渲染以後執行,但 React 的 class 組件沒有提供這樣的方法。即便咱們提取出一個方法,咱們仍是要在兩個地方調用它。express

class Counter extends React.Component{
    state = {number:0};
    add = ()=>{
        this.setState({number:this.state.number+1});
    };
    componentDidMount(){
        this.changeTitle();
    }
    componentDidUpdate(){
        this.changeTitle();
    }
    changeTitle = ()=>{
        document.title = `你已經點擊了${this.state.number}次`;
    };
    render(){
        return (
            <> <p>{this.state.number}</p> <button onClick={this.add}>+</button> </> ) } } 複製代碼

使用 useEffect 組件實現修改標題

function Counter(){
    const [number,setNumber] = useState(0);
    // useEffect裏面的這個函數會在第一次渲染以後和更新完成後執行
    // 至關於 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        document.title = `你點擊了${number}次`;
    });
    return (
        <> <p>{number}</p> <button onClick={()=>setNumber(number+1)}>+</button> </> ) } 複製代碼

useEffect 作了什麼? 經過使用這個 Hook,你能夠告訴 React 組件須要在渲染後執行某些操做。React 會保存你傳遞的函數(咱們將它稱之爲 「effect」),而且在執行 DOM 更新以後調用它。在這個 effect 中,咱們設置了 document 的 title 屬性,不過咱們也能夠執行數據獲取或調用其餘命令式的 API。redux

爲何在組件內部調用 useEffect? 將 useEffect 放在組件內部讓咱們能夠在 effect 中直接訪問 count state 變量(或其餘 props)。咱們不須要特殊的 API 來讀取它 —— 它已經保存在函數做用域中。Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經提供瞭解決方案的狀況下,還引入特定的 React API。axios

useEffect 會在每次渲染後都執行嗎? 是的,默認狀況下,它在第一次渲染以後和每次更新以後都會執行。(咱們稍後會談到如何控制它)你可能會更容易接受 effect 發生在「渲染以後」這種概念,不用再去考慮「掛載」仍是「更新」。React 保證了每次運行 effect 的同時,DOM 都已經更新完畢。api

清除反作用

  • 反作用函數還能夠經過返回一個函數來指定如何清除反作用,爲防止內存泄漏,清除函數會在組件卸載前執行。若是組件屢次渲染,則在執行下一個 effect 以前,上一個 effect 就已被清除。
function Counter(){
  let [number,setNumber] = useState(0);
  let [text,setText] = useState('');
  // 至關於componentDidMount 和 componentDidUpdate
  useEffect(()=>{
      console.log('開啓一個新的定時器')
      let timer = setInterval(()=>{
          setNumber(number=>number+1);
      },1000);
      // useEffect 若是返回一個函數的話,該函數會在組件卸載和更新時調用
      // useEffect 在執行反作用函數以前,會先調用上一次返回的函數
      // 若是要清除反作用,要麼返回一個清除反作用的函數
      // return ()=>{
      // console.log('destroy effect');
      // clearInterval($timer);
      // }
  });
  // },[]);//要麼在這裏傳入一個空的依賴項數組,這樣就不會去重複執行
  return (
      <>
        <input value={text} onChange={(event)=>setText(event.target.value)}/>
        <p>{number}</p>
        <button>+</button>
      </>
  )
}
複製代碼

跳過 Effect 進行性能優化

  • 依賴項數組控制着 useEffect 的執行
  • 若是某些特定值在兩次重渲染之間沒有發生變化,你能夠通知 React 跳過對 effect 的調用,只要傳遞數組做爲 useEffect 的第二個可選參數便可
  • 若是想執行只運行一次的 effect(僅在組件掛載時執行),能夠傳遞一個空數組([])做爲第二個參數。這就告訴 React 你的 effect 不依賴於 props 或 state 中的任何值,因此它永遠都不須要重複執行
function Counter(){
  let [number,setNumber] = useState(0);
  let [text,setText] = useState('');
  // 至關於componentDidMount 和 componentDidUpdate
  useEffect(()=>{
      console.log('useEffect');
      let timer = setInterval(()=>{
          setNumber(number=>number+1);
      },1000);
  },[text]);// 數組表示 effect 依賴的變量,只有當這個變量發生改變以後纔會從新執行 efffect 函數
  return (
      <>
        <input value={text} onChange={(e)=>setText(e.target.value)}/>
        <p>{number}</p>
        <button>+</button>
      </>
  )
}
複製代碼

使用多個 Effect 實現關注點分離

  • 使用 Hook 其中一個目的就是要解決 class 中生命週期函數常常包含不相關的邏輯,但又把相關邏輯分離到了幾個不一樣方法中的問題。
// class版

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
複製代碼
  • 咱們能夠發現 document.title 的邏輯是如何被分割到 componentDidMountcomponentDidUpdate 中的,訂閱邏輯又是如何被分割到 componentDidMountcomponentWillUnmount 中的。並且 componentDidMount 中同時包含了兩個不一樣功能的代碼。這樣會使得生命週期函數很混亂。數組

  • Hook 容許咱們按照代碼的用途分離他們, 而不是像生命週期函數那樣。React 將按照 effect 聲明的順序依次調用組件中的 每個 effect

// Hooks 版

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}
複製代碼

useContext

const value = useContext(MyContext);
複製代碼

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

當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext providercontext value 值。即便祖先使用 React.memoshouldComponentUpdate,也會在組件自己使用 useContext 時從新渲染。

別忘記 useContext 的參數必須是 context 對象自己:

  • 正確: useContext(MyContext)
  • 錯誤: useContext(MyContext.Consumer)
  • 錯誤: useContext(MyContext.Provider)

提示 若是你在接觸 Hook 前已經對 context API 比較熟悉,那應該能夠理解,useContext(MyContext) 至關於 class 組件中的 static contextType = MyContext 或者 <MyContext.Consumer>useContext(MyContext) 只是讓你可以讀取 context 的值以及訂閱 context 的變化。你仍然須要在上層組件樹中使用 <MyContext.Provider> 來爲下層組件提供 context。

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.light}> <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> ); } 複製代碼

自定義 Hooks

  • 自定義 Hook 更像是一種約定,而不是一種功能。若是函數的名字以 use 開頭,而且調用了其餘的 Hook,則就稱其爲一個自定義 Hook
  • 有時候咱們會想要在組件之間重用一些狀態邏輯,以前要麼用 render props ,要麼用高階組件,要麼使用 redux
  • 自定義 Hook 可讓你在不增長組件的狀況下達到一樣的目的
  • Hook 是一種複用狀態邏輯的方式,它不復用 state 自己
  • 事實上 Hook 的每次調用都有一個徹底獨立的 state
function useNumber(){
  let [number,setNumber] = useState(0);
  useEffect(()=>{
    setInterval(()=>{
        setNumber(number=>number+1);
    },1000);
  },[]);
  return [number,setNumber];
}
// 每一個組件調用同一個 hook,只是複用 hook 的狀態邏輯,並不會共用一個狀態
function Counter1(){
    let [number,setNumber] = useNumber();
    return (
        <div><button onClick={()=>{ setNumber(number+1) }}>{number}</button></div>
    )
}
function Counter2(){
    let [number,setNumber] = useNumber();
    return (
        <div><button onClick={()=>{ setNumber(number+1) }}>{number}</button></div>
    )
}
複製代碼

useMemo、useCallback

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

ab的變量值不變的狀況下,memoizedCallback的引用不變。即:useCallback的第一個入參函數會被緩存,從而達到渲染性能優化的目的。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼

ab的變量值不變的狀況下,memoizedValue的值不變。即:useMemo函數的第一個入參函數不會被執行,從而達到節省計算量的目的。

性能優化

Object.is淺比較
  • Hook 內部使用 Object.is 來比較新舊 state 是否相等。
  • class 組件中的 setState 方法不一樣,若是你修改狀態的時候,傳的狀態值沒有變化,則不從新渲染。
  • class 組件中的 setState 方法不一樣,useState 不會自動合併更新對象。你能夠用函數式的 setState 結合展開運算符來達到合併更新對象的效果。
function Counter(){
    const [counter,setCounter] = useState({name:'計數器',number:0});
    console.log('render Counter')
    // 若是你修改狀態的時候,傳的狀態值沒有變化,則不從新渲染
    return (
        <> <p>{counter.name}:{counter.number}</p> <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button> <button onClick={()=>setCounter(counter)}>++</button> </> ) } 複製代碼
減小渲染次數
  • 默認狀況,只要父組件狀態變了(無論子組件依不依賴該狀態),子組件也會從新渲染
  • 通常的優化:
    • 類組件:可使用 pureComponent
    • 函數組件:使用 React.memo ,將函數組件傳遞給 memo 以後,就會返回一個新的組件,新組件的功能:若是接受到的屬性不變,則不從新渲染函數
  • 可是怎麼保證屬性不會變呢?這裏使用 useState ,每次更新都是獨立的,const [number,setNumber] = useState(0) 也就是說每次都會生成一個新的值(哪怕這個值沒有變化),即便使用了 React.memo ,也仍是會從新渲染。
const SubCounter = React.memo(({onClick,data}) =>{
  console.log('SubCounter render');
  return (
      <button onClick={onClick}>{data.number}</button>
  )
})
const ParentCounter = () => {
  console.log('ParentCounter render');
  const [name,setName]= useState('計數器');
  const [number,setNumber] = useState(0);
  const data ={number};
  const addClick = ()=>{
      setNumber(number+1);
  };
  return (
      <>
          <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
          <SubCounter data={data} onClick={addClick}/>
      </>
  )
}
複製代碼
  • 更深刻的優化-使用 useMemo & useCallback
const SubCounter = React.memo(({onClick,data}) =>{
  console.log('SubCounter render');
  return (
      <button onClick={onClick}>{data.number}</button>
  )
})
const ParentCounter = () => {
  console.log('ParentCounter render');
  const [name,setName]= useState('計數器');
  const [number, setNumber] = useState(0);
  // 父組件更新時,這裏的變量和函數每次都會從新建立,那麼子組件接受到的屬性每次都會認爲是新的
  // 因此子組件也會隨之更新,這時候能夠用到 useMemo
  // 有沒有後面的依賴項數組很重要,不然仍是會從新渲染
  // 若是後面的依賴項數組沒有值的話,即便父組件的 number 值改變了,子組件也不會去更新
  //const data = useMemo(()=>({number}),[]);
  const data = useMemo(()=>({number}),[number]);
  const addClick = useCallback(()=>{
      setNumber(number+1);
  },[number]);
  return (
      <>
          <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
          <SubCounter data={data} onClick={addClick}/>
      </>
  )
}
複製代碼

常見問題

useEffect 不能接收 async 做爲回調函數

React 規定 useEffect 接收的函數,要麼返回一個能清除反作用的函數,要麼就不返回任何內容。而 async 返回的是 promise

如何在 Hooks 中優雅的 Fetch Data

function App() {
  const [data, setData] = useState({ hits: [] });
  useEffect(() => {
    // 更優雅的方式
    const fetchData = async () => {
      const result = await axios(
        'https://api.github.com/api/v3/search?query=redux',
      );
      setData(result.data);
    };
    fetchData();
  }, []);
  return (
    <ul> {data.hits.map(item => ( <li key={item.id}> <a href={item.url}>{item.title}</a> </li> ))} </ul>
  );
}
複製代碼

不要過分依賴 useMemo

  • useMemo 自己也有開銷。useMemo 會「記住」一些值,同時在後續 render 時,將依賴數組中的值取出來和上一次記錄的值進行比較,若是不相等纔會從新執行回調函數,不然直接返回「記住」的值。這個過程自己就會消耗必定的內存和計算資源。所以,過分使用 useMemo 可能會影響程序的性能。

  • 在使用 useMemo 前,應該先思考三個問題:

    • 傳遞給 useMemo 的函數開銷大不大? 有些計算開銷很大,咱們就須要「記住」它的返回值,避免每次 render 都去從新計算。若是你執行的操做開銷不大,那麼就不須要記住返回值。不然,使用 useMemo 自己的開銷就可能超太重新計算這個值的開銷。所以,對於一些簡單的 JS 運算來講,咱們不須要使用 useMemo 來「記住」它的返回值。
    • 返回的值是原始值嗎? 若是計算出來的是基本類型的值(string、 boolean 、null、undefined 、number、symbol),那麼每次比較都是相等的,下游組件就不會從新渲染;若是計算出來的是複雜類型的值(object、array),哪怕值不變,可是地址會發生變化,致使下游組件從新渲染。因此咱們也須要「記住」這個值。
    • 在編寫自定義 Hook 時,返回值必定要保持引用的一致性。 由於你沒法肯定外部要如何使用它的返回值。若是返回值被用作其餘 Hook 的依賴,而且每次 re-render 時引用不一致(當值相等的狀況),就可能會產生 bug。因此若是自定義 Hook 中暴露出來的值是 object、array、函數等,都應該使用 useMemo 。以確保當值相同時,引用不發生變化。

TypeScript

什麼是 TypeScript

TypeScriptJavaScript 的一個超集,主要提供了類型系統ES6 的支持

TypeScript

爲何選擇 TypeScript

  • TypeScript 增長了代碼的可讀性和可維護性
    • 類型系統其實是最好的文檔,大部分的函數看看類型的定義就能夠知道如何使用了
    • 能夠在編譯階段就發現大部分錯誤,這總比在運行時候出錯好
    • 加強了編輯器和 IDE 的功能,包括代碼補全、接口提示、跳轉到定義、重構等
  • TypeScript 很是包容
    • TypeScript 是 JavaScript 的超集,.js 文件能夠直接重命名爲 .ts 便可
    • 即便不顯式的定義類型,也可以自動作出類型推論
    • 能夠定義從簡單到複雜的幾乎一切類型
    • 即便 TypeScript 編譯報錯,也能夠生成 JavaScript 文件
    • 兼容第三方庫,即便第三方庫不是用 TypeScript 寫的,也能夠編寫單獨的類型文件供 TypeScript 讀取
  • TypeScript 擁有活躍的社區
    • 大部分第三方庫都有提供給 TypeScript 的類型定義文件
    • TypeScript 擁抱了 ES6 規範,也支持部分 ESNext 草案的規範

瞭解了 React Hooks 和 TypeScript,接下來就一塊兒看一下兩者的結合實踐吧!😄

實踐

本實踐來源於本人正在開發的開源組件庫項目 Azir Design中的 Grid 柵格佈局組件。

目標

Grid

API

Row

屬性 說明 類型 默認值
className 類名 string -
style Row組件樣式 object:CSSProperties -
align 垂直對齊方式 top|middle|bottom top
justify 水平排列方式 start|end|center|space-around|space-between start
gutter 柵格間隔,能夠寫成像素值設置水平垂直間距或者使用數組形式同時設置 [水平間距, 垂直間距] number|[number,number] 0

Col

屬性 說明 類型 默認值
className 類名 string -
style Col組件樣式 object:CSSProperties -
flex flex 佈局屬性 string|number -
offset 柵格左側的間隔格數,間隔內不能夠有柵格 number 0
order 柵格順序 number 0
pull 柵格向左移動格數 number 0
push 柵格向右移動格數 number 0
span 柵格佔位格數,爲 0 時至關於 display: none number -
xs <576px 響應式柵格,可爲柵格數或一個包含其餘屬性的對象 number|object -
sm ≥576px 響應式柵格,可爲柵格數或一個包含其餘屬性的對象 number|object -
md ≥768px 響應式柵格,可爲柵格數或一個包含其餘屬性的對象 number|object -
lg ≥992px 響應式柵格,可爲柵格數或一個包含其餘屬性的對象 number|object -
xl ≥1200px 響應式柵格,可爲柵格數或一個包含其餘屬性的對象 number|object -
xxl ≥1600px 響應式柵格,可爲柵格數或一個包含其餘屬性的對象 number|object -

大展身手

這一實踐主要介紹 React Hooks + TypeScript 的實踐,不對 CSS 過多贅述。

Step-1 根據 API 來給 Row 組件定義 Prop 的類型

// Row.tsx

+ import React, { CSSProperties, ReactNode } from 'react';
+ import import ClassNames from 'classnames';
+
+ type gutter = number | [number, number];
+ type align = 'top' | 'middle' | 'bottom';
+ type justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between';
+
+ interface RowProps {
+   className?: string;
+   align?: align;
+   justify?: justify;
+   gutter?: gutter;
+   style?: CSSProperties;
+   children?: ReactNode;
+ }
複製代碼

這裏咱們用到了 TypeScript 提供的基本數據類型、聯合類型、接口。

基本數據類型 JavaScript 的類型分爲兩種:原始數據類型(Primitive data types)和對象類型(Object types)

原始數據類型包括:布爾值數值字符串nullundefined 以及 ES6 中的新類型 Symbol。咱們主要介紹前五種原始數據類型在 TypeScript 中的應用。

聯合類型 聯合類型(Union Types)表示取值能夠爲多種類型中的一種。

類型別名 類型別名用來給一個類型起個新名字。

接口 在TypeScript中接口是一個很是靈活的概念,除了可用於對類的一部分行爲進行抽象之外,也經常使用於對**對象的形狀(Shape)**進行描述。咱們在這裏使用接口對 RowProps 進行了描述。

Step-2 編寫 Row 組件的基礎骨架

// Row.tsx

- import React, { CSSProperties, ReactNode } from 'react';
+ import React, { CSSProperties, ReactNode, FC } from 'react';

import ClassNames from 'classnames';

type gutter = number | [number, number];
type align = 'top' | 'middle' | 'bottom';
type justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between';

interface RowProps {
  // ...
}
+ const Row: FC<RowProps> = props => {
+   const { className, align, justify, children, style = {} } = props;
+   const classes = ClassNames('azir-row', className, {
+     [`azir-row-${align}`]: align,
+     [`azir-row-${justify}`]: justify
+   });
+
+   return (
+     <div className={classes} style={style}> + {children} + </div>
+   );
+ };

+ Row.defaultProps = {
+   align: 'top',
+   justify: 'start',
+   gutter: 0
+ };

+ export default Row;
複製代碼

在這裏咱們使用到了泛型,那麼什麼是泛型呢?

泛型 泛型(Generics)是指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性。

function loggingIdentity<T>(arg: T): T {
    return arg;
}
複製代碼

Step-3 根據 API 來給 Col 組件定義 Prop 的類型

// Col.tsx

+ import React, {ReactNode, CSSProperties } from 'react';
+ import ClassNames from 'classnames';
+
+ interface ColCSSProps {
+   offset?: number;
+   order?: number;
+   pull?: number;
+   push?: number;
+   span?: number;
+ }
+
+ export interface ColProps {
+   className?: string;
+   style?: CSSProperties;
+   children?: ReactNode;
+   flex?: string | number;
+   offset?: number;
+   order?: number;
+   pull?: number;
+   push?: number;
+   span?: number;
+   xs?: ColCSSProps;
+   sm?: ColCSSProps;
+   md?: ColCSSProps;
+   lg?: ColCSSProps;
+   xl?: ColCSSProps;
+   xxl?: ColCSSProps;
+ }

複製代碼

Step-4 編寫 Col 組件的基礎骨架

// Col.tsx

import React, {ReactNode, CSSProperties } from 'react';
import ClassNames from 'classnames';
interface ColCSSProps {
  // ...
}
export interface ColProps {
  // ...
}

+ type mediaScreen = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';

+ function sc(size: mediaScreen, value: ColCSSProps): Array<string> {
+   const t: Array<string> = [];
+   Object.keys(value).forEach(key => {
+     t.push(`azir-col-${size}-${key}-${value[key]}`);
+   });
+   return t;
+ }

+ const Col: FC<ColProps> = props => {
+   const {
+    className,
+    style = {},
+    span,
+    offset,
+    children,
+    pull,
+    push,
+    order,
+    xs,
+    sm,
+    md,
+    lg,
+    xl,
+    xxl
+   } = props;
+
+   const [classes, setClasses] = useState<string>(
+    ClassNames('azir-col', className, {
+      [`azir-col-span-${span}`]: span,
+      [`azir-col-offset-${offset}`]: offset,
+      [`azir-col-pull-${pull}`]: pull,
+      [`azir-col-push-${push}`]: push,
+      [`azir-col-order-${order}`]: order
+    })
+   );
+
+   // 響應式 xs,sm,md,lg,xl,xxl
+   useEffect(() => {
+     xs && setClasses(classes => ClassNames(classes, sc('xs', xs)));
+     sm && setClasses(classes => ClassNames(classes, sc('sm', sm)));
+     md && setClasses(classes => ClassNames(classes, sc('md', md)));
+     lg && setClasses(classes => ClassNames(classes, sc('lg', lg)));
+     xl && setClasses(classes => ClassNames(classes, sc('xl', xl)));
+     xxl && setClasses(classes => ClassNames(classes, sc('xxl', xxl)));
+   }, [xs, sm, md, lg, xl, xxl]);
+
+   return (
+     <div className={classes} style={style}> + {children} + </div>
+   );
+ };
+ Col.defaultProps = {
+   offset: 0,
+   pull: 0,
+   push: 0,
+   span: 24
+ };
+ Col.displayName = 'Col';
+
+ export default Col;
複製代碼

在這裏 TypeScript 編譯器拋出了警告。

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ColCSSProps'. No index signature with a parameter of type 'string' was found on type 'ColCSSProps'. TS7053 71 | const t: Array<string> = []; 72 | Object.keys(value).forEach(key => { > 73 | t.push(`azir-col-${size}-${key}-${value[key]}`); | ^ 74 | }); 75 | return t; 76 | } 複製代碼

翻譯過來就是:元素隱式地具備 any 類型,類型 string 不能用於ColCSSProps的索引類型。那麼這個問題該如何結局呢?

interface ColCSSProps {
  offset?: number;
  order?: number;
  pull?: number;
  push?: number;
  span?: number;
+  [key: string]: number | undefined;
}
複製代碼

咱們只須要告訴 TypeScript ColCSSProps 的鍵類型是 string 值類型爲 number | undefined 就能夠了。

測試

寫到如今,該測試一下代碼了。

// example.tsx

import React from 'react';
import Row from './row';
import Col from './col';
export default () => {
  return (
    <div data-test="row-test" style={{ padding: '20px' }}> <Row className="jd-share"> <Col style={{ background: 'red' }} span={2}> 123 </Col> <Col style={{ background: 'yellow' }} offset={2} span={4}> 123 </Col> <Col style={{ background: 'blue' }} span={6}> 123 </Col> </Row> <Row> <Col order={1} span={8} xs={{ span: 20 }} lg={{ span: 11, offset: 1 }}> <div style={{ height: '100px', backgroundColor: '#3170bb' }}> Col1 </div> </Col> <Col span={4} xs={{ span: 4 }} lg={{ span: 12 }}> <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col2</div> </Col> </Row> </div>
  );
};

複製代碼

xs 尺寸屏幕下

lg 尺寸屏幕下
lg 尺寸屏幕下

xs 尺寸屏幕下

至此呢,效果還算不錯。

Step-5 限制 Row 組件的 Children

雖然效果還不錯,可是 Row 組件的 Children 能夠傳遞任何元素

// row.tsx

const Row: FC<RowProps> = props => {

  // ...

  return (
    <div className={classes} style={style}> {children} </div>
  );
};
複製代碼

這也太隨意了吧!若是 Children 中包含了不是 Col 組件的節點的話佈局確定會出問題,我決定在這裏限制一下 Row 組件的 Children 類型。

那麼該如何去限制呢?有的人會認爲,直接 children.map ,根據結構來判斷不就能夠了嗎?這樣作是不可取的,React 官方也指出在 children 上直接調用 map 是很是危險的,由於咱們不可以肯定 children 的類型。那該怎麼辦呢?React 官方很貼心的也給咱們提供了一個 API React.Children

在這以前咱們先給 Col 組件設置一個內置屬性 displayName 屬性來幫助咱們判斷類型。

// col.tsx

const Col: FC<ColProps> = props => {
  // ...
};
// ...
+ Col.displayName = 'Col';
複製代碼

而後咱們請出由於大哥 React.Children API。這個 API 能夠專門用來處理 Children。咱們給 Row 組件編寫一個 renderChildren 函數

// row.tsx
const Row: FC<RowProps> = props => {
  const { className, align, justify, children, style = {} } = props;
  const classes = ClassNames('azir-row', className, {
    [`azir-row-${align}`]: align,
    [`azir-row-${justify}`]: justify
  });

+  const renderChildren = useCallback(() => {
+     return React.Children.map(children, (child, index) => {
+       try {
+         // child 是 ReactNode 類型,在該類型下有不少子類型,咱們須要斷言一下
+         const childElement = child as React.FunctionComponentElement<ColProps>;
+         const { displayName } = childElement.type;
+         if (displayName === 'Col') {
+           return child;
+         } else {
+           console.error(
+             'Warning: Row has a child which is not a Col component'
+           );
+         }
+       } catch (e) {
+         console.error('Warning: Row has a child which is not a Col component');
+       }
+     });
+   }, [children]);

  return (
    <div className={classes} style={style}> - {children} + {renderChildren()} </div>
  );
};

複製代碼

至此咱們已經完成了80%的工做,咱們是否是忘了點什麼???

Step-6 錦上添花-gutter

咱們經過 外層 margin + 內層 padding 的模式來配合實現水平垂直間距的設置。

// row.tsx

import React, {
  CSSProperties, ReactNode, FC, FunctionComponentElement, useCallback, useEffect, useState
} from 'react';

// ...

const Row: FC<RowProps> = props => {
-  const { className, align, justify, children, style = {} } = props;
+  const { className, align, justify, children, gutter, style = {} } = props;

+ const [rowStyle, setRowStyle] = useState<CSSProperties>(style);

  // ...

  return (
-   <div className={classes} style={style}> + <div className={classes} style={rowStyle}> {renderChildren()} </div> ); }; // ... export default Row; 複製代碼

Row 組件的 margin 已經這設置好了,那麼 Col 組件的 padding 該怎麼辦呢?有兩中辦法,一是傳遞 props、二是使用 context,我決定使用 context 來作組件通訊,由於我並不想讓 Col 組件的 props 太多太亂(已經夠亂了...)。

// row.tsx

import React, {
  CSSProperties, ReactNode, FC, FunctionComponentElement, useCallback, useEffect, useState
} from 'react';

// ...


export interface RowContext {
  gutter?: gutter;
}
export const RowContext = createContext<RowContext>({});

const Row: FC<RowProps> = props => {
-  const { className, align, justify, children, style = {} } = props;
+  const { className, align, justify, children, gutter, style = {} } = props;

+ const [rowStyle, setRowStyle] = useState<CSSProperties>(style);

+ const passedContext: RowContext = {
+   gutter
+ };

  // ...

  return (
    <div className={classes} style={rowStyle}> + <RowContext.Provider value={passedContext}> {renderChildren()} + </RowContext.Provider> </div> ); }; // ... export default Row; 複製代碼

咱們在 Row 組件中建立了一個 context,接下來就要在 Col 組件中使用,並計算出 Col 組件 gutter 對應的 padding 值。

// col.tsx
import React, {
  ReactNode,
  CSSProperties,
  FC,
  useState,
  useEffect,
+  useContext
} from 'react';
import ClassNames from 'classnames';
+ import { RowContext } from './row';

  // ...
const Col: FC<ColProps> = props => {
  // ...

+ const [colStyle, setColStyle] = useState<CSSProperties>(style);
+ const { gutter } = useContext(RowContext);
+ // 水平垂直間距
+ useEffect(() => {
+   if (Object.prototype.toString.call(gutter) === '[object Number]') {
+     const padding = gutter as number;
+     if (padding >= 0) {
+       setColStyle(style => ({
+         padding: `${padding / 2}px`,
+         ...style
+       }));
+     }
+   }
+   if (Object.prototype.toString.call(gutter) === '[object Array]') {
+     const [paddingX, paddingY] = gutter as [number, number];
+     if (paddingX >= 0 && paddingY >= 0) {
+       setColStyle(style => ({
+         padding: `${paddingY / 2}px ${paddingX / 2}px`,
+         ...style
+       }));
+     }
+   }
+ }, [gutter]);
  // ...

  return (
-   <div className={classes} style={style}>
+   <div className={classes} style={colStyle}>
      {children}
    </div>
  );
};

// ...

export default Col;

複製代碼

到這裏呢,咱們的柵格組件就大功告成啦!咱們來測試一下吧!😄

測試

import React from 'react';
import Row from './row';
import Col from './col';
export default () => {
  return (
    <div data-test="row-test" style={{ padding: '20px' }}> <Row> <Col span={24}> <div style={{ height: '100px', backgroundColor: '#3170bb' }}> Col1 </div> </Col> </Row> <Row gutter={10}> <Col order={1} span={8} xs={{ span: 20 }} lg={{ span: 11, offset: 1 }}> <div style={{ height: '100px', backgroundColor: '#3170bb' }}> Col1 </div> </Col> <Col span={4} xs={{ span: 4 }} lg={{ span: 12 }}> <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col2</div> </Col> </Row> <Row gutter={10} align="middle"> <Col span={8}> <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div> </Col> <Col offset={8} span={8}> <div style={{ height: '100px', backgroundColor: '#3170bb' }}> Col2 </div> </Col> </Row> <Row gutter={10} align="bottom"> <Col span={4}> <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div> </Col> <Col span={8}> <div style={{ height: '100px', backgroundColor: '#3170bb' }}> Col2 </div> </Col> <Col push={3} span={9}> <div style={{ height: '130px', backgroundColor: '#2170bb' }}> Col3 </div> </Col> <Col span={4}> <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div> </Col> <Col span={8}> <div style={{ height: '100px', backgroundColor: '#3170bb' }}> Col2 </div> </Col> <Col span={8}> <div style={{ height: '130px', backgroundColor: '#2170bb' }}> Col3 </div> </Col> <Col pull={1} span={3}> <div style={{ height: '100px', backgroundColor: '#3170bb' }}> Col2 </div> </Col> </Row> </div>
  );
};

複製代碼

總結

至此 React Hooks + TypeScript 的實踐分享結束了,我這隻列舉了比較經常使用 Hooks APITypeScript 的特性,麻雀雖小、五臟俱全,咱們已經能夠體會到 React Hooks + TypeScript 帶來的好處,兩者的配合必定會讓咱們的代碼變得既輕巧有健壯。關於 HooksTypeScript 的內容但願讀者去官方網站進行更深刻的學習。

相關文章
相關標籤/搜索