iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 15
0
Modern Web

激戰 ReactJS 30天系列 第 15

【Day15】 進入最佳狀態 - Lifting State Up

在程式開發過程常常會出現一種情況
一個屬性或狀態同時控制好幾個網頁元素
這個時候怎麼寫就會直接影響著網頁的運行效能
為了養成良好的習慣
今天就來談談應該要怎麼寫會比較好吧!

Lifting State Up

如果多個元素被一個因素影響
多個元素又各自獨立
那麼運行起來大家各自忙碌
造成許多額外的效能浪費
網頁的效能如果不好
使用者體驗就會變差
這並不是我們所期望的結果

因此
React 建議
當出現資料同時影響著多個組件(網頁元素)的時候
應該把這個資料提升到最接近這些組件的共同祖先
這麼一來當資料發生改變
就能夠一次更新那些需要被改變的組件
達到重複使用的精神
也讓程式變得更好管理

官網提供了一個 攝氏華氏溫度計 的範例來說明這件事情
不過任性如我當然就要自己想一個例子來做囉
所以說
我決定要來做個 公分公尺換算器 來記錄今天的學習!

Coding

要想做一個長度換算器
首先我們需要讓使用者輸入的欄位

import React from 'react';
import ReactDOM from 'react-dom';

class LengthInput extends React.Component{
    constructor(){
        super();
        this.state = {
            length : "",
        }
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e){
        this.setState({length: e.target.value});
    }

    render(){
        return(
            <fieldset>
                <legend>公分</legend>
                <input type="text" value={length} onChange = {this.handleChange} />
            </fieldset>
        );
    }
}
export default LengthInput;

先產生一個讓使用者輸入長度的組件
裡面有一個叫做length的狀態用來存放輸入的值
handleChange函式更改狀態為輸入的內容
執行結果:
https://ithelp.ithome.com.tw/upload/images/20180103/201076740gWt6AK1R6.png
由於至少要兩個欄位才能夠互相轉換
所以這邊要再新增一個輸入欄位

import React from 'react';
import ReactDOM from 'react-dom';

const scaleName = {
    cm: '公分',
    m: '公尺'
}

class LengthInput extends React.Component{
    constructor(){
        super();
        this.state = {
            length : "",
        }
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e){
        this.setState({length: e.target.value});
    }

    render(){
        const length = this.state.length;
        const scale = this.props.scale;
        return(
            <fieldset>
                <legend>{scaleName[scale]}:</legend>
                <input  type="text" 
                        value={length} 
                        onChange = {this.handleChange} />
            </fieldset>
        );
    }
}

class Caculator extends React.Component{
    render(){
        return(
            <div>
                <LengthInput scale='cm' />
                <br />
                <LengthInput scale='m' />
            </div>
        );
    }
}
export default Caculator;

在這裡我們使用同一個LengthInput組件來產生第二個輸入欄位
透過傳入的 props 來分辨兩個輸入欄位
並且建立一個新的組件作為他們的的父組件(parent)
也就是整個長度換算器的外觀了
執行結果:
https://ithelp.ithome.com.tw/upload/images/20180103/201076745AQsw0fIlp.png
接下來要讓單位轉換並且呈現在畫面上
所以我們需要寫轉換的function:

function toMeter(cm){
    return cm / 100;
}

function toCentermeter(m){
    return m * 100;
}

function doConvert(unit, convert){
    const input = parsetFloat(unit);
    if(isNaN(input))
        return '';
    const output = convert(input);
    return output.toString();
}

toMetertoCentermeter是基本的轉換函式
doConvert是因為要讓他們呼叫同一個函式
透過這個函式來判斷要如何轉換單位
參數unit是用來存放長度的數值資料
convert則是應該要呼叫的轉換函式
寫到這邊會出現一個問題:

長度單位分別在兩個獨立的組件內部,要怎麼讓他們同步轉換呢?

除此之外
兩個組件明明使用的是同一份資料(長度)
卻各自持有一個獨立的狀態重複存放
讓帶有狀態的組件數量增加了一個
這樣造成網頁的效能下降
所以
接下來我們要把這兩個組件的狀態合併起來
讓兩種長度單位能同步轉換
同時實作今天的主要目標 - 提升狀態(lifting state up)

