React 开发总结
使用 React 开发的一些总结
渲染
- 同时改变 props & state,会触发几次渲染? 比如:在 handleClick 中调用函数改变 props 同时 setState,只会触发一次渲染
- react useEffect, useCallback, useMemo 的 dependencies -> [a]: 表示在第 1 次和 a 改变的时候会去执行;
- 如果依赖项是对象,两个对象必须是同一个对象
useEffect
才会跳过执行effect
。所以,即使内容完全相同,内存地址不同的话,useEffect
还是会执行effect
。 - 如果一个函数作为 props 传给子组件A,那么它最好使用 useCallback,否则会因为父组件其他 props/state(子组件A未使用到的)的更新导致该函数更新,进而导致子组件A重复渲染;
- 如果一个变量作为 props 传给子组件A,也会因为父组件其他 props/state(子组件A未使用到的)的更新导致子组件A重复渲染,可以使用 useMemo(props),更推荐 React.memo(A),这样写法简单,不用每个 props 都 useMemo 一遍;
- .then, setTimeout 回调中每次 setState 都会触发一次渲染。
- react 事件回调函数中(比如:handleClick)多次 setState 只会触发一次渲染。
const [value, setValue] = useState(props.value)
这种写法在 props.value 变化时不会更新 value 的值。
依赖
React 官方推荐的依赖插件会自动加上依赖,那么自动加上的是啥呢?
- state: 不会加上 setState, 例如:const [visible, setVisible] = useState(false), visible 是依赖,setVisible 不是
- props: 变量 props & 函数 props
- const memoVariable = useMemo()
- const handleClick = useCallback()
- 内部变量:在内部定义的没有用 useMemo 包起来的变量
- 内部函数:在内部定义的没有用 useCallback 包起来的函数
哪些不会被当成 dependencies:
- 函数入参
- 引用的第三方
实例:
下面的 getBrandList 依赖是插件自动添加的
const { brandId: qcBrandId, qcOriginalRegion } = props.qcBrand;
const [loading, setLoading] = useState<boolean>(false);
const [searchType, setSearchType] = useState<SearchType>(SearchType.ID);
const debounceSearch = useDebounce(search, 500);
const [page, setPage] = useState<number>(DEFAULT_PAGE);
const region = getCountry();
const regionList = useMemo(
() =>
isLocal
? [region, WORLDWIDE]
: qcOriginalRegion
? [qcOriginalRegion, WORLDWIDE]
: [WORLDWIDE],
[isLocal, qcOriginalRegion, region],
);
const getBrandList = useCallback(async () => {
try {
if (loading || !qcBrandId) return;
setLoading(true);
const searchParams: globalBrand.ISearchBrandWithBlacklistInfoRequest = {
regionList,
statusList: [BRAND_NORMAL],
offset: page * 10,
limit: 10,
orderBy: 'ctime desc',
subBrandId: qcBrandId,
};
const search = debounceSearch;
if (search) {
if (searchType === SearchType.ID) {
const brandId = parseInt(search);
if (!isBrandId(brandId)) {
setLoading(false);
return;
}
searchParams.brandId = brandId;
} else {
searchParams.brandName = search;
}
}
const resp =
(await searchBrandWithBlacklistInfo(searchParams))?.brandList || [];
const brandList = resp.map(
({ blacklistedCatWithKeywords, brand }) =>
({
...brand,
blacklistedCatWithKeywords,
} as IBrandWithDuplicateInfo),
);
setBrandList((brands) =>
page === DEFAULT_PAGE ? brandList : [...brands, ...brandList],
);
onAfterResponse && onAfterResponse();
setLoading(false);
} catch (error) {
onAfterResponse && onAfterResponse();
setLoading(false);
}
}, [
loading,
qcBrandId,
regionList,
page,
debounceSearch,
onAfterResponse,
searchType,
]);
插件帮我们加的依赖是否都是必须的呢?多数情况是需要的,但有时候会造成重复请求/重复渲染。
开发中一个常见的场景:先 loading, 发送请求,拿到返回数据后,loading 消失。下面的写法如果使用插件自动添加依赖的话就会造成重复请求。
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(false)
const [data, setData] = useState();
const getData = useCallback(aync () => {
if (loading) return;
setLoading(true)
const res = await getDataFromServer({
search,
})
setData(res)
setLoading(false)
}, [search, loading]) // 插件会自动加上 search, loading
useEffect(() => {
getData()
}, [getData]) // 插件会自动加上 getData
执行过程:
第 4 次渲染和第 1 次很像:调用 getData, setLoading(false)。然后继续触发第 2、3 次渲染。整个渲染过程其实就是[1,2,3]的循环,因此造成重复请求。
上面有两种改法:
- 方式一:useEffect 的依赖改成 search
const getData = useCallback(() => {}, [search, loading])
useEffect(() => {
getData()
}, [search])
- 方式二:useCallback 的依赖改成 search
const getData = useCallback(() => {}, [search])
useEffect(() => {
getData()
}, [getData])
哪种方式更好呢?
之所以总结是因为我开发时多次遇到重复请求/重复渲染的问题,我希望在一开始写代码的时候就能避免写成重复请求/重复渲染的代码。那有没有一种规范,如果我们遵守这个规范,就能达到目的,即:尽量在一开始写代码的时候就正确添加依赖,不额外渲染,不重复请求。
探索添加依赖的一种实践规范:
- 不建议一上来就使用依赖插件。很多时候,我们不会仔细看自动添加了哪些依赖,我接手的第一个 react 项目开启了保存自动添加依赖的功能,当时就是无脑添加,出了问题再去改;后面新的项目没有开启这个功能,手动添加就变得不知所措。依赖插件会造成惰性心理。
- useCallback / useMemo 加上所有依赖。useCallback / useMemo 闭包了 state / props 等,当它们变化时如果 useCallback / useMemo 没有更新,就会导致执行的时候取的是 state / props 旧值,所以加上所有依赖。由于依赖很多,自己去添加可能会遗漏,这时可以利用插件加上所有。
- useEffect 依赖不使用插件添加。添加依赖的核心是依赖的变化是否需要 useEffect 重复执行,如果需要的话,那它是一个依赖,如果不需要,即使插件帮我们自动加上了,也应该去掉,否则可能造成死循环,陷入重复渲染!
基于上面的规范,我觉得方式二比较好。
场景
示例一
我在开发中经常遇到的一个场景是:有一个组件 A,可接收一个外部值作为初始值,组件 A 可修改该状态。这种场景有多少种实现方式呢?
方式一:组件 A 内部维护一个 state,状态变更时使用 state 保存最新状态,同时调用 props.onChange 将最新状态暴露给父组件;使用 useEffect 设置初始值,并且 props.value 变更时去更新 state。
const [value, setValue] = useState()
const handleClick = useCallback((val) => {
setValue(val)
props.onChange && props.onChange(val)
})
useEffect(() => {
setValue(props.value)
}, [props.value])
方式二:其他同上,但使用 useEffect 设置初始值时,props.value 不作为依赖。
const [value, setValue] = useState()
const handleClick = useCallback((val) => {
setValue(val)
props.onChange && props.onChange(val)
})
useEffect(() => {
setValue(props.value)
}, [])
方式一和方式二用哪种呢?
方式一是更通用的一种写法,如果父组件中的某个操作改变了 props.value,那么子组件也能及时更新。antd 的 radio/input/select 都是类似的写法。如果是实现一个很基础通用的组件,那么建议第一种。如果是业务组件,已经明确知道父组件不会有改变 props.value 的操作,那么可以用第二种。
方式三:使用方式一的时候,发现 handleClick 调用后 setValue 了两次同样的值(1次在 handleClick,1次在 useEffect),所以在方式一的基础上删除了 setValue(val)
,发现:哎,能用,还少了一次 setState,真棒!但其实这种写法有个问题:组件 A 此时维护的是个空壳状态!一旦父组件没有提供 onChange,组件 A 内部操作无法更新状态,所以,不要这样写。
const [value, setValue] = useState()
const handleClick = useCallback((val) => {
props.onChange && props.onChange(val)
})
useEffect(() => {
setValue(props.value)
}, [props.value])
方式四:组件 A 内部没有维护一个 state,状态变更时直接调用 props.onChange 将最新值传给父组件;父组件更新状态(父组件维护 state)从而组件 A 重新渲染。
const handleClick = useCallback((val) => {
props.onChange && props.onChange(val)
})
return (
<div>{props.value}</div>
)
方式四适合纯展示的组件,内部无需维护状态。
示例二
如果组件 A 在需求初期需要父组件传对象的一个属性,那是将该属性作为 props 还是该对象作为 props 呢?
我一开始开发的时候会选择传属性,觉得传那么多数据干嘛?但好多次的项目经验都证明后续需要的属性会越来越多,还不如一开始传对象。这个场景其实还是要具体分析了,特别提出来提醒自己下,如果能在开发初期预见更好,省的后续改代码了,你们说是不?
---------------------------------这是一条分界线--------------------------------
懒了大半年了🥱...... 中秋快乐🎑!打工人们
转载自:https://juejin.cn/post/7142404503868801054