React Hook 实战指南!(4)

2,977 阅读7分钟

业务逻辑与其他Hooks

在这一篇内容中,我们一起来看一下其他的Hooks在业务中的使用。
首先,我们先来研究一下Todolist根组件的实现,首先,TodoList根组件需要利用useTodolistStoreContext来获取到items和actions,因为如果items在初始状态没有数据的话,需要调用actions中的getInitialItems方法来获取数据:

const TodoListPage = (props) => {
  const [items, actions] = useTodolistStoreContext('items')
  useEffect(() => {
    if (!items.length) actions.getInitialItems()
  })
  // ... 忽略其他
}

useEffect

此时我们看到useEffect这个Hook,它是做什么的呢?很简单,useEffect其实就可以理解为函数组件的生命周期函数,因为函数组件不是类组件,无法使用componentDidMount等生命周期钩子,所以Hook提供useEffect Hook来监听数据的变化,来替代生命周期钩子,它其实是componentDidMount、componentDidUpdate、componentWillUnmount三个钩子函数的组合体,用起来确实方便来很多,但是类组件可以是有十来个钩子的,就算useEffect能顶三个,能够用吗?真的够用了,不信一起来看:

  1. 组件还未渲染前的动作?直接在return 组件结构上面执行就可以 (componentWillMount)
  2. 组件初次渲染后的动作?这个时候useEffect()传入的函数会执行哟 (componentDidMount)
  3. 组件props或者state变化后的动作?这个时候useEffect()传入的函数也会执行哟 (componentDidUpdate)
  4. 组件销毁前的动作?这个时候useEffect()传入的函数当然会执行啦 (componentWillUnmount)

有这些就足够了呢,而且useEffect可以多次使用,每次第二个参数传入监听的数据后,可以根据监听的数据是否变化决定是否执行,多么智能。

const Example = (props) => {
  let [count, setCount] = useState(0)
  // 这样,初始化的时候,effect会执行,而且无论是count变化还是props变化,effect也都会执行
	useEffect(() => {
  	
  })
  // 这样表示下面的这个effect什么都不监听,初始的执行一次后,不管props还是state变化都不再执行了
  useEffect(() => {
  	
  }, [])
  // 除了初次执行,props变化会执行
  useEffect(() => {
  	
  }, [props])
  // 除了初次执行,props.title变化会执行
  useEffect(() => {
  	
  }, [props.title])
  // 除了初次执行,props.title变化会执行,而且count变化也会执行
  useEffect(() => {
  	
  }, [props.title, count])
}

啧啧啧,真是甩往常的update阶段的钩子函数五条街都不知道,比如如果我们想要在某个数据变化后才去做某个动作,在往常的钩子中需要手动判断该数据是否变化: if (state.a !== this.state.a) { ..... }, 而useEffect的使用就简单多了,传个参数就可以了。

我们上面的代码也很简单,就是当items数据没有的时候就调用actions的方法去获取下初始的数据。

接下来因为要编辑item,所以我们需要一个用来存放编辑的item的状态,而且为了控制TodoListCAE组件(左侧划出的抽屉)显示隐藏,所以也需要一个状态来控制,那么在TodoList这样的函数组件中如何构建一个状态呢?

useState这个Hook终于闪亮登场了,它的使用非常简单,前面以及介绍过了。

各个组件的实现

下面直接放出最终的TodoList根组件:

import { useTodolistStoreContext } from '@/hooks/todolist'

import TodoListContent from './particles/TodoListContent'
import TodoListDes from './particles/TodoListDes'
import TodoListCAE from './particles/TodoListCAE'

const TodoListPage = (props) => {
  // 取出store的items数据和actions
  const [items, actions] = useTodolistStoreContext('items')
  // 建立控制右侧抽屉显示的状态,默认值为false
  let [visible, setVisible] = useState(false)
  // 建立用来编辑的item,默认值为null
  let [editItem, setEditItem] = useState(null)
  
  // 当items数据没有的时候就去初始的获取
  useEffect(() => {
    if (!items.length) actions.getInitialItems()
  })

  // 右侧抽屉显示隐藏动作
  function toggleVisible () {
    setVisible((visible) => !visible)
    setEditItem(null)
  }
  // 编辑动作
  function editAction (item) {
    setVisible((visible) => !visible)
    setEditItem(item)
  }

  return (
    <div className="page-container todolist-page">
      <Typography>
        <Typography.Title level = {3}>待办事项</Typography.Title>
        <TodoListDes/>
        {/* 新建按钮 */}
        <Button onClick={toggleVisible} className="create-btn" type="primary" shape="circle" icon="plus" size="large" />
      </Typography>
      {/* 新建和编辑的右侧抽屉 */}
      <TodoListCAE editItem={editItem} visible={visible} toggleVisible={toggleVisible}/>
      {/* 显示items的内容 */}
      <TodoListContent editAction={editAction} toggleVisible={toggleVisible}/>
    </div>
  )
  
}

