1.5w字记录我使用useState useEffect踩过的12个坑

话不多说直接上案例!

1.state异步更新 批处理

这是在react官方文档提到的,但开发中有些时候大意也会经常搞错

简单案例

tsx 复制代码
import { useState } from 'react'
export default function Demo1() {
	const [count, setCount] = useState(0)
	const handleClick = () => {
		setCount(count + 1)
		setCount(count + 1)
		setCount(count + 1)
	}
	return (
		<div>
			<button onClick={handleClick} className="bg-blue-400 px-4 py-1 text-white rounded">
				Click me{' '}
			</button>
			<div className="mx-2">{`count is ${count}`}</div>
		</div>
	)
}
  1. 触发handleClick函数,由于React中的状态更新是异步的,会将三次setCount添加入队列
  2. 当React最终决定执行状态更新时使用的是最新的count值,而不是在handleClick函数中每次调用setCount时的值 即:
tsx 复制代码
setCount(0 + 1)
setCount(0 + 1)
setCount(0 + 1)

这个现象通常被称为"React中的批处理"(Batching in React)。React 会将多个状态更新操作合并到一个批处理中,以提高性能并减少不必要的渲染。这意味着在一次渲染周期内,多个状态更新可能会被合并成一个,从而导致某些操作不会立即生效

解决方案是 修改为更新函数,每个更新函数都会加入队列,更新函数会获取上个更新函数的返回值经过计算传递给下个更新函数

tsx 复制代码
const handleClick = () => {
    setCount(prev => prev + 1)
    setCount(prev => prev + 1)
    setCount(prev => prev + 1)
}

详细可见官方文档 react文档 react中文文档

2.条件渲染

React Hooks必须以相同的顺序被调用: 如果你在一个条件语句中使用了 useEffect等hooks,这个条件语句的结果可能会导致在某些情况下提前返回组件,那么就会触发这个错误 例如:

tsx 复制代码
import { useEffect, useState } from 'react'
export default function Demo2({ id }: { id: number }) {
	if (!id) return 'hello world'
	const [doSomething, setSomething] = useState()

	useEffect(() => {}, [doSomething])
	return <div>{'other..'}</div>
}

如果要粗暴的解决直接把这个eslint关掉也行。。。但更好的办法是把条件渲染语句放在hooks之后

tsx 复制代码
import { useEffect, useState } from 'react'
export default function Demo2({ id }: { id: number }) {
	const [doSomething, setSomething] = useState()
	useEffect(() => {}, [doSomething])
	return <div>{!id ? 'hello asworld' : <div>{'other thing'}</div>}</div>
}

这样就ok, 这个setSomething是没有使用 做了个警告⚠️

3.更新对象状态

假设你有一个表单,你需要通过一个输入框更新初始化的person中的名字,你可能会这样做

tsx 复制代码
import { useEffect, useState } from 'react'
export default function Demo() {
	const [person, setPerson] = useState({ name: 'herry', age: '18', sex: 'female' })
	const handleClick = e => {
		setPerson((person.name = e.target.name))
	}
	return (
		<form>
			<input
				onChange={handleClick}
				className="rounded h-10  bg-sky-100"
				type="text"
				placeholder="enter you name"
			/>
		</form>
	)
}

报错是因为person是一个对象,修改之后返回的也应该是对象

tsx 复制代码
// 可以复制一个对象 但是修改的name需要放在...person之下,防止被覆盖
const handleClick = e => {
    setPerson({
        ...person,
        name : e.target.value
    })
}

// 或者写成函数形式
const handleClick = e => {
    setPerson(perv => {
        return {
            ...perv,
            name: e.target.value,
        }
    })
}
// 或者直接返回
const handleClick = e => {
    setPerson(perv => ({
        ...perv,
        name: e.target.value,
    }))
}

4.统一改变一个对象的值

假设你现在有一个表单 每个输入框改变你都需要传入不同的参数,如果你知道可以传键值你可能会这样做

