有幸参加了成都的第五届FEDAY,对各位大神的分享感触很深,其中关于reack hook的重构应用分享引起了我的兴趣,之前虽然有看过一些关于react hook的文章,甚至有react hook取代redux之说,开始也是并没有太在意,认为这大概是对一些新技术的吹捧,redux作为react生态数据管理的一哥,怎么可能轻易被钩子函数取代,听了unbug的分享让我想法有了改观,百闻不如一见,于是有了下面实践,用react hook重构todo list小应用。
什么是react hook
Hook 这个单词的意思是"钩子"。React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。
你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。 我实践的todo list中用到了4个最常用的钩子,其他的钩子大家有兴趣可以自己再尝试
useState()
useContext()
useReducer()
useEffect()
界面展示
为了样式美观引入了antd
体验地址
代码解析
为了更快的开发,项目使用create-react-app作为脚手架,引入了andtUI库
数据获取
原本打算用线上的easy-mock作为接口,提供假数据,但是接口太不稳定,所以自己模仿接口提供数据api.js
const list = [
{
id:1,
name:'task1',
done:false
}
...
]
export default function getList() {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(list)
},100)
})
}
然后用useEffect()钩子在组件挂载的时候获取数据,在这里我也遇到一个坑,useEffect()可以看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 的组合,开始只传了一个参数,流程是这样的 组件挂载->调用useEffect获取数据更新组件->更新组件->调用useEffect,进入了一个死循环,原来useEffect接收2个参数,后面还可以加一个依赖数组对象,当数组对象变化后会触发useEffect(),如果不依赖外部对象只想useEffect运行一次,可以传一个空数组,我是这样解决的,如果你有更好的解决方案也可以与我交流。
function App() {
...
// 用useEffect钩子发送请求
useEffect(() => {
getList().then(res=>{
dispatch({type:'INIT',list:res})
})
},[]);
...
return (
// 用AppContext.Provider提供共享的数据
<AppContext.Provider value={{todoList:state,dispatch}}>
</AppContext.Provider>
);
}
数据管理
这里用useReducer()钩子结合React.createContext()api进行数据管理,React.createContext()新建了一个context对象,作为组件的共享数据,组件用useReducer()钩子传入写好的reducer来管理数据,虽然原理类似redux,但是书写更清爽,避免了redux中高阶组件的多层嵌套,还有mapState、mapAction的书写。为了子组件进行数据变跟dispach方法也需要作为数据传入。同时要导出context对象给子组件引用。
reducer.js
export default function todos(state = [], action) {
switch (action.type) {
case "INIT":
return [...action.list];
case "ADD_TODO":
return [...state, action.todo];
case "DEL_TODO":
return state.filter(item => {
return item.id !== action.id;
});
case "TOGGLE_TODO":
return state.map(item =>
item.id === action.id ? { ...item, done: !item.done } : item
);
default:
return state;
}
}
App.js
import React,{useState,useEffect,useReducer} from 'react';
import getList from './api'
import todosReducer from './reducer'
export const AppContext = React.createContext({}) //新建AppContext对象
function App() {
// reducer钩子
const [state, dispatch] = useReducer(todosReducer, []);
// 用useEffect钩子发送请求
useEffect(() => {
getList().then(res=>{
dispatch({type:'INIT',list:res})
})
},[]);
return (
// 用AppContext.Provider提供共享的数据
<AppContext.Provider value={{todoList:state,dispatch}}>
</AppContext.Provider>
);
}
export default App;
子组件通过useContext()钩子获取AppContext数据对象,再从里面解析出需要的数据,需要修改数据时就dispachx相应的方法
import React, { useContext } from "react";
import { List, Button } from "antd";
import { AppContext } from "./App";
import classNames from "classnames";
const TodoList = props => {
// 使用useContext钩子获取共享的AppContext数据
const { todoList, dispatch } = useContext(AppContext);
const onDelete = id => {
dispatch({ type: "DEL_TODO", id });
};
const onToggleFinished = id => {
dispatch({ type: "TOGGLE_TODO", id });
};
return (
<List>
{todoList.map(item => {
const className = classNames({
"list-item": true,
"list-item__finished": item.done
});
let {queryText} = props
if (queryText && !item.name.includes(queryText)) return null;
return (
<List.Item className={className} key={item.id}>
<div className="list-item-wrap">
<span
style={{ width: "80%" }}
onClick={() => onToggleFinished(item.id)}
>
{item.name}
</span>
<Button size="small" type="danger" onClick={() => onDelete(item.id)}>删除</Button>
</div>
</List.Item>
);
})}
</List>
);
};
export default TodoList;
state管理
useState()用于为函数组件引入状态。纯函数不能有状态,所以把状态放在钩子里面。于是我们可以用跟轻便的方法去管理我们的数据,不用再xx.bind(this)我们的方法,也不用this.setState()了。
useState()这个函数接受状态的初始值,作为参数,返回一个数组,数组的第一个成员是一个变量指向状态的当前值。第二个成员是一个函数,用来更新状态。如下面的输入框我们可以用const [todoName, setTodoName] = useState('');进行组件内部数据的管理
import React,{useContext,useState} from "react";
import { Input, Button, Row, Col } from "antd";
import {AppContext} from './App'
const AddTodo = () => {
const { dispatch,todoList } = useContext(AppContext)
// useState()对组件内的状态进行管理
const [todoName, setTodoName] = useState('');
const len = todoList.length
const lastOne = len>0?todoList[len-1]:{id:0}
const addTodo = ()=>{
dispatch({type:'ADD_TODO',todo:{id:lastOne.id+1,name:todoName,done:false}})
setTodoName('')
}
return (
<Row style={{marginTop:'10px'}}>
<Col span={16}>
<Input
onPressEnter={addTodo}
placeholder="添加任务"
value={todoName}
onChange={e => setTodoName(e.target.value)}
></Input>
</Col>
<Col span={2}></Col>
<Col span={6}>
<Button type="primary" onClick={addTodo}>添加任务</Button>
</Col>
</Row>
);
};
export default AddTodo;
结语
经过todoList这个小应用对react hook的实践,更新了我对react hook的认知,通过使用react hook我们可以不用再使用class XXX extends React.Component这样的class类写法,让组件更趋近于函数而不是类,也更符合react一切皆函数的哲学,我们可以用userReducer()来进行复杂数据交互的状态管理,而不用引用其他第三方库来加重应用,避免多级组件嵌套的复杂结构,让你的代码变得更简洁优雅。用useState()将状态和状态变更一起管理,再也不用setState()、XXX.bind(this),useEfect()也结合了一些之前的生命周期不再区分componentDidMount和componentDidUpdate。除此之外我们还可以自己定制react hook进行开发,提供了相当高的自由度。
虽然react hook带来了上面这些便利性,但仍还有了一些问题,比如useEfect()因为调用时机原因,如不小心管控就很容易产生性能浪费,甚至产生bug,react hook目前也没有类似componentDidCatchand的错误捕捉钩子,以及开发习惯的改变需要一定的学习成本去适应react hook的理念。
事务的发展总是伴随着改变,前端尤其如此。我们能做的只有不断学习,拥抱变化