共计 8241 个字符,预计需要花费 21 分钟才能阅读完成。
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 未来的趋势。