export default TodoListPage

TodoListContent组件也很简单,因为需要实现分页,所以也需要构建page状态来控制当前的页数:

import { useTodolistStoreContext } from '@/hooks/todolist'

import TodoListItem from './TodoListItem'

const TodoListContent = memo((props) => {
  // 取出items待用
  let [items] = useTodolistStoreContext('items')
  // 构建page状态代表当前页数,默认值为1
  let [page, setPage] = useState(1)
  // 每页8条数据
  let pageSize = 8
	
  // 根据page、pageSize及items来渲染TodoListItem组件的方法
  function renderItems () {
    // 没有数据显示为空
    if (!items.length) return <Empty />
    // 准备渲染的分页后的items
    let renderItems = items.slice(
      (page - 1) * pageSize,
      page * pageSize
    )
    return renderItems.map((item) => (
      <Col className="todolist__item-col" key={item.id} span={24 / (pageSize / 2)}>
        <TodoListItem editAction={props.editAction} info={item}/>
      </Col> 
    ))
  }

  return (
    <div className="todolist-content">
      <Row gutter={16} className="todolist-content__container">
        <Suspense fallback={<Spin/>}>{ renderItems() }</Suspense>
      </Row>
      <Pagination
        total={items.length}
        showTotal={(total, range) => `${range[0]}-${range[1]} of ${total} items`}
        pageSize={pageSize}
        current={page}
        onChange={(page) => setPage(page)}
      />
    </div>
  )
})

export default TodoListContent

TodoListItem组件更多的是数据的展示,它需要接受到对于item的信息,得到items,还需要接受到父级组件传入的编辑item的动作,以及actions中完成、删除item的方法:

import { useTodolistStoreContext } from '@/hooks/todolist'
const TodoListItem = memo((props) => {
  const [state, actions] = useTodolistStoreContext()
	// item的信息
  let { title, description, finished, id } = props.info
  // 根据是否完成来确定主题颜色
  let color = finished ? '#1890ff' : '#ccc'
	// 完成此todo
  function finishAction () {
    actions.finishTodo({ id })
  }
  // 删除此todo
  function deleteAction () {
    actions.deleteTodo({ id })
  }
  // 编辑此todo
  function editAction () {
    props.editAction(props.info)
  }
  
  return (
    <Card
      className={`todolist__item ${ finished ? 'finished' : '' }`}
      title={ title }
      extra={<Icon type="check-circle" style={{ fontSize: '24px', color }}/>}
      actions={[
        <Icon onClick={finishAction} type="check" key="check" />,
        <Icon onClick={editAction} type="edit" key="edit" theme="filled" />,
        <Icon onClick={deleteAction} type="delete" key="delete" theme="filled" />,
      ]}
    >
      <Typography.Paragraph ellipsis={{ rows: 5 }}>{ description }</Typography.Paragraph>
    </Card>
  )

})

export default TodoListItem

还有一个TodoListDes.js组件,这个组件只需要展示一些计数信息即可:


import { useTodolistStoreContext } from '@/hooks/todolist'

const TodoListDes = memo(() => {
  let [detail] = useTodolistStoreContext('todoDetail')
  return (
    <Typography.Paragraph>
      { detail.description }
    </Typography.Paragraph>
  )
})

export default TodoListDes

Ok,接下来我们来看一下TodoListCAE.js的实现

import React, { useEffect, useState, useRef } from 'react'
import { Button, Drawer, Form, Input } from 'antd';
import { useTodolistStoreContext } from '@/hooks/todolist'

const { TextArea } = Input