tsx 复制代码
import { useEffect, useState } from 'react'
export default function Form() {
    const [form, setForm] = useState({
        firstName: '',
        secondName: '',
        email: '',
        address: '',
    })
    const handleChange = (e, propsName) => {
        setForm({
            ...form,
            [propsName]: e.target.value,
        })
    }
    return (
        <form className="flex flex-col gap-y-2">
            <input
                onChange={e => handleChange(e, 'firstName')}
                className="px-4 py-2"
                name="firstName"
                type="text"
                placeholder="firstName"
            />
            <input
                onChange={e => handleChange(e, 'secondName')}
                className="px-4 py-2"
                name="secondName"
                type="text"
                placeholder="secondName"
            />
            //...
            <button className="px-4 py-2 bg-blue-500 text-white">Submit</button>
        </form>
    )
}

因为每个input都有个name属性,可以利用name来简洁我们的代码

js 复制代码
import { useEffect, useState } from 'react'
export default function Form() {
	const [form, setForm] = useState({
		firstName: '',
		secondName: '',
		email: '',
		address: '',
	})
	console.log(form)
	const handleChange = e => {
		setForm({
			...form,
			[e.target.name]: e.target.value,
		})
	}
	return (
		<form className="flex flex-col gap-y-2">
			<input
				onChange={handleChange}
				className="px-4 py-2"
				name="firstName"
				type="text"
				placeholder="firstName"
			/>
			<input
				onChange={handleChange}
				className="px-4 py-2"
				name="secondName"
				type="text"
				placeholder="secondName"
			/>
			<input
				onChange={handleChange}
				className="px-4 py-2"
				name="email"
				type="text"
				placeholder="email"
			/>
			<input
				onChange={handleChange}
				className="px-4 py-2"
				name="address"
				type="text"
				placeholder="address"
			/>
			<button className="px-4 py-2 bg-blue-500 text-white">Submit</button>
		</form>
	)
}

5.省去不必要是state,effect

假设你现在有一辆购物车,每件商品是PRICE利润,当增加商品时可以通过商品件数 * PRICE算出总利润profit

你可能会这么写

js 复制代码
import { useEffect, useState } from 'react'
const PRICE = 10
export default function CountProfle() {
	const [count, setCount] = useState(0)
	const [profit, setProfit] = useState(0)
	useEffect(() => setProfit(PRICE * count), [count])
	return (
		<div>
			<button
				onClick={() => setCount(count + 1)}
				className="bg-blue-400 px-4 py-1 text-white rounded"
			>
				add one
			</button>
			<div className="mx-2">{`profit is ${profit}`}</div>
		</div>
	)
}

但其实每次add one都会重新渲染页面,可以直接通过计算得出profit

js 复制代码
import { useEffect, useState } from 'react'
const PRICE = 10
export default function CountProfle() {
	const [count, setCount] = useState(0)
	const profit = count * PRICE
	return (
		<div>
			<button
				onClick={() => setCount(count + 1)}
				className="bg-blue-400 px-4 py-1 text-white rounded"
			>
				add one
			</button>
			<div className="mx-2">{`profit is ${profit}`}</div>
		</div>
	)
}

6.state的浅比较

JavaScript中原始值和引用值,引用值就是object,原始值就是除object以外的类型,state在比较更新时只是浅比较

js 复制代码
import { useEffect, useState } from 'react'
export default function CompareState() {
	console.log('render..')
	const [count, setCount] = useState(0)
	const handleClick = () => {
		setCount(0)
	}
	return (
		<div>
			<button onClick={handleClick} className="bg-blue-400 px-4 py-1 text-white rounded">
				count
			</button>
		</div>
	)
}

可见如果原始值没有改变其实state不会重新渲染

而引用值是比较地址的不同,显然会重新渲染

js 复制代码
import { useEffect, useState } from 'react'
export default function CompareState() {
	console.log('render..')
	const [person, setAge] = useState({ name: `herry`, age: '18' })
	const handleClick = () => {
		setAge({ name: `herry`, age: '18' })
	}
	return (
		<div>
			<button onClick={handleClick} className="bg-blue-400 px-4 py-1 text-white rounded">
				{person.age}
			</button>
		</div>
	)
}