首先先調整LengthInput的資料來源:

class LengthInput extends React.Component{
    constructor(){
        super();
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e){
        // this.setState({length: e.target.value});
        this.props.onLengthChange(e.target.value);
    }

    render(){
        const length = this.props.length;
        const scale = this.props.scale;
        return(
            <fieldset>
                <legend>{scaleName[scale]}:</legend>
                <input  type="text" 
                        value={length} 
                        onChange = {this.handleChange} />
            </fieldset>
        );
    }
}

這裡我們拿掉了組件的狀態資料 state
然後在handleChange的部分
因為 props 是不能被更動的東西
所以必須呼叫父組件內部的函式
透過更新父組件的狀態來改變來自父組件的狀態資料
也就是const length = this.props.length;這句程式碼的意義了

子組件處理完後
現在要來把狀態移動到他們最接近的共同祖先
也就是Calculator這個組件啦
程式碼:

import React from 'react';
import ReactDOM from 'react-dom';

const scaleName = {
    cm: '公分',
    m: '公尺'
}

class LengthInput extends React.Component{
    constructor(){
        super();
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e){
        // this.setState({length: e.target.value});
        this.props.onLengthChange(e.target.value);
    }

    render(){
        const length = this.props.length;
        const scale = this.props.scale;
        return(
            <fieldset>
                <legend>{scaleName[scale]}:</legend>
                <input  type="text" 
                        value={length} 
                        onChange = {this.handleChange} />
            </fieldset>
        );
    }
}

function toMeter(cm){
    return cm / 100;
}

function toCentermeter(m){
    return m * 100;
}

function doConvert(unit, convert){
    const input = parseFloat(unit);
    if(isNaN(input))
        return '';
    const output = convert(input);
    return output.toString();
}

class Calculator extends React.Component{
    constructor(){  
        super();
        this.state = {
            length:"",
            scale:"cm"
        }
        this.handleCentermeterChange = this.handleCentermeterChange.bind(this);
        this.handleMeterChange = this.handleMeterChange.bind(this);     
    }

    handleCentermeterChange(len){
        this.setState({length:len , scale:'cm'});
    }

    handleMeterChange(len){
        this.setState({length:len , scale:'m'});
    }
    render(){
        const scale = this.state.scale;
        const length = this.state.length;
        const centermeter = 
        scale === 'm' ? doConvert(length, toCentermeter) : length;
        const meter = 
        scale === 'cm' ? doConvert(length, toMeter) : length;

        return(
            <div>
                <LengthInput scale='cm' length={centermeter} onLengthChange={this.handleCentermeterChange}/>
                <br />
                <LengthInput scale='m' length={meter} onLengthChange={this.handleMeterChange}/>
            </div>
        );
    }
}
export default Calculator;

在這段程式碼中
我們把使用者輸入的長度值統一先記錄在Calculator這個組件中
不論使用者輸入的位置是哪一個
Calculator都有對應的函式從子組件偵測並且回來呼叫父組件的函式
透過這個組件的狀態改變
同步傳遞 props 變動未被輸入的那個欄位的內容
執行結果:
https://ithelp.ithome.com.tw/upload/images/20180103/20107674w0AVPpGDIf.png
透過今日這個任性的練習
簡單來說
狀態提升可以重點紀錄成:

  1. 把狀態資料及處理的函式提升到最接近的祖先以減少攜帶狀態的組件數目。
  2. 子組件要接上反應行為函式,但僅負責偵測變化以及呈現網頁元素。
  3. 透過 props 同時從共同祖先傳遞資料與處理的函式到子組件。

參考資料

  1. tutorialspoint-ReactJS Tutorial
  2. React 官方文件

>>> 隊友任意門 <<<


Day15 end
by 瑞Ray ε≡ヘ( ´∀`)ノ


上一篇
【Day14】 我問你要答R - Forms
下一篇
【Day16】 合成合成雞蛋糕 - Composition
系列文
激戰 ReactJS 30天31

尚未有邦友留言

立即登入留言