1,勿滥用 useState,非受控组件可以直接通过 ref 从 dom 上取值
import React, { useState } from "react"
type Props = {}
export default function Form({}: Props) {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
function onSubmit(e) {
e.preventDefault()
console.log({ email, password })
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
onChange={e => setEmail(e.target.value)}
/>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
onChange={e => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
)
}
这个代码看着似乎没有任何问题,运行起来也很好。可是我们会发现这个email,password只在提交表单的时候会使用,而每次我们进行输入改变这个状态变量的时候都会让组件重新渲染,这并不好。所以下面这个版本,使用 ref 能很好解决这个问题。
import React, { useRef } from "react"
export default function Form() {
const emailRef = useRef()
const passwordRef = useRef()
function onSubmit(e) {
e.preventDefault()
console.log({
email: emailRef.current.value,
password: passwordRef.current.value,
})
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input ref={emailRef} type="email" id="email" />
<label htmlFor="password">Password</label>
<input ref={passwordRef} type="password" id="password" />
<button type="submit">Login</button>
</form>
)
}
这种写法不仅更加简洁优雅而且可以避免在输入的时候组件重新渲染。我们在写表单的组件的时候可以尝试用useRef替代useState。
2,关于useState
import React, { useState } from "react"
export default function Count() {
const [count, setCount] =useState (0)
function fn(num){
setCount(count+num)
setCount(count+num)
}
return (
<>
<button onClick={()=>{fn(-1)}}>-</button>
{count}
<button onClick={()=>{fn(+1)}}>+</button>
</>
)
}
会发现计数器函数fn虽然执行了两次setCount但是还是一次只加1,这是因为React的状态更新是异步的,所以两次setCount
调用会在同一渲染周期中处理。每次setCount的时候都是取一开始count的状态。
function fn(num) {
setCount(currentCount => {
return currentCount + num
})
setCount(currentCount => {
return currentCount + num
})
}
而如果这么写的话,就会发现一次点击会加2。这是因为这种写法使用了函数形式的setCount
,函数接受了currentCount
作为参数,确保了在每次更新状态时都是基于最新的状态。因此,这两次更新将各自增加num
的值到状态中。
3,useState的异步解决方案
function fn(num) {
setCount(currentCount => {
return currentCount + num
})
console.log(num);
}
由于setState是一个异步函数,所以在使用后直接使用state状态的时候,并不会取最新值。我们可以使用useEffect监听state的变化,代码也很简单。
function fn(num) {
setCount(currentCount => {
return currentCount + num
})
}
useEffect(() => {
console.log(count)
}, [count])
4, 请勿滥用useEffect
import React, { useEffect, useState } from "react"
export default function Count() {
const [firstName, setFirstName] = useState("")
const [lastName, setLastName] = useState("")
const [fullName, setFullName] = useState("")
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
return (
<>
<input
type="text"
onChange={e => {
setFirstName(e.target.value)
}}
/>
<br />
<input
type="text"
onChange={e => {
setLastName(e.target.value)
}}
/>
<br />
{fullName}
</>
)
}
这样写的话,也是能正常工作运行。但是会发现,用useEffect监听以及在使用setFullName的时候多少有些冗余以及同样的重复渲染。
const [firstName, setFirstName] = useState("")
const [lastName, setLastName] = useState("")
const fullName = `${firstName} ${lastName}`
什么时候用useState,什么时候直接用const,值得我们去实践与思考。
5,useMemo实用小技巧
import React, { useEffect, useState } from "react"
export default function Count() {
const [lastName, setLastName] = useState("")
const [firstName, setFirstName] = useState("")
const [num, setNum] = useState(0)
const fullName = {
lastName,
firstName,
}
useEffect(() => {
console.log(fullName)
}, [fullName])
return (
<>
<input
type="text"
onChange={e => {
setLastName(e.target.value)
}}
/>
<br />
<input
type="text"
onChange={e => {
setFirstName(e.target.value)
}}
/>
<br />
<button onClick={() => setNum(num + 1)}>click</button>
{num}
</>
)
}
大家可以复制代码到本地实践一下。我们会很神奇的发现,我们不仅在输入框中输入的时候会输出fullName,在点击button按钮的时候也会输出fullName!为什么呢? 原来,这是因为每次setNum的时候,react会重新渲染组件,所以会从上到下读一遍代码,当读到
const fullName = {
lastName,
firstName,
}
的时候。react会对fullName重新赋值。也就是说fullName会改变(虽然看起来对象似乎没有改变,但我们知道 {} === {} 是一个false ,所以重新赋值之后就是不一样的对象了)。然后就会执行useEffect函数。
怎么避免这种情况呢,我们可以使用useMemo。
const fullName = useMemo(() => {
return {
lastName,
firstName,
}
}, [lastName, firstName])
把fullName这个对象用useMemo写,并监听键,就可以不会反复的去让fullName去重新赋值了。可能现在看来,有些例子的钩子用法看着跟原来的写法也没啥区别,可是等工作的时候,就会发现这些基础知识知道了就是知道了,并且会在某个瞬间让你恍然大悟。笔者就深有体会,在某一天改bug的时候,看到同事写的代码中用了useEffect监听一个字段,当你要对这个字段进行维护的时候,就会发现很容易出问题,但是如果你对useEffect的各种用法烂熟于心,就会在维护的过程中得心应手很多。还有的时候看到useMemo的时候也不会想为什么要在这用,不用的话会产生什么影响之类的疑惑了。还有就是呢,面试也爱问useMemo,useCallback这些,到时候面试官一问useMemo你把我这个例子给他这么一说,也能对你印象好不少。
6,一个实用的自定义钩子
最后是一个自定义钩子,也是面试官爱问的东西。笔者在面试小米的时候就有被问到平时是否有自己写过自定义钩子,返回值是什么之类的。
import { useEffect, useState } from "react"
const useFetch = url => {
const [loading, setLoading] = useState(true)
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
const controller = new AbortController()
setLoading(true)
fetch(url, { signal: controller.signal })
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
return () => {
controller.abort()
}
}, [url])
return { loading, data, error }
}
export default useFetch
亮点在于当url输入时候导致频繁改变的时候,会触发 controller.abort() 清理副作用,取消请求,也就是只有最后一次请求被完全处理。 例如在处理实时搜索时,你可能只关心用户最后一次输入的搜索词。然而,在其他情况下,你可能想要在useEffect
的依赖数组中包含更多的变量,以确保每次这些变量发生变化时都触发请求。controller.abort()
的作用是确保在组件卸载时取消正在进行的Fetch请求,以避免潜在的问题和资源泄漏,是一种处理异步操作的良好实践。