在实际开发时,有许多之前没注意到的踩坑的点,记录一下。
通过 Enter 键提交表单但对话框未关闭。
前提:点击了DialogClose内部元素,才能触发对话框关闭。
❎ onKeyDown 直接调用了 handleJoinClick(),导致没有触发onClick事件,因此无法触发对话框关闭。
<DialogContent onKeyDown={(event) => { if (event.key === "Enter") { handleJoinClick(); } }} > <input /> <input /> <DialogClose asChild> <Button onClick={handleJoinClick} > Join </Button> </DialogClose> </DialogContent> const handleJoinClick = () => { // ... void openUrl(url, true); };
❎ 试图通过 ref ,在 handleJoinClick() 中手动触发按钮点击事件。但是,在 handleJoinClick()内部调用 handleJoinClick(),会导致 click 两次!!
const joinButtonRef = useRef<HTMLButtonElement>(null); const handleJoinClick = () => { if (!currentVcApp) return; // Input processing logic... if (joinButtonRef.current) { joinButtonRef.current.click(); // 触发对话框关闭 } };
✅ 不要让 handleJoinClick()在内部再次被调用,而是用 useRef 给 Button 打一个标签,在外部模拟点击事件(相当于点了一下 Button)。
const joinButtonRef = useRef<HTMLButtonElement>(null); <DialogContent onKeyDown={(event) => { if (event.key === "Enter") { if (joinButtonRef.current) { joinButtonRef.current.click(); // 或者直接调用 handleJoinClick(); } } }} > <input /> <DialogClose asChild> <Button onClick={handleJoinClick} ref={joinButtonRef} > Join </Button> </DialogClose> </DialogContent>
注意: 脑子放清醒点
使用 Map 按需渲染组件时,某组件内部组件的状态变化,导致整个组件重新渲染。
首先明确react组件重渲染的触发条件:
props
或者state
发生改变触发组件重渲染;父组件的重渲染触发子组件的重渲染。❎ 一些本应该存在于内部的状态被定义在顶层函数内。
此处,恰巧 input 的内容会导致这些内部状态的改变,而这些内部状态被定义在了顶层函数……这样,当改变 input 内容时,顶层函数会渲染,继而这些内部也全会被重新渲染,
const Dialog = () => { const [status, setStatus] = useState("a"); const [state1, setState1] = useState(""); const [state2, setState2] = useState(""); const [state3, setState3] = useState(""); const A = () => { const handleChange = (e) => { setState1(e.target.value); }; return ( <div> <input value={state1} onChange={handleChange} /> </div> ); }; const B = () => { const handleClick = () => { setState2(state2 + 1); }; return ( <div> <button onClick={handleClick}>Increment</button> </div> ); }; const dialogMap = { a: <A />, b: <B />, }; return <>{dialogMap[status]}</>; };
✅ 将组件的内部状态管理和逻辑,内聚到单个组件或更小的上下文中,避免让顶层组件管理所有状态。
const Dialog = () => { const [status, setStatus] = useState("a"); const A = () => { const [state1, setState1] = useState(""); const handleChange = useCallback((e) => { setState1(e.target.value); }, []); return ( <div> <input value={state1} onChange={handleChange} /> </div> ); }; const B = () => { const [state2, setState2] = useState(0); const handleClick = useCallback(() => { setState2((prev) => prev + 1); }, []); return ( <div> <button onClick={handleClick}>Increment</button> </div> ); }; const dialogMap = useMemo(() => ({ a: <A />, b: <B />, }), []); return <>{dialogMap[status]}</>; };
不必要的 state,导致理解成本增加。
写完代码记得 review 每个 state:是否有必要存在?能不能被别的 state 直接计算出来?如果可以,直接把相关 state 删掉!
一个规范:react 中尽量用箭头函数。
bad
function xxx(){ return () }
good
const xxx = () => { return () }
用 map 来替代switch-case
bad
const renderDialogContent = (status) => { switch (status) { case 'loading': return <LoadingComponent />; case 'error': return <ErrorComponent />; case 'success': return <SuccessComponent />; default: return null; } }; // ...调用 renderDialogContent
good
const dialogContentMap: Record<StartOrJoinMeetingDialogEnum, JSX.Element> = { startOrJoin: <StartOrJoinContent />, startPlatform: <StartPlatformContent />, joinByCode: <JoinByCodeContent />, tryingToJoin: <TryingToJoinContent />, incorrectCode: <WrongCodeContent />, incorrectCodeOrPasscode: <WrongCodeOrPasscodeContent />, hidden: <></>, }; // ...通过 dialogContentMap[startOrJoin] 取出对应JSX用即可。
TS 中可以用 Record<key, value>来规定类型
Record<k,v>
type User = { id: number; name: string; }; type UserRecord = Record<string, User>; const users: UserRecord = { johnDoe: { id: 1, name: 'John Doe' }, janeDoe: { id: 2, name: 'Jane Doe' } }; // 类型安全 const john = users.johnDoe; // users.janeDoe.age = 30; // 编译错误,因为 User 类型中没有 age 属性
闭包陷阱
问题描述
当在 React 组件中使用函数时,如果函数捕获了组件的状态(state)或属性(props),但没有正确处理更新,可能会导致函数始终使用旧的值。
示例场景
// ❌ 错误示例 const MyComponent = () => { const [data, setData] = useState(null); const handleData = () => { console.log(data); // 永远输出初始值 }; useEffect(() => { // 监听某个事件 eventListener.on('ready', handleData); }, []); };
问题原因
- 函数在组件初始化时被创建
- 函数捕获了当时的状态值(闭包)
- 即使状态更新了,函数内部还是引用着旧的值
- 函数没有随着依赖值的变化而更新
解决方案
使用
useCallback
钩子,并正确设置依赖项:// ✅ 正确示例 const MyComponent = () => { const [data, setData] = useState(null); const handleData = useCallback(() => { console.log(data); // 会输出最新的值 }, [data]); // 添加依赖项 useEffect(() => { eventListener.on('ready', handleData); }, [handleData]); // 注意这里也要添加依赖项 };
核心要点
- 使用
useCallback
包装函数
- 在依赖数组中添加所有被函数使用的状态和属性
- 在使用这个函数的
useEffect
中也要添加函数作为依赖项
如何识别这个问题
- 函数输出的值总是初始值
- 状态更新了,但相关函数还在使用旧的值
- 特别是在事件监听器、定时器等异步操作中更容易遇到
预防措施
- 对于需要访问最新状态的函数,始终考虑使用
useCallback
- ESLint 的
exhaustive-deps
规则可以帮助检查依赖项是否完整
- 在开发时多关注 React DevTools 中的组件重渲染情况
🗒️ React
基础
💡 组件重新渲染
- 何时会?
- 状态或属性改变时(state, atom, props…)
- 何时不会?
- onClick 本身不会,除非在里面改了状态或者传递的属性。
- tips:
- 不要在一次渲染过程中,对状态进行多次改变,否则会陷入无限循环。例如在一个组件里,对同一个 atom 先置空,再设置真实内容。
💡 实现「指定时间做指定事件」的几种方法
- setTimeout + useState
- 性能:由于此方法仅在每个最接近的时间点前设置一个定时器,而不是持续运行,因此性能开销相对较小。这种策略没有不必要的检查和计算,特别是当时间点列表很长或者组件复杂时,这个差距更加明显。
- 准确度:对时间点的检查是基于实际需要设置的
setTimeout
,理论上它可以非常准确地在指定时间点触发。然而,setTimeout
的准确度可能会受到JavaScript运行环境的限制,因此它可能会有轻微的延迟。
在最近的时间点设置单个
setTimeout
,到达指定时间点时触发回调。一旦触发,如果有更多的时间点,组件将重新计算下一个最近的时间点并设置新的setTimeout
。- setInterval + atom
- 性能:这种方法通过每秒运行一次的
setInterval
定期更新时间原子,并在每次更新时检查所有提供的时间点。虽然每次检查本身可能开销不大,但这种持续的检查是不区分是否接近任意时间点的,这意味着即使没有即将到来的时间点,它也会持续运行。在很多时间点或更长的运行时间里,这可能导致不必要的性能开销。 - 准确度:准确度上,由于使用
setInterval
可能受到各种因素的影响,如定时器延迟、浏览器的任务调度策略等,它可能不如预设setTimeout
触发的准确。不过,对于非高精度要求的应用,这种方法依然是可接受的。
通过一个每秒更新的原子来持续监控时间,并检查是否有时间点匹配当前时间,然后触发回调。
Hook
如果使用了引用(例如
useRef
)在自定义Hook中跟踪状态,并且在同一个组件中多次使用该Hook,则每个Hook实例将共享相同的引用,这可能导致状态污染。💡 useRef 生命周期
useRef
在组件的整个生命期内不会变化,所以当组件重新渲染但不卸载时,useRef
中的值不会重置。(如果要重置,可以考虑使用useState
,它在每次渲染时都会提供最新的状态,并且能够通过设定新的状态来触发组件的重新渲染。)🗒️ GIT
规范
- feat: 新功能
- fix: 在提测或者上线之后修复的bug
- docs: 仅仅修改了文档.如:README,CHANGELOG等
- style: 修改代码风格.如:修改了缩进,空格,逗号;增加,修改,删除了注释;删除多余的文件;删除console.log等
- refactor: 代码重构,没有新增功能也没有修复bug
- pref: 性能优化
- test: 修改测试用例。如单元测试,集成测试等
- revert: 回滚到某个版本
- chore: 改变构建流程,增加了依赖库或修改了配置文件等
💡 PUSH
注意事项
- 不要直接 push,因为可能关联的上游分支是 basic-flow。应该:
git push -u origin luke/some-feature:luke/some-feature
- 这里的
-u
是--set-upstream
的简写。
- 整个可以简写为
git push -u origin luke/some-feature
常用命令
- 推送分支
git push origin luke/bot:luke/xxxbot
- 将本地分支 luke/bot 推送到远程仓库, 并将其命名为 luke/xxxbot。origin 是远程仓库名称,第一个 luke/bot 是本地名称,第二个 luke/xxxbot 是远程名称。
- 查看 upstream
git branch -vv
- 重命名分支
git branch -m luke/xxx-xxx
- 如果当前就在要重命名的分支
git branch -m old-branch new-branch
- 如果当前不在要重命名的分支
我已经 commit 了一个东西,但我修改了一下,我怎么把修改的合并到刚刚那个 commit 里?
先添加到暂存区,然后直接 git commit —amend
Comments