React發布了幾個月的Hooks,最近也開始嘗試接觸,後面會稍微提一下PureComponent,不會介紹hooks各種特殊用法,就只針對hooks performance優化做介紹,還有搭配redux的處理。

因為前陣子有處理過React優化效能,對於這件事情也開始在意,讓人絕望的google page speed…。

react lifecycle

React PureComponent

如果有在注意效能的話,你應該會聽過shouldComponentUpdate或PureComponent,這是較常見的處理方法,Purecomponent只關注state、props並作shallow equal比較,當不同值才會觸發 rerender。

React shallow-compare

以下是有無使用Purecomponent的比較,當我更新某個state,而這個state沒有傳進作為props,PureComponent會過濾掉更新。

使用教學: 你可以嘗試更新input text,會發現Purecomponent數字不會增加,而一般component則是會增加。這數字增加代表著react嘗試update Component次數。

這數字不代表是否真的更新dom,因為react會比較render後dom結構的不同,再決定是否更新某節點dom,但嚴格來說這也算是種浪費效能。

  • PureComponent code
    import React, { PureComponent } from "react";

    class CheckboxPure extends PureComponent {
    constructor(props) {
    super(props);
    this.state = {
    done: true
    };
    this.times = 0;
    }
    changeCheck = e => {
    this.setState({
    done: e.target.checked
    });
    };
    componentWillUpdate() {
    this.times = this.times + 1;
    }
    render() {
    const { done } = this.state;
    const { text } = this.props;
    return (
    <div>
    <div>PureComponent component Try Update time {this.times}</div>
    <div>
    <input onClick={this.changeCheck} type="checkbox" checked={done} />
    {text}
    </div>
    </div>
    );
    }
    }

    export default CheckboxPure;

ps. PureComponent不是全部都用,需要注意props的更新關係。假設你上層的update,一定會更動到PureCompoent的props,那你應該避免使用PureComponent,因為每次接受到props時,PureComponent還會多做一次shallow compare,那因為每次都一定更新props,多做比較就等於浪費效能,比起用一般方法還不好。

React hooks functional

前面會提到PureComponent,是因為react hooks是全面的使用functional Component,這代表我們不會在使用Class,以往Class使用是繼承React並讓我們建立instance,有instance就代表有memory位置,可以讓我們處理資料比較。functional代表我們只要調用一次更新,所有的react hooks function都會再被調用一次。

