使用Redux+Hooks完成一個小實例

95b6f8b5ab.png

React Hooks 是React在16.8版本出的新特性。在16.8以前,React函數組件無法使用state狀態、生命週期等功能,而有了Hooks,就可以使用函數式編寫和類一樣強大的組件。

類組件有什麼問題?

在以前使用一個類來封裝一個組件是很正常的事,但是類比函數複雜,即使是一個很簡單的組件,使用“類”來編寫顯得很重:

class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
            num: 0
        };
    }
    addClick = (e) => {
        this.setState({ num: this.state.num + 1 }); //狀態增1
    }
    render() {
        return <div>
            <label>num:{this.state.num}</label>
            <button type="button" onClick={this.addClick}>add</button>
        </div>
    }
}

以上是一個簡單的計數器組件,每點擊add按鈕狀態會增加1,只是這樣一個簡單的功能就會寫很多代碼,如果使用Hooks改寫爲函數組件:

function Counter(props) {
    let [num, setNum] = useState(0);
    return <div>
        <label>num:{num}</label>
        <button type="button" onClick={() => setNum(num + 1)}>add</button>
    </div>;
}

可以看到功能完全一樣,運行效果:

counter.gif

當組件越來越複雜類組件擴展功能首選是使用“高階組件”,這是造成代碼晦澀難懂的根源,每當增加一個邏輯組件都會外套一層,而Hooks會解決此類問題,讓開發者從中解脫出來。

Hooks意思是鉤子,React的意思是將組件的特性使用“鉤子”從外界鉤進來,力求達到類組件一樣豐富的功能,讓我們以函數式的思想來編寫組件。

React提供了很多現成的HooksAPI,簡單說兩個一會兒用到的:

useState

useState是React提供可以在函數組件使用狀態的鉤子,舊版的React中,函數組件只能開發一些顯示內容的簡單功能,要想使用state必須切回類組件中去。useState接收一個初始值,它返回一個數組,元素1是狀態對象,2是一個更新狀態的函數:

let [num, setNum] = useState(0);

返回數組是React讓我們更方便的重命名,我們直接解構即可,這樣每調用setNum(新值)即可更新狀態num,非常方便。

useRef

React是基於組件的技術,我們在類組件中要想直接操作DOM則是通過ref引用(使用React.createRef()),而useRef鉤子是幫助我們創建ref對象:

let inputRef = useRef(null);

同樣useRef接收一個初始值,它用來初始化對象的current屬性,注意不是current.value,這樣在相應的DOM元素上將對象賦給ref屬性即可:

<input type="text" id="name" ref={inputRef} />

想要取元素的內容通過value屬性:

console.log(inputRef.current.value); //輸出文本框的值

下面使用Hooks結合Redux編寫一個小項目

使用Redux+Hooks完成一個TODO實例

在以前剛剛學習Redux的時候我寫了一個TODO待辦的小功能,目的就是熟悉一下Redux的使用,而Hooks新特性推出了之後爲了掌握Hooks與Redux,這次寫一個Hooks的版本,功能與之前完全一至:

  • 可以讓用戶添加待辦事項(todo)
  • 可以統計出還有多少項沒有完成
  • 用戶可以勾選某todo置爲已完成
  • 可篩選查看條件(顯示全部、顯示已完成、顯示未完成)

目錄結構:

src
┗━ components 存放組件
    ┗━ TodoHeader.jsx
    ┗━ TodoList.jsx
    ┗━ TodoFooter.jsx
┗━ store 保存redux的相關文件
    ┗━ action
        ┗━ types.js 定義動作類型
    ┗━ reducer
        ┗━ index.js 定義reducer
    ┗━ index.js 默認文件用於導出store

組件分爲3個:

  • TodoHeader 用於展示未辦數量
  • TodoList 按條件展示待辦項列表及添加待辦
  • TodoFooter 功能按鈕(顯示全部、未完成、已完成)

在從頭開始時,我們先要定義好初始的狀態,在reducer目錄中新建index.js文件,定義好初始的state數據:

let initState = {
    todos: [
        {
            id: ~~(Math.random() * 1000000),
            title: '學習React',
            isComplete: true
        },
        {
            id: ~~(Math.random() * 1000000),
            title: '學習Node',
            isComplete: false
        },
        {
            id: ~~(Math.random() * 1000000),
            title: '學習Hooks',
            isComplete: false
        }
    ]
};

function reducer(state = initState, action) {}

export default reducer;

以上我們手寫3條來模擬初始數據,把它們存放到todos的數組中。接下來創建一個空的reducer方法傳入初始數據,這樣就可以基於舊的state更新出新的對象。

寫好reducer方法後,我們接下來創建redux倉庫,在store目錄中也新建一個index.js,引入剛剛寫好的reducer,即可創建出倉庫對象:

import { createStore } from 'redux';
import reducer from './reducer';
let store = createStore(reducer); //傳入reducer
export default store; //導出倉庫

這樣準備工作已經完成。

統計未完成的事項

接下來完成第一個功能,統計出TODO列表中所有未完成的數量。首先,我們定義一個頭組件TodoHeader.jsx:

import React from 'react';

