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>;
}
可以看到功能完全一樣,運行效果:
當組件越來越複雜類組件擴展功能首選是使用“高階組件”,這是造成代碼晦澀難懂的根源,每當增加一個邏輯組件都會外套一層,而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小應用已實現完畢。
運行效果:
可以看到,使用Hooks開發組件更加的優雅,也是React未來的趨勢。