舉例來說,將關注點變到更小,所以useEffect才能實現像是componentDidUpdate的功能。

  • Hooks like componentDidUpdate
    useEffect(() => {
    document.title = `You clicked ${count} times`;
    };
    // it will setting title everytime when render function

useEffect就是一個例子,你看到useEffect的額外第二個參數,useEffect會綁定count更新,才會調用callback。

  • Hooks useEffect bind count
    useEffect(() => {
    document.title = `You clicked ${count} times`;
    }, [count]);
    // Only setting title if count changes

React hooks 實現todoList

嘗試建立一個toDo List,方便我們來看怎樣讓React hooks實現PureComponent的特性。我們會需要建立三個檔案,分別是是container/todoView、component/todoLis、hooks/useTodoList。

假設你已經用過react hooks,這部分可以直接略過。

首先建立container/TodoView,我們會需要建立toDo的Array,這邊我們會用到useState,還有useRef,讓我們能夠取得input value,剩下部份就是更新處理todoList state。

  • React hooks function
    // toDo array 
    const [todoList, setTodoList] = useState([]);

    // create inputRef
    const inputEl = useRef(null);

    // add Array
    const addTodo = event => {
    event.preventDefault();
    if (!inputEl.current.value) {
    return;
    }
    const mergeArr = [...todoList, inputEl.current.value];
    inputEl.current.value = "";
    return setTodoList(mergeArr);
    };

    // delete Array by index
    const deleteToDo = index => {
    const newArr = [...todoList];
    newArr.splice(index, 1);
    return setTodoList(newArr);
    };

建立hooks/useTodoList,並把上面這些hooks function移動過去。就完成了todoList的自製hooks。

  • hooks/useTodoList.js
    import { useState, useCallback } from "react";

    function useTodoList(value, inputEl) {
    const [todoList, setTodoList] = useState(value);
    const addTodo = event => {
    event.preventDefault();
    if (!inputEl.current.value) {
    return;
    }
    const mergeArr = [...todoList, inputEl.current.value];
    inputEl.current.value = "";
    return setTodoList(mergeArr);
    };

    const deleteToDo = index => {
    const newArr = [...todoList];
    newArr.splice(index, 1);
    return setTodoList(newArr);
    };

    return [todoList, addTodo, deleteToDo];
    }

    export default useTodoList;

會多建立一個 const [count, setCount] = useState(0);,讓我們在這層setState,並觀察TodoList更新狀況。

  • container/TodoView.js
    import React, { useState, useRef } from "react";
    import TodoList from "../component/TodoList";
    import useTodoList from "../hooks/useTodoList";

    function TodoView() {
    const inputEl = useRef(null);
    const [todoList, addTodo, deleteToDo] = useTodoList([], inputEl);

    // use to update TodoView
    // let us check TodoList update situation
    const [count, setCount] = useState(0);

    return (
    <>
    <span>Counter : {count}</span>
    <button onClick={() => setCount(count + 1)}>Add Counter</button>
    <form className="input-container" onSubmit={addTodo}>
    <input ref={inputEl} placeholder="Type your to Do" />
    <button className="add-button">Create</button>
    </form>
    <TodoList todoList={todoList} deleteToDo={deleteToDo} />
    </>
    );
    }

    export default TodoView;

額外再加上toDoList.js加上計算器,每次的render function都會加上1,方便我們看toDoList重新render的次數。

  • component/TodoList.js
    import React from "react";
    let count = 0;

    function TodoList(props) {
    const { todoList, deleteToDo } = props;
    count = count + 1;
    return (
    <div className="list">
    TodoList render Times {count}
    {todoList.map((value, index) => (
    <li className="list-item" key={`to_${index}`}>
    <div>
    {index + 1}. {value}
    </div>
    <span onClick={() => deleteToDo(index)}>-</span>
    </li>
    ))}
    </div>
    );
    }

    export default TodoList;

React hooks 效能處理

我們已經完成了簡易版的todoList,當你輸入input建立後,會發現TodoList會更新一次,但是你點擊count後,會發現TodoList居然也會更新,這是因為所有的component已經都是純functional component,當我們最上層更新state,都會一路往下更新到底層。

這時候我們就必須依賴React.memo,React.memo是一個high Order Component,功能就像是PureComponent,讓我們擋住調用更新function,但差異在於memo是用在於function components,並會幫我們memory住props,只在props更新才會往下更新。

React memo

  • component/TodoList
    // use React memo for TodoList;
    export default React.memo(TodoList);

更新上去後,讓我們在嘗試點擊count,觀察TodoList是否就卡住更新了。

你會發現數字還是增加。

查看上層傳進的props後,發現還有一個問題,就是傳進去的function,每次都會是一個新的function。因為沒有function沒有memory住,導致每次都會render後都會重新建立addTodo、deleteToDo,所以對toDoList的memo來說,你每次都給我新的props function,當然會每次都更新component。

幸好react hooks有提供useCallback,讓我們可以把function memory起來,useCallback會需要依賴第二個參數,讓他比較判斷是否要更新function。

  • React hooks useCallback
    const memoizedCallback = useCallback(
    () => {
    doSomething(a, b);
    },
    [a, b],
    );

React hooks usecallback

const addTodo = useCallback(
event => {
event.preventDefault();
if (!inputEl.current.value) {
return;
}
const mergeArr = [...todoList, inputEl.current.value];
inputEl.current.value = "";
return setTodoList(mergeArr);
},
[todoList, inputEl]
);

const deleteToDo = useCallback(
index => {
const newArr = [...todoList];
newArr.splice(index, 1);
return setTodoList(newArr);
},
[todoList]
);

更新上去後,再嘗試點擊count看看,會發現toDoList終於沒有更新數字了。這樣就完成了hooks的render效能處理。使用React.memo實現了類似PureComponent的功能,再解決掉function components沒有memory的問題,讓我們todo、delete function,都不會因為function component被更新而重新被建立。

增加 Redux

另外改用redux管理todo資料,沒有特別用最新react-redux的hooks版本,因為還在alpha階段。基本上就移除掉useState,建立store、reducer,再建立Provider,還有state、dispatch傳遞到需要使用的元件上。

不想偏離主題就直接貼上作法了。

Source code: React hooks with redux

心得

因為準備要開始運用hooks在專案上,才發現function components要注意的問題,遠比我想像的還多。以往react class的寫法,react處理了component的rerender問題,但改為function components後,多了處理rerender的問題。

個人覺得用過class在轉用hooks後,lifeCycle的部分最不習慣,感覺拉高了點React的學習門檻。hooks讓react的複用單位拉到在更小,用得好確實能夠加速開發,期待日後實際運用hooks在專案上後能有更多心得分享。