如果你想监听object中的每个属性

js 复制代码
useEffect(()=>{
    doSometing()
},[obj.prop])

7.初始化加载数据

考虑一个场景,进入一个页面首先获取数据然后展示特定数据,你可能会这样写

js 复制代码
import { useEffect, useState } from 'react'
export default function InitData() {
	console.log('render..')
	const [data, setData] = useState(null)
	useEffect(() => {
		fetch('https://dummyjson.com/posts/1')
			.then(e => e.json())
			.then(e => setData(e))
	}, [])
	return (
		<div>
			<h1>{data.title}</h1>
			<p>{data.body}</p>
		</div>
	)
}

这是因为在数据还获取到之前data为Null,直接.title取值肯定是取不到的 为空 一般做法是在加载页面之前会有个loading, 数据加载完loading撤去

js 复制代码
import { useEffect, useState } from 'react'
export default function InitData() {
	console.log('render..')
	const [data, setData] = useState(null)
	const [loading, setLoading] = useState(true)
	useEffect(() => {
		fetch('https://dummyjson.com/posts/1')
			.then(e => e.json())
			.then(e => {
				setData(e)
				setLoading(false)
			})
	}, [])
	return (
		<div>
			{loading ? (
				'loading ...'
			) : (
				<>
					<h1>{data?.title}</h1>
					<p>{data?.body}</p>
				</>
			)}
		</div>
	)
}

8.给state加必要的类型

比如上个例子,其实稍不注意万一写成data.titleee也是有可能的 ,js也不会报错

js 复制代码
type Data{
    title : string
    body : string
}
const [data, setData] = useState<Data | null>(null)

9.尽可能将重复的代码抽离成hook

假设你有俩个组件同时需要用到windowSize, 其中也有一些重复的逻辑,可能是这样的代码

js 复制代码
import { useEffect, useState } from 'react'
export function ExampleComponent1() {
	const [windowsize, setWindowsize] = useState(1092)
	useEffect(() => {
		const changeWindowSize = () => {
			setWindowsize(window.innerWidth)
		}
		window.addEventListener('resize', changeWindowSize)
		return () => {
			window.removeEventListener('resize', changeWindowSize)
		}
	})
	return <>component1</>
}
export function ExampleComponent2() {
	const [windowsize, setWindowsize] = useState(1092)
	useEffect(() => {
		const changeWindowSize = () => {
			setWindowsize(window.innerWidth)
		}
		window.addEventListener('resize', changeWindowSize)
		return () => {
			window.removeEventListener('resize', changeWindowSize)
		}
	})
	return <>component2</>
}

都是重复代码,逻辑一样 ===> hook!

js 复制代码
import { useEffect, useState } from 'react'
const useWindowSize = () => {
	const [windowsize, setWindowsize] = useState(1092)
	useEffect(() => {
		const changeWindowSize = () => {
			setWindowsize(window.innerWidth)
		}
		window.addEventListener('resize', changeWindowSize)
		return () => {
			window.removeEventListener('resize', changeWindowSize)
		}
	})
	return windowsize
}
export function ExampleComponent1() {
	const windowsize = useWindowSize()
	return <>component1</>
}

export function ExampleComponent2() {
	const windowsize = useWindowSize()
	return <>component2</>
}

10.node环境使用hook

如果你在node环境中比如express、nest、koa框架中直接使用useState useEffect是找不到的,当然一些浏览器特有的东西也不存在,比如window localStoarge也没有 你必须要声明在客户端环境'use client'才能使用 或者你在根层级就已经声明'use client'

11.记得给useEffect添加必要的清除函数

假设你的需求是显示每秒数字增1,你可能会这样写

js 复制代码
import { useEffect, useState } from 'react'
export default function Count() {
	const [count, setCount] = useState(0)
	useEffect(() => {
		setInterval(() => {
			console.log('render...')
			setCount(count + 1)
		}, 1000)
	}, [])
	return (
		<div>
			<h1 className="bg-blue-400 px-4 py-1 text-white rounded">{count}</h1>
		</div>
	)
}

