const [state, setState] = useState(initialState);
返回一个state,以及更新state的函数
在初始渲染期间,返回的状态(state
)与传入的参数initialState
的值相同。
setState
用于更新state,接收一个新的state值并将组件的一次重新渲染加入队列。
setState(newState)
import {useState} from 'react';
const Test = () =>{
const [count, setCount] = useState(0);
return (
<>
<h1>点击了{count}次</h1>
<button
onClick={
() => setCount(count + 1)
}>
+1
</button>
</>
);
}
export default Test;
在class组件中,this.setState
更新是state合并,useState
中setState
是替换。
例如:
// 错误示例
import { useState } from 'react';
const Test = () => {
const [counts, setCounts] = useState({
num1: 0,
num2: 0
});
return (
<>
<h1>num1:{counts.num1}</h1>
<h1>num2:{counts.num2}</h1>
<button onClick={() => setCounts({ num1: counts.num1 + 1})}>num1+1</button>
<button onClick={() => setCounts({ num2: counts.num2 + 1})}>num2+1</button>
</>
);
}
export default Test;
可以看到useState
中setState
是替换,不会合并,正确更新:
import { useState } from 'react';
const Test = () => {
const [counts, setCounts] = useState({
num1: 0,
num2: 0
});
return (
<>
<h1>num1:{counts.num1}</h1>
<h1>num2:{counts.num2}</h1>
<button onClick={() => setCounts({ ...counts, num1: counts.num1 + 1})}>num1+1</button>
<button onClick={() => setCounts({ ...counts, num2: counts.num2 + 1})}>num2+1</button>
</>
);
}
export default Test;
如果新的state
需要通过使用先前的state
计算得出,那么可以将函数传递给 setState
。
该函数将接收先前的 state
,并返回一个更新后的值。
下面的计数器组件示例展示了setState
的两种用法:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
function handleClickFn() {
setCount((prevCount) => {
return prevCount + 1;
})
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
handleClick
通过一个新的 state 值更新。handleClickFn
通过函数式更新返回新的 state。在同步更新时,两种方法没有区别。如果是异步更新则是有区别的。
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
setCount(count + 1)
}, 3000);
}
function handleClickFn() {
setTimeout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
当设置为异步更新时,点击按钮延迟到3s之后去调用setCount
函数。当进行快速点击时,即在3s内多次触发更新,但是只有一次是生效的,因为count
的值并没有发生变化。
当使用函数式更新 state 的时,则不会出现这种情况。因为它可以获取之前的 state
值,也就是代码中的 prevCount
每次都是最新的值。
这与类组件中的setState
类似,可以接收一个新的 state 值更新,也可以函数式更新。如果新的 state 需要根据之前的 state 计算得出,则需要使用函数式更新。
因为setState
更新可能是异步,当你在事件绑定中操作 state 的时候,setState
更新就是异步的。
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick = () => {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// 这样写只会加1
}
handleClickFn = () => {
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
}
render() {
return (
<>
Count: {this.state.count}
<button onClick={this.handleClick}>+</button>
<button onClick={this.handleClickFn}>+</button>
</>
);
}
}
当你在定时器中操作 state 的时候,而 setState
更新就是同步的。
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick = () => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// 这样写是正常的,两次setState最后是加2
}, 3000);
}
handleClickFn = () => {
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 }
})
}
render() {
return (
<>
Count: {this.state.count}
<button onClick={this.handleClick}>+</button>
<button onClick={this.handleClickFn}>+</button>
</>
);
}
}
注意这里的同步和异步指的是 setState
函数。因为涉及到 state 的状态合并,react 认为当你在事件绑定中操作 state 是非常频繁的,所以为了节约性能 react 会把多次 setState
进行合并为一次,最后在一次性的更新 state,而定时器里面操作 state 是不会把多次合并为一次更新的。
useEffect
可以在函数组件中执行副作用操作。
副作用:除了状态相关的逻辑,如网络请求、监听事件、查找dom
例子
需求:当组件状态更新时改变document.title
普通写法:
class App extends React.Component{
state = {
count: 0
}
componentDidMount() {
document.title = count;
}
componentDidUpdate() {
document.title = count;
}
render () {
const {count} = this.state;
return (
<div>
页面名称:{count}
<button onClick={() => { this.setState({ count: count++ })}}>click</button>
</div>
)
}
}
hook写法:
function App () {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = count;
});
return (
<div>
页面名称:{count}
<button onClick={() => { this.setState({ count: count++ })}}>click</button>
</div>
);
}
可以将useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
以往我们在绑定事件、解绑事件、设定定时器、查找 dom 等操作的时候都是通过这三个生命周期函数来实现的,而useEffect
会在组件每次render
之后调用,相当于这三个生命周期函数,只不过可以通过传参来决定是否调用。
useEffect
会返回一个回调函数,作用于清除上一次副作用遗留下来的状态,如果该useEffect
只调用一次,该回调函数相当于componentWillUnmount
函数。
例子
function App () {
const [ count, setCount ] = useState(0);
const [ width, setWidth ] = useState(document.body.clientWidth);
const onChange = () => {
setWidth(document.body.clientWidth);
};
useEffect(() => {
window.addEventListener('resize', onChange, false);
return () => {
window.removeEventListener('resize', onChange, false);
}
});
useEffect(() => {
document.title = count;
});
return (
<div>
页面名称: { count }
页面宽度: { width }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
);
}
在这个例子中,我们既要处理title
,还要监听屏幕宽度。
在 hook 中,我们需要使用两个useEffect
来解决问题。useEffect
可以返回一个函数,用来清除上一次副作用留下的状态,这个地方可以用来解绑事件监听。但是存在一个问题:useEffect
每次render
后就会调用,比如title
改变,相当于componentDidUpdate
,但我们的事件监听不应该在每次的render
之后进行一次绑定和解绑,也就是需要让useEffect
变成componentDidMount
,它的返回函数变成componentWillUnmount
,这里需要用的useEffect
的第二个参数。
useEffect
的第二个参数有三种情况:
render
之后 useEffect
都会调用,相当于 componentDidMount
和 componentDidUpdate
[]
。 只会调用一次,相当于 componentDidMount
和 componentWillUnmount
useEffect
才会执行function App () {
const [ count, setCount ] = useState(0);
const [ width, setWidth ] = useState(document.body.clientWidth);
const onChange = () => {
setWidth(document.body.clientWidth);
};
useEffect(() => {
// 相当于 componentDidMount
console.log('add resize event');
window.addEventListener('resize', onChange, false);
return () => {
// 相当于 componentWillUnmount
window.removeEventListener('resize', onChange, false);
}
}, []);
useEffect(() => {
// 相当于 componentDidUpdate
document.title = count;
});
useEffect(() => {
console.log(`count change: count is ${count}`);
}, [ count ]);
return (
<div>
页面名称: { count }
页面宽度: { width }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
);
}
根据上面的例子的运行结果:
useEffect
中的 'add resize event'
只会在第一次运行时输出一次,无论组件怎么 render
,都不会在输出;useEffect
会在每次组件render
之后都执行,title
每次点击都会改变; useEffect
, 只要有在第一次运行和 count
改变时,才会执行,屏幕发生改变引起的render
并不会影响第三个 useEffect
。在上面的操作中都不用清理的副作用,然而,有些副作用是需要去清理的,不清理会造成异常甚至内存泄漏,比如开启定时器,如果不清理,则会多次开启,从上面可以看到useEffect
的第一个参数是一个回调函数,可以在回调函数中再返回一个函数,该函数可以在状态更新后第一个回调函数执行之前调用,具体实现:
seEffect(() => {
// 设置副作用
return () => {
// 清理副作用
}
});
在Hooks之前,react开发者都是使用class组件进行开发,父子组件之间通过props
传值。但是现在开始使用方法组件开发,没有constructor
构造函数,也就没有了props
的接收,所以父子组件的传值就成了一个问题。
于是,就有了useContext
。
useContext
的参数必须是 context
对象本身:
useContext(MyContext)
useContext(MyContext.Consumer)
useContext(MyContext.Provider)
createContext
方法,得到Context
对象,并导出import { createContext } from 'react';
export const Context = createContext();
return(
<Context.Provider value={传递数据}>
<后代组件 />
</Provider>
)
import React, { useContext } from 'react';
import { Context } from './index';
const 函数组件 = () => {
const 公共数据 = useContext(Context); //value中的值
return (函数组件内容);
}
import React, { useContext, useState, createContext } from 'react';
import { Button } from 'antd';
const CountContext = createContext();
const App = () => {
const [count, setCount] = useState(0);
console.log(CountContext);
console.log(useContext(CountContext));
return (
<div>
<div>
<p>父组件点击次数:{count}</p>
<Button type={'primary'} onClick={() => setCount(count + 1)}>
点击+1
</Button>
<CountContext.Provider value={count}>
<Counter />
</CountContext.Provider>
</div>
</div>
);
};
export default App;
const Counter = () => {
const count = useContext(CountContext);
console.log(CountContext);
// console.log(count);
// console.log(useContext(CountContext));
return (
<div>
<p>子组件获得的点击数量:{count}</p>
</div>
);
};
// useCallback(回调函数,[依赖值])
const handleClick = useCallback(()=> {
// 做一些事
}, [value]);
useCallback返回的是一个 memoized(缓存)函数,在依赖不变的情况下,多次定义的时候,返回的值是相同的,他的实现原理是当使用一组参数初次调用函数时,会缓存参数和计算结果,当再次使用相同的参数调用该函数时,会直接返回相应的缓存结果。
例子:
import React, { useState, useEffect } from 'react';
let count = 0;
function App() {
const [val, setVal] = useState('');
function getData() {
setTimeout(() => {
setVal('new data' + count);
count++;
}, 500);
};
useEffect(()=>{
getData();
},[]);
return <div>{val}</div>;
};
export default App;
getData
模拟发起网络请求。在这种场景下,没有useCallback
什么事,组件本身是高内聚的。
如果涉及到组件通讯,情况就不一样了:
import React, { useState, useEffect } from 'react';
let count = 0;
function App() {
const [val, setVal] = useState('');
function getData() {
setTimeout(() => {
setVal('new data' + count);
count++;
}, 500);
}
return <Child val={val} getData={getData} />;
}
export default App;
function Child({ val, getData }) {
useEffect(() => {
getData();
}, [getData]);
return <div>{val}</div>;
}
执行过程:
val
和getData
useEffect
获取数据。getData
执行时,调用了setVal
,导致 App 重新渲染getData
,重新传入 ChildgetData
引用改变,Child 再次使用getData
如果明确getData
只执行一次,最简单的方式当然是将其从依赖列表中删除。但如果装了 hook 的lint 插件,会提示:React Hook useEffect has a missing dependency
useEffect(() => {
getData();
}, []);
实际情况很可能是当getData
改变的时候,是需要重新获取数据的。这时就需要通过useCallback
来将引用固定住:
import React, { useState, useEffect, useCallback } from 'react';
let count = 0;
function App() {
const [val, setVal] = useState('');
const getData = useCallback(() => {
setTimeout(() => {
setVal('new data' + count);
count++;
}, 500);
}, []);
return <Child val={val} getData={getData} />;
}
export default App;
function Child({ val, getData }) {
useEffect(() => {
getData();
}, [getData]);
return <div>{val}</div>;
}
上面例子中getData
的引用永远不会变,因为他它的依赖列表是空。可以根据实际情况将依赖加进去,就能确保依赖不变的情况下,函数的引用保持不变。
假如在getData
中需要用到val
( useState
中的值),就需要将其加入依赖列表,这样的话又会导致每次getData
的引用都不一样,死循环又出现了
import React, { useState, useEffect, useCallback } from 'react';
let count = 0;
function App() {
const [val, setVal] = useState('');
const getData = useCallback(() => {
setTimeout(() => {
console.log(val);
setVal('new data' + count);
count++;
}, 500);
}, [val]);
return <Child val={val} getData={getData} />;
}
export default App;
function Child({ val, getData }) {
useEffect(() => {
getData();
}, [getData]);
return <div>{val}</div>;
}
如果我们希望无论val
怎么变,getData
的引用都保持不变,同时又能取到val
最新的值,可以通过自定义 hook 实现。注意这里不能简单的把val
从依赖列表中去掉,否则getData
中的val
永远都只会是初始值(闭包原理)。
function useRefCallback(fn, dependencies) {
const ref = useRef(fn);
// 每次调用的时候,fn 都是一个全新的函数,函数中的变量有自己的作用域
// 当依赖改变的时候,传入的 fn 中的依赖值也会更新,这时更新 ref 的指向为新传入的 fn
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
使用
import React, { useState, useEffect, useCallback, useRef } from 'react';
let count = 0;
function App() {
const [val, setVal] = useState('');
const getData = useRefCallback(() => {
setTimeout(() => {
console.log(val);
setVal('new data' + count);
count++;
}, 500);
}, [val]);
function useRefCallback(fn, dependencies) {
const ref = useRef(fn);
// 每次调用的时候,fn 都是一个全新的函数,函数中的变量有自己的作用域
// 当依赖改变的时候,传入的 fn 中的依赖值也会更新,这时更新 ref 的指向为新传入的 fn
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
return <Child val={val} getData={getData} />;
}
export default App;
function Child({ val, getData }) {
useEffect(() => {
getData();
}, [getData]);
return <div>{val}</div>;
}
例子:
import React, { useState } from 'react';
function App() {
const [value, setValue] = useState(0);
const [count, setCount] = useState(1);
const getDoubleCount = () => {
console.log('getDoubleCount进行计算');
return (count * 2);
};
return (
<div>
<h2>value: {value}</h2>
<h2>doubleCount: {getDoubleCount()}</h2>
<button onClick={() => setValue(value + 1)}>value+1</button>
</div>
);
}
export default App;
可以看到getDoubleCount
依赖的是count
,但value
发生变化它也重新进行了计算渲染,现在只需要将getDoubleCount
使用useMemo
进行包裹,如下:
import React, { useState, useMemo } from 'react';
function App() {
const [value, setValue] = useState(0);
const [count, setCount] = useState(1);
const getDoubleCount = useMemo(() => {
console.log('getDoubleCount进行计算');
return count * 2;
}, [count]);
return (
<div>
<h2>value: {value}</h2>
<h2>doubleCount: {getDoubleCount}</h2>
<button onClick={() => setValue(value + 1)}>value+1</button>
</div>
);
}
export default App;
现在getDoubleCount
只有依赖的count
发生变化时才会重新计算渲染。
useMemo和useCallback的共同点:
接收的参数都是一样的,第一个是回调函数,第二个是依赖的数据
它们都是当依赖的数据发生变化时才会重新计算结果,起到了缓存作用
useMemo和useCallback的区别:
useMemo计算结果是return回来的值,通常用于缓存计算结果的值
useCallback计算结果是一个函数,通常用于缓存函数
const refContainer = useRef(initialValue);
ref
对象,该对象只有个 current
属性,初始值为传入的参数( initialValue )
。ref
对象在组件的整个生命周期内保持不变current
值时并不会 re-render ,这是与 useState
不同的地方useRef
是 side effect (副作用),所以一般写在 useEffect
或 event handler
里useRef
类似于类组件的 this
需求:点击button时选中文本框
实现:
import React, { useRef, MutableRefObject } from 'react';
function App() {
const inputEl = useRef(null);
const handleFocus = () => {
inputEl.current.focus();
};
return (
<div>
<input ref={inputEl} type="text" />
<button onClick={handleFocus}>Focus the input</button>
</div>
);
}
export default App;
通过useRef
定义inputEl
变量,在 input 元素上定义ref = {inputEl}
,这样通过inputEl.current
即可获取到 input DOM 元素,选中则调用focus函数。
需求:跨渲染取状态值
只使用useState
import React, { useState } from 'react';
function App() {
const [like, setLike] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like}`);
//形成闭包,所以弹出来的是当时触发函数时的like值
}, 3000);
}
return (
<div>
<button onClick={() => setLike(like + 1)}>{like}赞</button>
<button onClick={handleAlertClick}>Alert</button>
</div>
);
}
export default App;
为什么不是界面上like的实时状态?
当我们更改状态的时候,React会重新渲染组件,每次的渲染都会拿到独立的like值,并重新定义个handleAlertClick函数,每个handleAlertClick函数体里的like值也是它自己的,所以当like为6时,点击alert,触发了handleAlertClick,此时的like是6,哪怕后面继续更改like到10,但alert时的like已经定下来了。
useRef
import React, { useRef } from 'react';
function App() {
let like = useRef(0);
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like.current}`);
}, 3000);
}
return (
<div>
<button
onClick={() => {
like.current = like.current + 1;
}}
>
{like.current}赞
</button>
<button onClick={handleAlertClick}>Alert</button>
</div>
);
}
export default App;
import React, {
useState,
useEffect,
useRef,
useCallback,
MutableRefObject,
} from 'react';
const ChildInput = (props) => {
const { label, cRef } = props;
const [value, setValue] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setValue(value);
};
const getValue = useCallback(() => {
return value;
}, [value]);
useEffect(() => {
if (cRef && cRef.current) {
cRef.current.getValue = getValue;
}
}, [getValue]);
return (
<div>
<span>{label}:</span>
<input type="text" value={value} onChange={handleChange} />
</div>
);
};
function App() {
const childRef = useRef({});
const handleFocus = () => {
const node = childRef.current;
alert(node.getValue());
};
return (
<div>
<ChildInput label={'名称'} cRef={childRef} />
<button onClick={handleFocus}>focus</button>
</div>
);
}
export default App;
forwardRef: 将父类的ref作为参数传入函数式组件中
示例
React.forwardRef((props, ref) => {})
//创建一个React组件,
//这个组件将会接受到父级传递的ref属性,
//可以将父组件创建的ref挂到子组件的某个dom元素上,
//在父组件通过该ref就能获取到该dom元素
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 可以直接获取到button的DOM节点
const ref = React.useRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
useImperativeHandle: 在函数式组件中,用于定义暴露给父组件的ref方法,用来限制子组件对外暴露的信息,只有useImperativeHandle
第二个参数定义的属性跟方法才可以在父组件获取到
为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
解决: 使用 uesImperativeHandle 解决,在子函数式组件中定义父组件需要进行的 DOM 操作,没有定义的就不会暴露给父组件
useImperativeHandle(ref, createHandle, [deps]) // 第一个参数暴露哪个ref;第二个参数暴露什么信息
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
// 渲染 的父组件
// 可以调用 inputRef.current.focus()
实现
import React, {
MutableRefObject,
useState,
useImperativeHandle,
useRef,
forwardRef,
useCallback,
} from 'react';
let ChildInput = forwardRef((props, ref) => {
const { label } = props;
const [value, setValue] = useState('');
// 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
// 参数1: 父组件传递的ref属性
// 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
useImperativeHandle(ref, () => ({
getValue,
}));
const handleChange = (e) => {
const value = e.target.value;
setValue(value);
};
const getValue = useCallback(() => {
return value;
}, [value]);
return (
<div>
<span>{label}:</span>
<input type="text" value={value} onChange={handleChange} />
</div>
);
});
const App = (props) => {
const childRef = useRef({});
const handleFocus = () => {
const node = childRef.current;
alert(node.getValue());
};
return (
<div>
<ChildInput label={'名称'} ref={childRef} />
<button onClick={handleFocus}>focus</button>
</div>
);
};
export default App;