react 使用Hook的一些笔记
Hook特点
1.无需修改组件结构的情况下复用状态逻辑
2.可将组件中相互关联的部分拆分成更小的函数,复杂组件将变得更容易理解
3.每一个组件内的函数(包括事件处理函数,effects,定时器或者api调用等等)会捕获某次渲染中定义的props和state
4.memo缓存组件 ,useMemo缓存值, useCallback缓存函数
5.每次render都有自己的props与state。每次render都有自己的effects。(每一个组件内的函数,包括事件处理函数,effects,定时器或者api调用等等,会捕获某次渲染中定义的props和state)
6.当我们更新状态的时候(如setCount(count + 1))React会重新渲染组件,每一次渲染都能拿到独立的count状态,这个状态值是函数中的一个常量
useEffect
函数式组件中不存在传统类组件生命周期的概念
会捕获props和state,所以即便在回调函数里,你拿到的还是初始的props和state。如果你想得到最新的值,可以使用ref
count累加的例子(并不是count 的值在 不变 的effect中发生了变化,而是effect函数本身在每一次渲染中都是不相同,React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕厚去调用它)
effect是如何读取到最新的count?
我们已经知道count是某个特定渲染中的常量。事件处理函数看到的是属于它那次特定渲染中的count状态值。对于effects也同样如此
React会记住你提供的effect函数,并且会在每次更改作用于DOM,并让浏览器绘制屏幕后去调用它
useMemo
跟vue中的computed计算属性很类似
主要用来解决使用React hooks产生的无用渲染的性能问题,使用function的形式来声明组件,失去了shouldCompnentUpdate在组件更新之前,这个声明周期,也就是我们没有办法
通过组件更新前条件来决定组件是否更新,而且在函数组件中,也补在区分mount和update两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗
React.memo
把组件传递给memo之后,就会返回一个新的组件,新组件有了一个功能如果属性不变,则不重新渲染
<div className="App"> <h1>{ title }</h1> <button onClick={() => setTitle("title 已经改变")}>改名字</button> <Child name="桃桃" /> </div>
父组件重新渲染了,并且子组件也重新渲染了。你可能会想,传递给 Child 组件的 props 没有变,要是 Child 组件不重新渲染就好了,为什么会这么想呢?
我们假设 Child 组件是一个非常大的组件,渲染一次会消耗很多的性能,那么我们就应该尽量减少这个组件的渲染,否则就容易产生性能问题,所以子组件如果在 props 没有变化的情况下,就算父组件重新渲染了,子组件也不应该渲染。
那么我们怎么才能做到在 props 没有变化的时候,子组件不渲染呢?
答案就是用 React.memo
在给定相同 props 的情况下渲染相同的结果,并且通过记忆组件渲染结果的方式来提高组件的性能表现。
export default React.memo(Child)
通过 React.memo
包裹的组件在 props 不变的情况下,这个被包裹的组件是不会重新渲染的,也就是说上面那个例子,在我点击改名字之后,仅仅是 title 会变,但是 Child 组件不会重新渲染(表现出来的效果就是 Child 里面的 log 不会在控制台打印出来),会直接复用最近一次渲染的结果。
这个效果基本跟类组件里面的 PureComponent
效果极其类似,只是前者用于函数组件,后者用于类组件。
function App() { const [title, setTitle] = useState("这是一个 title"); const [subtitle, setSubtitle] = useState("我是一个副标题"); const callback = () => { setTitle("标题改变了"); }; return ( <div className="App"> <h1>{title}</h1> <h2>{subtitle}</h2> <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button> <Child onClick={callback} name="桃桃" /> </div> ); }
在函数式组件每次重新渲染,函数组件都会会重头开始重新执行,那么这两次创建的callback函数肯定发生了改变,所以导致了子组件重新渲染
React.memo和useCallBack都是为了减少重新render的次数。对于如何减少计算的量,就是userMemo来做
useState
当父组件set的时候,父组件里面有包含子组件,子组件会被重新渲染
解决:用memo包含子组件。(PureComponent也可以用来是否重新渲染,走的是shouldComponentUpdate,但是适用于 类组件,函数式组件用memo)
memo
1.针对的是一个组件的渲染是否重用,来优化函数组件重渲染行为,
当传入属性值都不变的情况下,就不会触发组件的重新渲染(父组件中传递给子组件useState的值变化时,子组件必会渲染)
2.当父组件给子组件传递一个函数时(<Foo cb={()=> {}} />),这时候父组件useState时,Foo都会重新渲染
(React.memo类似于React.PureComponent能对props做浅比较,防止无效的重复渲染)
(useCallback用户缓存函数,繁殖因属性更新时生成新的函数导致子组件重复渲染)
useMemo
1.一个函数是否重复执行,可以当useMemo当做性能优化手段
2.useMemo需要返回值,返回值直接参与渲染,因此useMemo是在渲染期间完成的
useReducer
局部状态不推荐使用useReducer,会导致函数内部状态过于复杂难以阅读
userReducer建议和useContext一起使用
import React from 'react' const countReducer = (state, newState) => newState(state) function Counter({step = 1, initialCount = 10}) { const [count, setCount] = React.useReducer(countReducer, initialCount) const increment = () => setCount(c => c + step) return <button onClick={increment}>{count}</button> } function Usage() { return <Counter /> }
import React, {useReducer} from 'react' const countReducer = (state, newState) => { console.log('state, newState------', typeof state, typeof newState) return state + newState } function Counter({step = 4, initialCount = 10}) { const [count, setCount] = useReducer(countReducer, initialCount) const increment = () => { setCount(step) } return <div> <button onClick={increment}>按钮{count}</button> </div> } function Usage() { return <Counter /> }
import React from 'react' const countReducer = (state, action) => ({ ...state, ...(typeof action === 'function' ? action(state) : action), }) function Counter({initialCount = 0, step = 1}) { const [state, setState] = React.useReducer(countReducer, { count: initialCount, }) const {count} = state const increment = () => setState(currentState => ({count: currentState.count + 1})) return <button onClick={increment}>{count}</button> } function Usage() { return <Counter /> }
import React from 'react' function countReducer(state, action) { switch (action.type) { case 'INCREMENT': { return {count: state.count + 1} } default: { throw new Error(`Unsupported action type: ${action.type}`) } } } function Counter({initialCount = 0, step = 1}) { const [state, dispatch] = React.useReducer(countReducer, { count: initialCount, }) const {count} = state const increment = () => dispatch({type: 'INCREMENT'}) return <button onClick={increment}>{count}</button> } function Usage() { return <Counter /> }
useCallback
所有Function Component内函数必须使用React.useCallback包裹,以保证准确性与性能
useContext
在上下文中缓存响应数据 (useContext: Caching response data in context)
const PokemonCacheContext = React.createContext() function pokemonCacheReducer(state, action) { switch (action.type) { case 'ADD_POKEMON': { return {...state, [action.pokemonName]: action.pokemonData} } default: { throw new Error(`Unhandled action type: ${action.type}`) } } } function PokemonCacheProvider(props) { const value = React.useReducer(pokemonCacheReducer, {}) console.log('value------', value) return <PokemonCacheContext.Provider value={value} {...props} /> }
在父组件下两个子组件,当中一个子组件修改了父组件的值,对应组件接收到值的变化
import React from 'react' const CountContext = React.createContext() function CountProvider(props) { const [count, setCount] = React.useState(0) const value = [count, setCount] // could also do it like this: // const value = React.useState(0) return <CountContext.Provider value={value} {...props} /> } function CountDisplay() { const [count] = React.useContext(CountContext) return <div>{`The current count is ${count}`}</div> } function Counter() { const [, setCount] = React.useContext(CountContext) const increment = () => setCount(c => c + 10) return <button onClick={increment}>Increment count</button> } function Usage() { return ( <div> <CountProvider> <CountDisplay /> <Counter /> </CountProvider> </div> ) } Usage.title = 'useContext: simple Counter' export default Usage
useImperativeHandle
回到top
import React, {useImperativeHandle, forwardRef} from 'react' // 🐨 wrap this in a React.forwardRef and accept `ref` as the second argument const MessagesDisplay = forwardRef(function MessagesDisplay({messages}, ref) { const containerRef = React.useRef() React.useLayoutEffect(() => { scrollToBottom() }) useImperativeHandle(ref, () => ({ scrollToTop, scrollToBottom, })) // 💰 you're gonna want this as part of your imperative methods function scrollToTop() { containerRef.current.scrollTop = 0 } function scrollToBottom() { containerRef.current.scrollTop = containerRef.current.scrollHeight } // 🐨 call useImperativeHandle here with your ref and a callback function // that returns an object with scrollToTop and scrollToBottom return ( <div ref={containerRef} role="log" style={{ height: 300, overflowY: 'scroll', width: 300, outline: '1px solid black', paddingLeft: 10, paddingRight: 10, }} > {messages.map(message => ( <div key={message.id}> <strong>{message.author}</strong>: <span>{message.content}</span> <hr /> </div> ))} </div> ) })
注意点:
1.useEffect
useEffect(async () => )
react首次渲染和之后的每次渲染都会调用一遍useEffect函数,而之前我们要用两个生命周期函数分别表示首次渲染(componentDisMount)和更新导致的重新渲染(componentDidUpdte)
useEffect中定义的函数执行不会阻塞浏览器更新视图,也急速说这些函数是异步执行的,而componentDidMount和componentDidUpdate中的代码都是同步执行
function Counter() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
上面例子中,count仅是一个数字而已,它不是神奇的 databinding,watcher,proxy或者其他东西,它就是一个普通的数字 const count = 0
我们的组件第一次渲染的时候,从useState()拿到count的初始值0,当我们调用setCount(1),React会再次渲染组件,这次count是1,如此等等
count
,将它的初始值设为0,然后我们告诉react,我们的这个组件有一个副作用。给useEffecthook
传了一个匿名函数,这个匿名函数就是我们的副作用。在这里我们打印了一句话,当然你也可以手动的去修改一个DOM
元素。当React要渲染组件时,它会记住用到的副作用,然后执行一次。等Reat更新了State状态时,它再一次执行定义的副作用函数。
// During first render function Counter() { // ... useEffect( // Effect function from first render () => { document.title = `You clicked ${0} times`; } ); // ... } // After a click, our function is called again function Counter() { // ... useEffect( // Effect function from second render () => { document.title = `You clicked ${1} times`; } ); // ... } // After another click, our function is called again function Counter() { // ... useEffect( // Effect function from third render () => { document.title = `You clicked ${2} times`; } ); // .. }
React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它。不过在class中的this state并不是这样运行的,
所以虽然我们说的是一个 effect(这里指更新document的title),但其实每次渲染都是一个不同的函数 — 并且每个effect函数“看到”的props和state都来自于它属于的那次特定渲染。
计数器版本模拟class中的行为
function App() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
每秒递增的计数器,可能会有如下代码
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>{count}</h1>; }
在第一次渲染中,count
是0
。因此,setCount(count + 1)
在第一次渲染中等价于setCount(0 + 1)
。
既然我们设置了[]
依赖,effect不会再重新运行,它后面每一秒都会调用setCount(0 + 1) (依赖没有变,所以不会再次运行effect。)
2.mapDispatchToProps里不要使用第二个参数
3.为了避免子组件发生不必要的re-render,一些表单提交handleSubmit其实也应该用useCallback包裹,
useCallback会返回被它包裹的函数memeorized版本,只要依赖项不变,memorize的函数就不会更新
const handleSubmit = useCallback(fieldValues => { // 在组件内使用dispatch dispatch(submitFormData(fieldValues)) .then(res => { history.push('/home'); }); })