// 这是我们的表单组件,专门用来新建和编辑item
const TodoListFormUI = (props) => {
  const [state, actions] = useTodolistStoreContext()
  const { toggleVisible, form } = props
  const { getFieldDecorator } = form
	
  
  // 取消的动作
  function handleCancel () {
    toggleVisible()
    form.resetFields()
  }
  
  // 保存的动作
  function handleOk () {
    // 保存的时候进行数据验证
    form.validateFields((err, values) => {
      if (!err) {
        // 数据验证通过后,如果editItem不存在说明是新建模式
        if (!props.editItem) {
          // 调用actions中的新建方法
          actions.createTodoItem({ item: values })
        } else {
          // 如果是编辑的话,调用actions中的编辑动作
          actions.updateTodoItem({ item: { ...values, id: props.editItem.id } })
        }
        // 完成后隐藏抽屉
        toggleVisible()
        // 重置表单
        form.resetFields()
      }
    })
  }
	
  // 监听editItem的变化
  useEffect(() => {
    // 如果editItem存在,说明是要编辑了
    if (props.editItem) {
      // 取出要编辑的item的信息,同步到form的input value值
      let { title, description } = props.editItem
      form.setFieldsValue({ title, description })
    } else {
      // 新建模式的时候重置表单
      form.resetFields()
    }
  }, [props.editItem])

  return (
    <Form layout="vertical">
      <Form.Item label="Title">
        {getFieldDecorator('title', {
          rules: [{ required: true, message: '事项标题不能为空!' }],
        })(<Input />)}
      </Form.Item>
      <Form.Item label="Description">
        {getFieldDecorator('description', {
          rules: [{ required: true, message: '事项内容不能为空!' }],
        })(<TextArea autosize={{ minRows: 15 }} type="textarea" />)}
      </Form.Item>
      <Form.Item >
        <Button onClick={handleOk} type="primary" >
          保存
        </Button>
        <Button type="default" onClick={handleCancel}>
          取消
        </Button>
      </Form.Item>
    </Form>
  )
}

const TodoListForm = Form.create({ name: 'todolist-form' })(TodoListFormUI)

// 新建和编辑Item的组件
const TodoListCAE = (props) => {
  // 接受到的是否隐藏的状态和切换隐藏状态的动作
  const { visible, toggleVisible } = props
  // 抽屉上方展示的title状态
  let [title, setTitle] = useState('')
  // 准备标记下面的TodoListForm,标记后放到form中去
  const form = useRef(null)
	
  // 根据editItem的变化确定是编辑模式还是新建模式,然后更新title
  useEffect(() => {
    setTitle(props.editItem ? '编辑待办事项' : '新增一条待办事项')
    // 根据编辑模式还是新建模式来设置表单的值或者重置表单
    // if (form.current) {
    //   if (props.editItem) {
    //     let { title, description } = props.editItem
    //     form.current.setFieldsValue({ title, description })
    //   } else {
    //     form.current.resetFields()
    //   }
    // }  
  })

  return (
    <Drawer
      width="500"
      title={title}
      placement="right"
      closable={false}
      onClose={toggleVisible}
      visible={visible}
    >
      <TodoListForm editItem={props.editItem}  ref={form} toggleVisible = {toggleVisible}/>
    </Drawer>
  )
  
}



export default TodoListCAE

useRef

大家可以看到在TodoListCAE中有useRef钩子的使用,这个专门用来做ref标记的,在类组件中我们只需要将标记的元素或者子组件放入到this上,例如:

return (
	<div>
  	<input ref = {inp => this.inp = inp} />
		<Child ref = {child => this.child = child}/>
  </div>
)

然后就可以在任意的钩子函数中利用this.inp和this.child来获取到标记的input-dom元素和Child子组件了,但是在函数组件中没有this怎么挂载呢?没错,就是用useRef来生成一个标记数据,然后将要挂载的子组件或Dom元素挂载在这个标记数据上就可以了。

上面在TodoListCAE中我们可以利用useRef构建一个form标记,然后用form来标记表单子组件,然后就可以在子组件中调用form.XXX来调用到子组件的一些API方法了。

注释掉是因为没有这么做的必要(虽然可以这么干),我们在Form子组件中自己监听数据变化来调用重置和设置值的api方法就可以了。

后语

到这里位置,我们的Todolist实例也就编写完成了,我们一共研究了useState/useEffect/useReducer/useRef/Custom Hook的实现方式,小伙伴们也要加油哟,其实这些东西都在官方文档上,要好好看文档哟。

后续还有其他有关于React Hook使用方面的干活我会慢慢放上来的,再见啦。