但实际的效果却是

其实setInterval有正常工作,只是setCount中的count始终都是初始的0 执行的总是setCount(0 + 1) 解决办法是可以监听count变化,那么每次变化都会添加一个定时器,但是添加定时器一定要记得清理!

js 复制代码
import { useEffect, useState } from 'react'
export default function Count() {
	const [count, setCount] = useState(0)
	useEffect(() => {
		const i = setInterval(() => {
			console.log('render...')
			setCount(count + 1)
		}, 1000)
		return () => clearInterval(i)
	}, [count])
	return (
		<div>
			<h1 className="bg-blue-400 px-4 py-1 text-white rounded">{count}</h1>
		</div>
	)
}

当然其实还有之前提到的更简单的办法 写成更新函数的形式

js 复制代码
useEffect(() => {
        setInterval(() => {
        console.log('render...')
        setCount(prev => prev + 1)
    }, 1000)
}, [])

12.useEffect使用fetch

假如这是你最后的需求,用户随机点击切换数据id时,需要展示相应的数据,如果你用fetch可能会这么写

js 复制代码
import { useEffect, useState } from 'react'
export default function ShowData() {
	const [id, setId] = useState(1)
	return (
		<div className="w-screen">
			<button
				onClick={() => setId(Math.floor(Math.random() * 100))}
				className="bg-blue-400 w-60 mx-20 px-3 py-1 text-white rounded"
			>
				{'随机一个数据'}
			</button>
			<DataBody id={id} />
		</div>
	)
}
function DataBody({ id }: { id: number }) {
	const [data, setData] = useState('')
	useEffect(() => {
		fetch(`https://dummyjson.com/posts/${id}`)
			.then(res => res.json())
			.then(data => setData(data.body))
	}, [id])
	return (
		<>
			<div>{data}</div>
		</>
	)
}

大体是完成任务的,但是如果老奸巨猾的测试使劲点刷新还是能发现点bug:每次点击都要重新请求然后渲染上上次点击的数据,但是如果点击过快最终展示是上次点击的数据 但是过程中会不断闪现之间的随机的数据 解决办法是使用AbortController用于管理和中止网络请求,在fetch第二个参数添加signal属性,将该AbortController 与该请求关联起来以便在需要时能够中止该请求,组件卸载时在清理函数中止请求controller.abort()

tsx 复制代码
function DataBody({ id }: { id: number }) {
	const [data, setData] = useState('')
	useEffect(() => {
		const controller = new AbortController()
		fetch(`https://dummyjson.com/posts/${id}`, {
			signal: controller.signal,
		})
			.then(res => res.json())
			.then(data => setData(data.body))
            .catch(err => console.log(err))
		return () => controller.abort()
	}, [id])
	return (
		<>
			<div>{data}</div>
		</>
	)
}

完成任务!! 有收获记得点赞收藏哦 ~

相关推荐
Android洋芋12 分钟前
Android的资源管理规范
前端
代码搬运媛12 分钟前
startTransition:React 18 的并发渲染控制详解
react.js
土豆125019 分钟前
React-Markdown 完全上手指南
react.js·markdown
深呼吸99319 分钟前
如何用div手写一个富文本编辑器(contenteditable="true")
前端·vue.js
小赵学鸿蒙35 分钟前
CodeGenie 工具功能汇总
前端
叫一只猪36 分钟前
基于Antd+Dumi搭建组件库
前端·前端框架
LovelyAqaurius39 分钟前
WebGL详解Part2:着色器的奥秘
前端
断竿散人39 分钟前
彻底吃透CSS盒模型:从布局坍塌到精准控制!
前端·css
小赵学鸿蒙41 分钟前
DevEco Studio 安装与使用全流程
前端
踢足球的,程序猿42 分钟前
WebAssembly的本质与核心价值
前端·javascript·前端框架·wasm·webassembly