Hook出现原因
在实际项目中,充斥着各种生命周期代码,React的生命周期把我们业务逻辑撕裂成各自分割的一部分。而Hook的出现,让我们从面向生命周期编程变成了面向业务逻辑编程。让我们可以不再需要关心React的生命周期,而把注意力放在我们需要关注的业务逻辑上。
// Class Component
import React from 'react'
export default class App extends React.Component {
state = {
msg: ''
}
componentWillMount () {
this.setState({ msg: 'hello, world' })
}
render () {
const { msg } = this.state
return (
<div>{msg}</div>
)
}
}
// Function Component
import React, { useState } from 'react'
function App () {
const [ msg, setMsg ] = useState('')
return (
<div>{msg}</div>
)
}
export defaut App
React提供的常用Hook
useState(状态管理)
在函数组件里调用它来给组件添加一些内部 state。通过传入一个初始值,返回一对值:当前状态和一个让你更新它的函数,你可以在任何需要更新 state 的地方调用这个函数。和 class component 不同的是,这个初始值不一定是个对象。
import React, { useState } from 'react'
function Example() {
// 声明一个叫 “count” 的 state 变量。
const [ count, setCount ] = useState(0)
// 入参传入方法
const [ count2, setCount2 ] = useState(() => 0)
// 入参传入字符串
const [ fruit, setFruit ] = useState('banana')
// 入参传入对象
const [ user, setUser ] = useState({ name: '张三', age: 12 })
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
等同于
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
count2: 0,
fruit: 'banana',
user: { name: '张三', age: 12 }
}
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
)
}
}
useEffect(副作用管理)
给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。在开发过程中,请将所有副作用的代码放到useEffect中统一管理。useEffect在浏览器渲染完成后执行。
import React, { useState, useEffect } from 'react'
function Example() {
const [count, setCount] = useState(0)
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
注意,useEffect尽管可以实现 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 的功能,但是useEffect本质上和他们没有任何关系,使用的时候请不要对号入座:
useEffect实现componentDidMount
useEffect(() => {
// todo
console.log('实现了componentDidMount')
}, [])
componentDidMount () {
// todo
}
useEffect实现componentDidUpdate
useEffect(() => {
// todo
console.log('实现了componentDidUpdate')
})
componentDidUpdate () {
// todo
}
useEffect实现componentWillUnmount
useEffect(() => {
return () => {
// todo
console.log('实现了componentWillUnmount')
}
}, [])
componentDidUpdate () {
// todo
}
所以,如果你在页面中使用的定时器,想在组件被移除的时候,关闭定时器,你可以这么做
useEffect(() => {
const timer = setInterval(() => {
console.log('我是小溪流,永远向前流,小啊小啊小溪流,永远不停留')
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
如果你组件中有很多state,但是你在处理副作用逻辑中,只依赖其中一个或几个 state,你可以这么做:
import React, { useState, useEffect } from 'react'
function Example() {
const [ count, setCount ] = useState(0)
const [ fruit, setFruit ] = useState('apple')
const [ user, setUser ] = useState({ name: '张三', age: 10 })
useEffect(() => {
console.log(`${user.name}爱吃${fruit}`)
}, [ user, fruit ]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useCallback
把内联回调函数及依赖项数组作为参数传入useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
import React, { useState, useCallback } from 'react'
function Example() {
const [count, setCount] = useState(0);
const [msg, setMsg] = useState('yes');
// 方法一
const sayHello = useCallback(() => {
setCount(count + 1)
}, [ count ]);
return (
<div>
<p>{msg}</p>
<button onClick={() => setMsg(msg === 'yes' ? 'no' : 'yes')}>say yes</button>
<p>{count}</p>
<Info say={sayHello} />
</div>
);
}
const Info = (props) => {
return (
<button onClick={props.say}>say hello</button>
)
}
有同学可能就奇怪了,如果把sayHello写成如下形式,不是也可以正常运行吗?
// 方法二
const sayHello = () => {
setCount(count + 1)
}
从运行效果上看,确实都可以运行,而且都没有报错。但是你可以在Info组件内部添加props.say方法的监听会发现:
const Info = (props) => {
useEffect(() => {
console.log('this is effect')
}, [ props.say ])
return (
<button onClick={props.say}>say hello</button>
)
}
当你点击 say yes时,你会发现方法二的写法,会在控制台一直打印this is effect,但是方法一并没有打印。因此,在我们开发过程中,对于会传给子组件的方法可以适当考虑用useCallback包裹一下。上面的例子还有没有可优化的空间呢?答案是:yes!
import React, { useState, useCallback } from 'react'
function Example() {
const [count, setCount] = useState(0);
const [msg, setMsg] = useState('yes');
const sayHello = useCallback(() => {
setCount(count + 1)
}, [ count ]);
return (
<div>
<p>{msg}</p>
<button onClick={() => setMsg(msg === 'yes' ? 'no' : 'yes')}>say yes</button>
<p>{count}</p>
<Info say={sayHello} />
</div>
);
}
const Info = React.memo((props) => {
useEffect(() => {
console.log('this is effect')
})
return (
<button onClick={props.say}>say hello</button>
)
})
这个时候会发现点击 say yes,Info一点副作用都没有了。perfect!
引申一下:如果我想点击 Info组件内部的 say hello方法,useCallback依然缓存回调函数,但是需要 count能取到最新的值,那该怎么办呢?答案是:useRef + useLayoutEffect!
import React, { useState, useRef, useLayoutEffect, useEffect, useCallback } from 'react'
function Example() {
const [count, setCount] = useState(0);
const [msg, setMsg] = useState('yes');
const ref = useRef()
// 当值发生变化时,将最新的值赋值给ref
useLayoutEffect(() => {
ref.current = count
}, [ count ])
// ref没有发生变化,则一直返回之前缓存的回调,current值变化,不会触发ref变化
const sayHello = useCallback(() => {
setCount(ref.current + 1)
}, [ ref ]);
return (
<div>
<p>{msg}</p>
<button onClick={() => setMsg(msg === 'yes' ? 'no' : 'yes')}>say yes</button>
<p>{count}</p>
<Info say={sayHello} />
</div>
);
}
const Info = React.memo((props) => {
useEffect(() => {
console.log('this is effect')
})
return (
<button onClick={props.say}>say hello</button>
)
})
如果此时不理解useRef的用法和useLayoutEffect的用法,可以先放下。这个时候计算你点击 Info 组件的 say hello 按钮,你会发现Info中完全没有副作用,perfect!
useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
const refContainer = useRef(initialValue)
refContainer.current = newValue
import React, { useRef } from 'react'
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
export default TextInputWithFocusButton
useMemo
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。和Vue中的computed作用一致。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
useLayoutEffect
用法和useEffect一样,但是useLayoutEffect在浏览器渲染完成前执行。
function Example() {
useLayoutEffect(() => {
console.log("useLayoutEffect ...");
});
useEffect(() => {
console.log("useEffect ...");
});
return <div></div>;
}
上述的例子,会先打印 “useLayoutEffect ...” ,再打印 “useLayoutEffect ...”
useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
import React, { useContext } from 'react'
const theme = {
light: {
color: 'white',
bg: 'black'
},
dark: {
color: 'black',
bg: 'white'
}
}
// 创建context,并设置默认值
const ThemeContext = React.createContext(theme.light)
function Example () {
return (
<ThemeContext.Provider value={theme.light}>
<div><Info /></div>
</ThemeContext.Provider>
)
}
function Info () {
const ctx = useContext(ThemeContext)
return (
<div style={{ color: ctx.color, backgroundColor: ctx.bg }}>
主要内容
</div>
)
}
export default Example;