使用 React Hooks 重構 React Class Component 的改造筆記

React hooks 的導入解決的很多過去在使用 class-based component 的問題。

就像是陳近南的武功。

「一掌打出」

一掌打出

「不論人畜、蝦蟹、跳蚤都會化成飛灰」那樣的神 (  ̄ 3 ̄)y▂ξ

化為灰飛

緣由、想要解決什麼問題

基本上就是 stateful logic 產生了一些問題。

改寫的 Tips

既然 hooks 可以解決這麼多問題,那要怎麼改寫呢?

在策略上

在技術上

元件改寫

// 修改前
import React, { Component } from 'react';

// 修改後
import React, { useEffect } from 'react';
// 修改前
class SampleComponent extends Component {
  // 以下省略
}

// 修改後
const SampleComponent= () => {
  // 以下省略
}
// 範例:以下是一個簡單的計數器,點擊按鈕即可把計數加 1,但只能設定 10 次
// 使用 useState 來取代 state 宣告與 setState

import React, { useState } from 'react';

const Count = ({ count, times }) => {
  return (
    <>
      {count}
      你還剩下 {times} 
    </>
  );
};

const Counter = ({ initValue }) => {
  const [count, setCounter] = useState(0);
  const [times, setTimes] = useState(10);

  const handleClickIncrement = () => {
    setCounter(count + 1);
    setTimes(times - 1);
  };

  return (
    <>
      <Count count={count} times={times} />
      <button disabled={!times} onClick={handleClickIncrement}>
        click
      </button>
    </>
  );
};

const App = () => {
  return <Counter />;
};

export default App;
// 承上,改用 useReducer 來改寫複雜的多個的 useState 情況
import React, { useReducer } from 'react';

const initialState = {
  count: 0,
  times: 10
};

const reducer = (state, action) => {
  switch (action) {
    case 'SET_TIMES':
      return {
        ...state,
        times: state.times - 1
      };
    case 'SET_COUNT':
      return {
        ...state,
        count: state.count + 1
      };
    default:
      return state;
  }
};

const Count = ({ count, times }) => {
  return (
    <>
      {count}
      你還剩下 {times} 
    </>
  );
};

const Counter = ({ initValue }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, times } = state;

  const handleClickIncrement = () => {
    dispatch('SET_TIMES');
    dispatch('SET_COUNT');
  };

  return (
    <>
      <Count count={count} times={times} />
      <button disabled={!times} onClick={handleClickIncrement}>
        click
      </button>
    </>
  );
};

const App = () => {
  return <Counter />;
};

export default App;
// 範例:在元件渲染完成後,從 API 取得商店列表的資料並存到 redux 中
// 修改前
componentDidMount() {
  const { fetchStoreList } = this.props;
  fetchStoreList();
}

// 修改後
useEffect(() => {
  // 備註,在重構過程中 redux 無法一下子被移除,因此會使用 react-redux 的 useSelector 和 useDispatch 來做過渡
  dispatch(fetchStoreList());
}, []);
// 範例:由於點選不同的分類或是標籤需要顯示不同的商店清單,
// 而分類或是標籤是從網址參數取得的,
// 因此當 keyword 有變化的時候就要重發一次 API 來取資料

// 修改前
componentDidUpdate(prevProps) {
  const {
    fetchStoreList,
    match: { params: { keyword }}
  } = this.props;

    if (keyword !== prevProps.match.params.keyword) {
      fetchStoreList(keyword);
    }
  }

// 修改後:在 useEffect 的 dependency 陣列加入 keyword
// 這樣 keyword 一有變化就會重發一次 API 來取資料
const dispatch = useDispatch();
const { keyword } = useParams();
const stores = useSelector(state => state.stores);

useEffect(() => {
  dispatch(fetchStoreList(keyword));
}, [keyword]);
// 範例:元件渲染完成後設定監聽 click 事件,使用者只要點擊變觸發 handleSomething -> 等同 componentDidMount
// 元件移除前移除監聽 click 事件不再讓使用者點擊後觸發 handleSomething -> 等同於 componentWillUnmount

useEffect(() => {
  document.addEventListener('click', handleSomething, true);
  return () => document.removeEventListener('click', handleSomething, true);
}, []);
// 範例:由於父元件 StoreList 需要將 store 傳給子元件 Stores,
// 為了避免參數層層傳遞,使用 createContext 與 useContext 來改寫

// 修改前:父元件 StoreList 需要將 store 傳給子元件 Stores
class StoreList extends Component {
  componentDidMount() {
    const { fetchStoreList } = this.props;
    fetchStoreList(); // 觸發 action,觸發 API 取資料
  }

  render() {
    return <Stores stores={stores} />;
  }
}

const Stores = (stores) => {
  return _.map(stores, (store) =>
    <Store key={store.id} store={store} />
  );
};

// 修改後:使用 useContext 而不用層層傳遞
import React, { createContext, useContext, useEffect } from 'react';

const StoreListContext = createContext();

const Stores = () => {
  const stores = useContext(StoreListContext); // 要用就這樣取出資料來用

  return _.map(stores, (store) =>
    <Store key={store.id} store={store} />
  );
};

const StoreList = () => {
  const dispatch = useDispatch();
  const stores = useSelector(state => state.stores);

  useEffect(() => {
    dispatch(fetchStoreList());
  }, [keyword]);

  // 使用 `StoreListContext.Provider` 來包覆要用到的元件,
  // 並且把值用 props 的方式傳入,在此是 value
  return (
    <StoreListContext.Provider value={stores}>
      <Stores />
    </StoreListContext.Provider>
  )
};
// 範例:點擊按鈕後 scroll 到最下方
// 滑到最下方可看到 "You see me!" 的訊息

// 修改前
import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);
    this.bottomRef = React.createRef();

    this.scrollToBottom = this.scrollToBottom.bind(this);
  }

  scrollToBottom() {
    console.log(this.bottomRef);
    this.bottomRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'start'
    });
  }

  render() {
    return (
      <>
        <button onClick={this.scrollToBottom}>
          Click to scroll to bottom!
        </button>
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        ... <br />
        You see me!
        <div ref={this.bottomRef} />
      </>
    );
  }
}

export default App;

// 修改後
import React, { useRef } from 'react';

const App = () => {
  const bottomRef = useRef(null);

  const scrollToBottom = () => {
    bottomRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'start'
    });
  };

  return (
    <>
      <button onClick={scrollToBottom}>Click to scroll to bottom!</button>
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      ... <br />
      You see me!
      <div ref={bottomRef} />
    </>
  );
};

export default App;

無法一次改完整個專案

很多時候呢,是通通都要的 在這裡這樣講不太好,不要學

我全都要

import { useDispatch, useSelector } from 'react-redux';

const dispatch = useDispatch();
const stores = useSelector(state => state.stores);

useEffect(() => {
  dispatch(fetchStoreList());
}, []);

renderStores(stores);

這樣應該足夠做簡易的重構了,那就先寫到這裡,之後想到再來補補 (✪ω✪)

參考文件


react hooks functional programming react.js 吃什麼 redux