function TodoHeader(props) {

    let state = useSelector((state) => ({ todos: state.todos }));
    return <div></div>;
}

export default TodoHeader;

可以看到,使用Hooks我們的組件一律使用函數寫法,目前此組件還沒有任何功能,我們先導出它給頂層組件用於注入Redux倉庫。

然後在頂層組件中使用react-redux庫提供的Provider組件注入store:

import React from 'react';
import ReactDOM from 'react-dom';
import TodoHeader from './components/TodoHeader.jsx';
import { Provider } from 'react-redux';
import store from './store';

function Index(props) {
    return <>
        <Provider store={store}>
            <TodoHeader />
        </Provider>
    </>;
}

ReactDOM.render(<Index />, document.querySelector('#root'));

可以看到,此時此刻和類組件的開發方式沒有任何區別,接下來的工作就是要在TodoHeader.jsx組件拿到倉庫數據,來編寫統計功能。

在使用Hooks開發時,關聯Redux倉庫不再使用connect高階函數來實現,react-redux包爲我們提供了一個自定義鉤子:useSelector

它的功能與高階函數connect類似,它接收兩個函數,其中第一個函數的功能就是將返回值作爲useSelector的返回值,並自動處理好訂閱,當派發動作時會自動觸發組件的渲染:

let state = useSelector((state) => ({ todos: state.todos }));    

以上返回了state.todos,這樣就可以在組件中拿到初始化的3條todos數據。下面即可編寫邏輯,統計出所有未完成的數量:

function TodoHeader(props) {

    let state = useSelector((state) => ({ todos: state.todos }));
    /**
     * 統計未完成數量
     */
    function getUncompleteCount(todos) {
        return todos.filter(item => !item.isComplete).length;
    }
    return <div>您有{getUncompleteCount(state.todos)}項TODO未完成</div>
}

多說幾句,useSelector與connect有幾處不同,userSelector可以返回任意值不僅僅是對象;而且它可以多次調用。當動作派發的時候,useSelector會將當前的結果值與上一次進行比較(使用嚴格相等===),如果相比不同,則會觸發組件的渲染。

當一個組件中多次使用了useSelector,爲了提高效率,react-redux將多次的useSelect作爲批量更新,只會渲染1次。

展示待辦列表

接下來完成展示待辦列表的功能,新建一個TodoList.jsx組件,同樣使用useSelector獲取倉庫數據:

function TodoList(props) {

    let state = useSelector((state) => state);
    //其它代碼略...
}

然後通過循環將倉庫中的todos數據渲染到頁面上,這裏抽出一個方法來實現:

    /**
     * 渲染Todo列表
     */
    function renderList(todos) {
        return todos.map((item, index) => {
            if (item.isComplete) {
                return <li key={index}>
                    <input type="checkbox" data-id={item.id} checked={true} />
                    <del>{item.title}</del>
                </li>;
            } else {
                return <li key={index} data-id={item.id}>
                    <input type="checkbox" data-id={item.id} checked={false} />
                    <span>{item.title}</span>
                </li>;
            }
        });
    }

返回此函數結果即可完成:

function TodoList(props) {
    let state = useSelector((state) => state);
    //其它代碼略...
    return <div>
        <ul>
            {renderList(state.todos)}
        </ul>
    </div>
}

更新待辦狀態

下面完成更新待辦項狀態的功能,當用戶給一條待辦打勾,就將這條數據的isComplete屬性置爲true,標記爲已完成。

由於有了用戶的操作,我們需要編寫動作Action,我們在action目錄下新建一個types.js,用於存放動作類型:

//更新完成狀態
export const TOGGLE_COMPLETE = 'TOGGLE_COMPLETE';

以上就定義好了一個動作類型,可以看到非常簡單,就是一個描述Action的字符串指令。

接下來爲checkbox添加事件,當用戶勾選了某一條待辦,將記錄的id值傳給reducer來作更新:

<input type="checkbox" 
    data-id={item.id} 
    checked={true} 
    onChange={itemChange} />
function TodoList(props) {

    let dispatch = useDispatch(); //取得派發方法
    /**
     * Todo勾選事件
     */
    function itemChange(e) {
        const { target } = e;
        //派發TOGGLE_COMPLETE動作以更新倉庫
        dispatch({
            type: TOGGLE_COMPLETE, payload: {
                id: target.dataset.id, //取得當前id值
                isComplete: target.checked
            }
        });
    }
    //其它代碼略...
}

以上使用了react-reudx庫提供的第2個勾子方法:useDispatch

在使用Redux時,更新倉庫的唯一辦法就是使用派發方法dispatch來派發一個動作,在使用Hooks開發組件,useDispatch返回一個Redux庫的dispatch方法引用,使用它與之前類組件通過connect的方式完全一致。

接下來就是在reducer中處理更新邏輯:

function reducer(state = initState, action) {
    let nextState = null;
    switch (action.type) {
        case TOGGLE_COMPLETE:
            nextState = {
                ...state,
                todos: state.todos.map((item) => {
                    //將倉庫中id爲payload.id的那一條記錄更新
                    if (item.id == action.payload.id) {
                        return { 
                            ...item, 
                            isComplete: action.payload.isComplete 
                        };
                    } else {
                        return item;
                    }
                })
            };
            break;
            //其它代碼略...
        default:
            nextState = state;
            break;
    }
    return nextState;
}

