React hook 实践体验

345 阅读6分钟

有幸参加了成都的第五届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

体验地址

codesandbox

gitHub 地址

代码解析

为了更快的开发,项目使用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中高阶组件的多层嵌套,还有mapStatemapAction的书写。为了子组件进行数据变跟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()也结合了一些之前的生命周期不再区分componentDidMountcomponentDidUpdate。除此之外我们还可以自己定制react hook进行开发,提供了相当高的自由度。

虽然react hook带来了上面这些便利性,但仍还有了一些问题,比如useEfect()因为调用时机原因,如不小心管控就很容易产生性能浪费,甚至产生bug,react hook目前也没有类似componentDidCatchand的错误捕捉钩子,以及开发习惯的改变需要一定的学习成本去适应react hook的理念。

事务的发展总是伴随着改变,前端尤其如此。我们能做的只有不断学习,拥抱变化