React hooks 概要
|字数总计:2.8k|阅读时长:11分钟|阅读量:|
React 基础知识回顾
React(响应)的设计理念是,当数据发生变化时,UI能自动把变化反映出来。它的诞生颠覆了传统的web UI开发模式,它把UI的开发从复杂的DOM操作中解脱出来,让开发者专注于数据、逻辑和UI组件本身。
组件 (component)
React 是通过组件的方式来组织和描述UI的。组件可以分为两种类型:
- 内置组件。内置组件其实就是映射到 HTML 节点的组件,例如 div、input、table 等等,作为一种约定,它们都是小写字母。
- 自定义组件。自定义组件其实就是自己创建的组件,使用时必须以大写字母开头,例如 TopicList、TopicDetail。
1 2 3 4 5 6 7 8 9
| function CommentBox() { return ( <div> <CommentHeader /> <CommentList /> <CommentForm /> </div> ); }
|
状态 (state)和属性(props)
组件的状态用于维护组件用到的数据,而属性则用于父组件向子组件传递数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| import React from 'react'; import { Checkbox, Typography, List, ListItem, ListItemText, CircularProgress, } from '@material-ui/core';
class ToDoList extends React.Component { constructor(props) { super(props); this.state = { loading: true, items: [], }; this.handleCheck = this.handleCheckEvent.bind(this); }
componentDidMount() { this.getToDoList(); let nr = 0; this.state.items.forEach((item) => { if (item.done === false) { nr += 1; } }); document.title = `${nr} itmes left`; }
componentDidUpdate() { let nr = 0; this.state.items.forEach((item) => { if (item.done === false) { nr += 1; } }); document.title = `${nr} itmes left`; }
handleCheckEvent(id) { const tmpState = this.state; tmpState.items.forEach((task, index, array) => { if (task.id === id) { array[index].done = !array[index].done; } }); this.setState(tmpState); }
getToDoList() { setTimeout(() => { this.setState({ loading: false, items: [ { id: 0, text: 'Learn JavaScript', done: false }, { id: 1, text: 'Learn React', done: false }, { id: 2, text: 'Play around in JSFiddle', done: true }, { id: 3, text: 'Build something awesome', done: true }, ], }); }, 2000); }
render() { return ( <div className="todo-list-container"> <Typography variant="h6"> Todos </Typography> {this.state.loading ? <div className="loading-container"><CircularProgress /></div> : ( <List dense className="todo-list"> {this.state.items.map((item) => ( <ListItem key={item.id} className="todo-item"> <Checkbox edge="start" checked={item.done} tabIndex={-1} disableRipple inputProps={{ 'aria-labelledby': item.id }} onChange={() => { this.handleCheck(item.id); }} /> <ListItemText className={item.done ? 'done' : ''} id={item.id} primary={item.text} /> </ListItem> ))} </List> )} </div> ); } }
export default ToDoList;
|
JSX
JSX 并不是一个新的模板语言,而可以认为是一个语法糖。
1 2 3 4 5 6 7 8 9 10 11 12
| React.createElement( "div", null, React.createElement( "Typography", ... ), React.createElement( "List", ... ); );
|
React.createElement API的作用就是创建一个组件的实例。此外,这个 API 会接收一组参数:第一个参数表示组件的类型;第二个参数是传给组件的属性,也就是 props;第三个以及后续所有的参数则是子组件。
React 引入Hooks的原因
React 组件的模型其实很直观,就是从 Model 到 View 的映射,这里的 Model 对应到 React 中就是 state 和 props。
虽然之前的react也支持函数作为组件,但因为函数组件只能是纯函数,没法使用state,所以更多的情形是用class来实现UI组件。但我们可以从上图可以看到,state/props 到view的本质就是一种函数关系。react用到的class并没有真正使用到面向对象的优势,比如说子组件和父组件并不是一种继承关系,组件之间也不会调用对方的方法。
于是Hooks被引入到react中,Hooks能够把一个外部的数据绑定到函数的执行。当数据变化时,函数能够自动重新执行。这样的话,任何会影响 UI 展现的外部数据,都可以通过这个机制绑定到 React 的函数组件。
前面我们说了,react 引入hooks的原因是其本质是函数映射,那么把react组件函数化最大的优势是什么?答案就是数据和逻辑复用。class组件之间是没法共享state的,父组件的state只能通过子组件的props传递给子组件。没有父子关系的组件之间要共享数据只能通过高阶组件。
React常用的Hook
useState
useState可以让函数组件具有维护状态的能力。参考前面Counter的例子,const [count, setCount] = React.useState(0);
定义了名为count的状态,使得函数组件Counter的多次渲染可以共享它。0是状态的默认值,而setCount则是用来改变state的函数,调用setCount会让react刷新组件。useState 这个 Hook 的用法总结出来就是这样的:
- useState(initialState) 的参数 initialState 是创建 state 的初始值,它可以是任意类型,比如数字、对象、数组等等。
- useState() 的返回值是一个有着两个元素的数组。第一个数组元素用来读取 state 的值,第二个则是用来设置这个 state 的值。
- 如果要创建多个 state,那么我们就需要多次调用 useState。例如
1 2 3 4 5 6
| // 定义一个年龄的 state,初始值是 42 const [age, setAge] = useState(42); // 定义一个水果的 state,初始值是 banana const [fruit, setFruit] = useState('banana'); // 定一个一个数组 state,初始值是包含一个 todo 的数组 const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
|
useEffect
副作用(Side Effect)指的是与UI渲染没有直接关系的操作,例如从服务器端获取数据等。React使用useEffect来替代class中的生命周期函数。useEffect接受两个参数,一个是callback函数,另外一个是执行callback函数的条件。
1
| useEffect(callback, dependencies)
|
- 当只传递callback,没有dependencies时,callback会在组件每次渲染的时候执行一次。
- 当dependencies为空数组
[]
时,callback会在组件第一次渲染的时候执行,相当于componentDidMount
- 当callback返回一个函数时,这个函数会在组件卸载的时候执行一次,相当于componentWillUnmount
React hooks的使用规则:
- 在useEffect回调函数中使用的变量,都必须在依赖项中声明
- Hooks不能出现在条件语句和循环中,也不能出现在return之后
- Hooks只能在函数组件或者自定义Hook中使用
使用eslint可以检查这些规则:
- 安装eslint插件:
npm install --save-dev eslint-plugin-react-hooks
- 在eslint配置文件中添加规则:
react-hooks/rules-of-hooks
以及react-hooks/exhaustive-deps
useCallback
每次state的变化都会导致组件函数重新执行一遍,事件处理函数就会被定义多遍,而且事件处理函数通常是闭包,不会被垃圾回收清理掉。事件处理函数会作为props传递给组件,重新定义事件处理函数也会导致组件的频繁更新。为了提升性能,useCallback被引入到React Hooks之中。useCallback的定义如下:
fn是定义的函数,deps是依赖变量的数组。只有deps中的某个变量发生变化时,fn才会被重新声明。
useMemo
类似的,由于每次渲染都会重新执行组件函数,那些耗时的计算也会重复进行。useMemo则用于避免重复的耗时计算。
1
| const result = useMemo(fn, [deps])
|
同样,只有deps中的变量发生变化时,result才会用fn重新计算。
useRef
useRef可以使函数组件的多次渲染之间共享数据。它相当于在函数组件之外创建了一个存储对象,其current属性值可以在多次渲染之间共享。
1
| const container = useRef(initialValue)
|
useContext
React组件之间传递数据的方式通常是父子组件之间使用props来进行。React context API可以使得各个组件可以共享上下文数据。主要用于language, theme 等上下文的共享。大规模的数据共享还是应该使用redux这类的状态管理框架来进行。
要使用context数据,首先需要在顶层组件中定义context.
1 2 3 4 5
| const ThemeContext = React.createContext('light');
<ThemeContext.Provider value='light'> <App /> </ThemeContest.Provider>
|
然后子组件中就可以使用useContext来获取context的值了。
1 2 3
| const theme = useContext(ThemeContext);
<Button className={theme}>Submit</Button>
|
下面的例子展示了如何使用React Hooks将上面的ToDoList class组件改造为函数组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| const ToDoItems = () => { const [loading, setLoading] = useState(true); const [todos, setTodos] = useState([]); const theme = useContext(ThemeContext); const nrTasksLeft = useMemo(() => { let nr = 0; todos.forEach((item) => { if (item.done === false) { nr += 1; } }); return nr; }, [todos]);
useEffect(() => { document.title = `${nrTasksLeft} itmes left`; });
useEffect(() => { const getToDoList = () => { setTimeout(() => { setLoading(false); setTodos([ { id: 0, text: 'Learn JavaScript', done: false }, { id: 1, text: 'Learn React', done: false }, { id: 2, text: 'Play around in JSFiddle', done: true }, { id: 3, text: 'Build something awesome', done: true }, ]); }, 2000); }; getToDoList(); }, []);
const handleCheck = useCallback((id) => { const tmpTodos = [...todos]; tmpTodos.forEach((todo, index) => { if (todo.id === id) { tmpTodos[index].done = !tmpTodos[index].done; } }); setTodos(tmpTodos); }, [todos]);
return ( <div className="todo-list-container"> <Typography variant="h6"> Todos </Typography> {loading ? <div className="loading-container"><CircularProgress /></div> : ( <List dense className="todo-list"> {todos.map((item) => ( <ListItem key={item.id} className={`todo-item ${theme}`}> <Checkbox className={theme} edge="start" checked={item.done} tabIndex={-1} disableRipple inputProps={{ 'aria-labelledby': item.id }} onChange={() => { handleCheck(item.id); }} /> <ListItemText className={item.done ? `${theme} done` : `${theme}`} id={item.id} primary={item.text} color={theme === 'light' ? 'black' : 'white'} /> </ListItem> ))} </List> )} </div> ); };
|
自定义Hooks
除了上述react内置的hooks之外,用户可以根据自己的需求利用上述hooks来创建自定义hooks。自定义hooks主要有如下4种使用情况:
- 抽取业务逻辑,让组件之间能代码复用。
- 封装通用逻辑,让类似的逻辑只实现一遍。
- 监听浏览器状态,让组件能够使用浏览器的状态数据。
- 拆分复杂组件,让组件的逻辑更加清晰明了。
例如,定义如下的自定义hook可以让组件更简单的使用浏览器宽度。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { useState, useEffect } from 'react';
export const useWindowSize = () => { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleWindowSize = () => { setWidth(window.innerWidth); }; window.addEventListener('resize', handleWindowSize); return () => { window.removeEventListener('resize', handleWindowSize); }; }); return width; };
|
组件使用自定义hook时只需要引入即可。
1 2 3 4 5
| import { useWindowSize } from '../hooks'; ... const width = useWindowSize(); ... return <div>window size: {width}</div>
|
小结
Hook |
用途 |
const [width, setWidth] = useState(window.innerWidth) |
定义组件的状态 |
useEffect(fn, [deps]) |
替代class组件中的声明周期函数 |
useCallback(fn, [deps]) |
避免fn函数的重复定义和组件的重新渲染,只有当deps中的变量变化时才会重新定义 |
const result = useMemo(fn, [deps]) |
避免数据的重复计算 |
const container = useRef(initialValue) |
在函数组件的多次渲染之间共享数据 |
useContext |
用于组件使用页面的上下文 |
自定义Hook |
逻辑复用,监听浏览器状态, 拆分复杂组件 |