Hook 是 React v16.8 的新功能,讓我們在撰寫 React Component 時,
可以讓我們用 function 的寫法來取代 Class 的寫法,
來避免在 Class 中常會碰到的問題,例如:
1. Class 中的 this 意義常讓人無法容易地理解。
2. Component 上的事件 (event) 綁定,常須要多寫 bind()
的步驟,並且涵義也容易讓人不容易了解,例如:
class TextInput extends React.Component{
constructor(props){
super(props);
this.onHandleTextChanged = this.onHandleTextChanged.bind(this);
}
onHandleTextChanged(e){
this.props.handleTextChanged(e.target.value);
}
render(){
return (
<div>
<input type="text" value={this.props.text} onChange={this.onHandleTextChanged} />
</div>
);
}
}
而因為 function 不像 Class 可以儲存保存自己的成員變數,
為了可以實現像 Class 那樣保存、修改依賴於 Component 的成員變數,
React 使用了 useState() 這個 function,其可以以一個初始值做為輸入,
並返回一個包含 變數、修改變數的 function 的 array,
Ex:
import { useState } from "react"
const [text, setText] = useState("initial value");
React Redux 也推出了使用 React Hook 的版本,
其提供了 useSelector() 和 useDispatch() ,
useSelector() 可以讓我們方便得取得 Reducer Storde 裡 state 的值,
而不用像以前一樣使用
connect(mapStateToProps, actionCreators)
this.props.XXX
等的方法來取得 state 值,
Ex:
import {useSelector} from 'react-redux'
const toDoList = useSelector(state => state.toDoListReducer.toDoList);
useDispatch() 可以讓我們方便得送入要傳給 Reducer 的 Action 來觸發 Component
render 元件繪製的行為,
而不用像以前一樣使用
connect(mapStateToProps, actionCreators)
this.props.xxxAction()
等的方法來傳遞 Action 給 Reducer,
Ex:
import {useDispatch} from 'react-redux'
import * as actionCreators from './action'
const dispatch = useDispatch();
function handleDeleteToDo(){
dispatch(actionCreators.deleteToDo(toDo.id));
}
這篇文要用 React Hook 來改寫上一篇,
React - Redux 練習 - ToDo List (待辦事項) 簡易實作,
做為練習,並比較沒用 Hook 及有用 Hook 兩者程式碼的差異。
跟之前的例子比,有改動的檔案只有:
Main.jsx
TextDisplayer.jsx
TextInpupt.jsx
ToDoList.jsx
ToDoRow.jsx
如下圖所示
接著來看程式碼的部份:
Main.jsx :
import React, { useState } from "react"
import {TextDisplayer} from "./TextDisplayer.jsx"
import TextInput from "./TextInput.jsx"
import ToDoList from "./ToDoList.jsx"
function Main(){
const [text, setText] = useState("");
return (
<React.Fragment>
<TextInput text={text} handleTextChanged={setText}/>
<TextInput text={text} handleTextChanged={setText}/>
<TextDisplayer text={text}/>
<ToDoList/>
</React.Fragment>
);
}
export default Main
說明:
可以看到原本的 Main 不用 Class ,而改用了 function 去改寫,
因為只是 function ,所以連 constructor() 也不用了。
原本在用 Class Component 時,Component 所管理的 text 這個成員屬性,
是用 this.state = {text : ""} 的方式指定初始值的。
改用 Function Component 時,我們使用了 useState(初始值)
的方式來獲得設定了初始值的 text 變數,並且還得到了一個可以改變 text 的
function,即 setText() ,
const [text, setText] = useState("");
中的 text 及 setText() ,就跟以前一樣,
可以使用在 Main Component 中做UI的顯示,
也可以傳遞給 Child Component ,讓下層元件可以顯示及修改 text。
例如在 Main Component 中,我們把 text 及 setText()
傳遞給 TextInput、TextDisplayer 及 ToDoList。
TextDisplayer.jsx :
import React from "react"
function TextDisplayer({text}){
return (<div>Text you typed : {text}</div>);
}
export {TextDisplayer}
說明:
TextDisplayer Component 也寫成了 Hook 的 Function Component
的形式,
此時接收上層元件的值改由從 Function Component 的輸入值傳進來,
輸入會是一個 Object, key 及 value 由上層元件傳進來的時候指定,
例如 在 Main.jsx 中用 text={text} 的方式傳 text 進 TextDisplayer 元件,
TextDisplayer 的 function component 輸入就會是:
{text : text的值}
TextInpupt.jsx :
import React from "react"
import {useDispatch} from 'react-redux'
import * as actionCreators from './action'
function TextInput({text, handleTextChanged}){
const dispatch = useDispatch();
function onHandleTextChanged(e){
handleTextChanged(e.target.value);
}
function onHandleAddToDo(e){
dispatch(actionCreators.addToDo(new Date().getTime(), text));
handleTextChanged("");
e.preventDefault();
}
return (
<div>
<form onSubmit={onHandleAddToDo}>
<input type="text" value={text} onChange={onHandleTextChanged} />
<input type="submit" value="addToDo"/>
</form>
</div>
);
}
export default TextInput
說明:
TextInput Component 也被改寫成了 Hook 的 Function Component 形式,
此時不像 Class Component 時使用 this.props 的方式來取得上層元件傳進來的值,
而是直接從 Function Component 的 Input 輸入取得,
輸入會是一個 Object,其中會包括由上層元件傳進來的值,例如此例就是 Main.jsx
傳進來的
text 及 handleTextChanged 。
可以注意到的是, onChane 上面綁定的 onHandleTextChanged(),
不用再像以前 Class Component 那樣須要使用 bind() ,例如以前的寫法:
this.onHandleAddToDo = this.onHandleAddToDo.bind(this);
。
另一個可以注意到的是,我們沒有使用到如
export default connect(mapStateToProps, actionCreators)(TextInput)
的語法去得到 Reducer Store 裡的變數及函式,
那我們要怎麼把 Action 傳遞給 Reducer 呢?
答案是使用 Redux Hook 提供的 useDispatch() ,
其他傳回一個函式 dispatch(要送給 Reducer 的 Action) ,
我們可以直接把要傳遞的 Actoin 傳給 Reducer。
ToDoList.jsx :
import React from "react"
import ToDoRow from "./ToDoRow.jsx"
import {useSelector} from 'react-redux'
function ToDoList(){
const toDoList = useSelector(state => state.toDoListReducer.toDoList);
return (
<ul>
{
toDoList.map(function(toDo){
return (
<ToDoRow toDo={toDo} key={toDo.id}/>
);
})
}
</ul>
);
}
export default ToDoList
說明:
ToDoList 元件跟 TextInput 元件一樣,沒有使用 connect() 來從 Reducer Store
取得資料,
那要怎麼取得 Reducer Store 中取得資料呢? 例如此例是要取得 toDoList。
答案是使用 Redux Hook 提供的 useSelector(Reducer 的 state) ,useSelector()會有
Redcuer 的 state 作為 callback 輸入,這樣我們就能取得 Reducer State 裡的資料,
在這裡因為我們之前有在 reducer.js 裡使用了 combineReducers() 去 combine
toDoListReducer,所以取得 toDoList 的方式為 state.toDoListReducer.toDoList ,
如果沒有用 combineReducers() 的話,就會改成用
state.toDoList 直接取得值。
ToDoRow.jsx :
import React from "react"
import {useDispatch} from 'react-redux'
import * as actionCreators from './action'
function ToDoRow({toDo}){
const dispatch = useDispatch();
function handleDeleteToDo(){
dispatch(actionCreators.deleteToDo(toDo.id));
}
return (
<li>
{toDo.toDoText}
<button onClick={handleDeleteToDo}>Delete</button>
</li>
);
}
export default ToDoRow
說明:
ToDoRow 元件從本身 Function Component 的輸入中取得由 ToDoList 元件來的 toDo,
並且跟 TextInput 元件一樣使用了 dispatch() 來傳遞要送給 Reducer 的
Action,此例為 actionCreators.deleteToDo(toDo.id)
-------------------------------------------------------------------------------------
最後我們可以大概總結一下在這篇改用 React Hook 的文中,跟
React - Redux 練習 - ToDo List (待辦事項) 簡易實作
這篇沒使用 React Hook 文不一樣的地方:
-
Component 元件全部由 Class 寫法改成 Function 寫法,
故少了 Class
特性的語法,例如: constructor()、 this 的用法、 bind() 的用法。
-
Component 自己管理的 state (例如 this.state = {text}),因為無法使用 Class 的
this 寫法,
所以改成用 Hook 來管理,使用方法為 [變數名, 可修改變數的
function] = useState(初始值)。
-
上層把變數傳給下層的方法跟以前一樣,只是下層 Hook Component 因為不是
Class,
所以不用 this.props ,改用從 Function Component
本身的輸入去取得,例如:
function ChildComponent({inputValue1,
inputValue2, ...})
-
在取得 Reducer 管理的 state 方面,不使用 connect() 的方法來將 reducer state
及 action 綁到 this.props 上 ,而是使用 Redux Hook 所提供的 useSelector()
來取得,例如:
var 要取得的 state value 變數 =
useSelector(function(reducer的state){return 要的 value 變數});
-
在傳遞 Action 給 Reducer 方面,不使用 connect() 的方法來將 reducer state 及
action 綁到 this.props 上,而是使用 Redux Hook
所提供的 useDispatch() ,
useDispatch() 可以回傳一個
dispatch(),
dispatch() 可以輸入一個 action,其會將 action 傳遞給
Reducer,例如:
var dispatch = useDispatch();
dispatch(要傳遞的
action)。
最後要注意的地方是,
useState() 、useSelector()、useDispatch() 等
都要在 Function Component 的最上層呼叫,
即不要在迴圈、條件式或是巢狀的 function 內呼叫 Hook (參考)。
要理解這個,我們可以從 Hook 實作的原理去理解。
首先是,為什麼元件寫成 function ,卻可以如 Class 搬擁有自己可管理的 state (例如 text),
因為 function 裡的所有變數應該都只是區域變數而己 (local variable),
應該無法長期保存其值 (不管有沒有被修改)。
React Hook 的實作方式可以簡單理解為 (**不代表真實實作的細節,只是類似,可以從這想法大致理解 Hook 的概念),
使用了兩個(或多個,端看 React Hook 的詳細實作)陣列 (或鍵結串列等類似有序串列的資料結構) 來儲存了所有使用
useState() 設定的 值 (暫且稱作 state value) 及 可修改值的function (暫且稱作 setState function),例如:
[value1, value2, value3, ...] (暫且稱作 state array)
[setValu1, setValu2, setValu3, ...] (暫且稱作 setState array)
在 React 要重新繪製 (Render) 所有元件時,就會依序執行所有function component 的 function,
如果是第一次繪製,呼叫 useState() 時,各 state value 會用初始值儲存在 state array 中,
並且在相應的 index 中存放可修改 value 的函式在 setState array 中 (所以函式會知道它要去修改哪個 state value)。
如果是之後的繪製,呼叫 useState() 時,各 state value 會以同樣順序的方式從 state array 中取出,
因為 state array 只是個陣列,所以每次取出時必須要照順序,因為它並不知道要取出的 state value 是給哪個 Component 用的,
這樣就能理解,為什麼在 Component 中,只能一次的,在最上層呼叫 useState(),
因為這樣才能確保取出 state value 時,每次都是照一樣的順序。
源碼分享下載:
ReactReduxTest.7z
本Blog的其他相關文章:
參考資料:
- 使用 State Hook
- 打造你自己的 Hook
-
React Redux | 小孩子才做選擇! Hooks 和 Redux 我全都要!
- 原始碼解析 React Hook 構建過程:沒有設計就是最好的設計
- React Hooks 不是黑魔法,只是陣列
- [译] 深入 React Hook 系统的原理
- Hooks
- How to pass a React Hook to a child component