业务逻辑与其他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能顶三个,能够用吗?真的够用了,不信一起来看:
- 组件还未渲染前的动作?直接在return 组件结构上面执行就可以 (componentWillMount)
- 组件初次渲染后的动作?这个时候useEffect()传入的函数会执行哟 (componentDidMount)
- 组件props或者state变化后的动作?这个时候useEffect()传入的函数也会执行哟 (componentDidUpdate)
- 组件销毁前的动作?这个时候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使用方面的干活我会慢慢放上来的,再见啦。
作者:半盏屠苏
链接:https://juejin.im/post/5d78b086f265da03c8154d4b
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com