以上通過一個TOGGLE_COMPLETE分支來判斷是不是“更新待辦狀態”這個動作,然後找到參數中傳來的id,將對應的記錄更新即可。

使用Hooks要時刻記住reducer是一個純函數,一定要保證每一次返回的結果都是一個新對象,因此todos的更新不要使用slice來直接修改(引用地址不變)。

添加待辦

添加待辦要求用戶在一個文本框中輸入內容,將數據添加到倉庫中。

還是一樣的套路,在type.js中新增一個動作類型,用於描述添加待辦:

//添加TODO
export const ADD_TODO = 'ADD_TODO';

我們在TodoHeader.jsx組件中增加一個輸入域,用於接收用戶輸入的內容:

function TodoHeader(props) {

    let newTodoInput = useRef(null); //創建ref對象
    /**
     * 添加按鈕事件
     */
    function addClick(e) {
        //略...
    }
    //其它代碼略...

    return <div>
        <div>您有{getUncompleteCount(state.todos)}項TODO未完成</div>
        <div>
            {/* 將ref對象綁定到元素中 */}
            <input type="text" ref={newTodoInput} />
            <button type="button" onClick={addClick}>添加</button>
        </div>
    </div>
}

以上使用了React爲我們提供了另一個鉤子方法:useRef,使用它來創建一個ref對象將它綁定到對應的DOM元素上,即可以取得真實的DOM結點。這樣我們就可以方便的拿到用戶輸入的內容:

    function addClick(e) {
        //current即真實的input結點,value即輸入域的值
        let title = newTodoInput.current.value;
        dispatch({
            type: ADD_TODO, payload: {
                id: ~~(Math.random() * 1000000),
                title,
                isComplete: false
            }
        });
    }

接着還是派發對應的ADD_TODO動作即可,傳入用戶輸入的內容,並生成一個新id。

在reducer中再增加一處邏輯分支,用於處理“添加待辦”:

function reducer(state = initState, action) {
    let nextState = null;
    switch (action.type) {
        case ADD_TODO:
            //將新記錄增加到倉庫中
            nextState = {
                ...state,
                todos: [...state.todos, action.payload]
            };
            break;
        //其它代碼略...
        default:
            nextState = state;
            break;
    }
    return nextState;
}

仍要注意返回的對象要是個新的,到此添加功能已經完成。

刪除功能非常簡單,不在多說。

篩選查看條件

最後一個功能,根據用戶指定的條件來過濾數據的顯示。我們修改一下倉庫的初始值 ,增加一個“顯示條件”:

let initState = {
    display: 'all', //display用於控制顯示內容
    todos: [
        {
            id: ~~(Math.random() * 1000000),
            title: '學習React',
            isComplete: true
        },
        //略...
    ]
}

display用於控制數據顯示的內容,它只有3個值:已完成(complete)、未完成(uncomplete)和顯示全部(all),我們默認定義爲顯示全部:“all”。

仍然是先定義好動作類型:

//篩選查看
export const FILTER_DISPLAY = 'FILTER_DISPLAY';

新建一個TodoFooter.jsx組件,放入3個按鈕,分別對應3個篩選條件:

function TodoFooter(props) {

    const dispatch = useDispatch();

    /**
     * 篩選查看事件(dispaly爲all,complete,uncomplete3個值)
     */
    function displayClick(display) {
        dispatch({ type: FILTER_DISPLAY, payload: display });
    }

    return <div>
        <hr />
        <button 
            type="button"
            onClick={() => displayClick('all')}>顯示全部</button>
        <button 
            type="button"
            onClick={() => displayClick('complete')}>已完成</button>
        <button 
            type="button"
            onClick={() => displayClick('uncomplete')}>未完成</button>
    </div>
}

可以看到,這次抽出一個方法displayClick用於處理3個按鈕對應的“條件”,將全部、已完成和未完成作爲參數傳入事件函數,派發到倉庫即可。

接下來的工作就是再增加一個reducer分支,更新倉庫中的display即可:

function reducer(state = initState, action) {
    let nextState = null;
    switch (action.type) {
        case FILTER_DISPLAY:
            nextState = {
                ...state,
                //將倉庫中的display條件更新
                display: action.payload
            };
            break;
        //其它代碼略...
        default:
            nextState = state;
            break;
    }
    return nextState;
}

最後一步,在渲染TODO列表時,根據倉庫的display條件渲染即可:

    function renderList(todos, display) {
        return todos.filter(item => {
               //根據display的分類來返回不同的數據
            switch (display) {
                case 'complete':
                    return !item.isComplete;
                case 'uncomplete':
                    return item.isComplete;
                default:
                    return true;
            }
        }).map((item, index) => { //略... });
    }

到此,一個ReduxHooks版本的TODO小應用已實現完畢。

運行效果:
finish0123.gif

可以看到,使用Hooks開發組件更加的優雅,也是React未來的趨勢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章