--- title: 最佳实践 icon: star --- 本文档概述了在前端工作时应遵循的最佳实践。 ## 状态管理 React 和 Jotai 在代码库中处理状态管理。 ### 使用 Jotai 原子来存储状态 根据需要创建足够多的原子来存储你的状态,是一种良好实践。 与其为了通过 props 层层传递而过度精简,不如使用额外的原子。 ```tsx import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; export const myAtomState = createAtomState({ key: 'myAtomState', defaultValue: 'default value', }); export const MyComponent = () => { const [myAtom, setMyAtom] = useAtomState(myAtomState); return (
setMyAtom(e.target.value)} />
); } ``` ### 不要使用 `useRef` 来存储状态 避免使用 `useRef` 来存储状态。 如果想存储状态,应该使用 `useState` 或配合 `useAtomState` 的 Jotai 原子。 如果您觉得需要通过使用 `useRef` 来防止一些再渲染,请了解[如何管理再渲染](#managing-re-renders)。 ## 管理再渲染 在 React 中管理再渲染可能很困难。 这里有一些规则以帮助避免不必要的再渲染。 请记住,通过了解其原因,可以**始终**避免再渲染。 ### 在根级别工作 通过从根级消除它们,现在在新功能中避免再渲染变得容易。 `PageChangeEffect` sidecar 组件只包含一个 `useEffect` ,它持有页面更改时要执行的所有逻辑。 这样您就知道只有一个地方可以触发再渲染。 ### 在代码库中添加 `useEffect` 之前总是考虑一次 重新渲染往往是由不必要的“使用效果”造成的。 您应该想一想您是否需要`useEffect`,或者您是否可以在事件处理函数中移动逻辑。 你会发现移动一个 "handleClick" 或 "handleChange" 函数中的逻辑通常很容易。 您还可以在如 Apollo 的库中找到它们:`onCompleted`,`onError` 等。 ### 使用一个兄弟组件提取`useEffect`或数据获取逻辑。 如果你觉得需要在你的根组件中添加 "useEffect" ,你应该考虑在侧边组件中提取它。 对于数据获取逻辑也可以采用相同的方法,使用 Apollo hooks。 ```tsx // ❌ Bad, will cause re-renders even if data is not changing, // because useEffect needs to be re-evaluated export const PageComponent = () => { const [data, setData] = useAtomState(dataState); const [someDependency] = useAtomState(someDependencyState); useEffect(() => { if(someDependency !== data) { setData(someDependency); } }, [someDependency]); return
{data}
; }; export const App = () => ( ); ``` ```tsx // ✅ Good, will not cause re-renders if data is not changing, // because useEffect is re-evaluated in another sibling component export const PageComponent = () => { const [data, setData] = useAtomState(dataState); return
{data}
; }; export const PageData = () => { const [data, setData] = useAtomState(dataState); const [someDependency] = useAtomState(someDependencyState); useEffect(() => { if(someDependency !== data) { setData(someDependency); } }, [someDependency]); return <>; }; export const App = () => ( <> ); ``` ### 使用原子族状态和选择器 原子族状态和选择器是避免重新渲染的好方法。 当您需要存储项目列表时,它们很有用。 ### 您不应使用 `React.memo(MyComponent)` 避免使用 `React.memo()` 因为它不能解决再渲染的原因,而是中断了再渲染链,可能导致意外行为并使代码难以重构。 ### 限制 `useCallback` 或 `useMemo` 的使用 它们通常没有必要,会使代码难以阅读和维护,而性能提升难以察觉。 ## Console.logs `console.log` 语句在开发过程中非常有价值,提供关于变量值和代码流的实时洞察。 但是,将它们留在生产代码中可能导致几个问题: 1. **性能**:过多的日志记录会影响运行时性能,尤其是在客户端应用程序中。 2. **安全性**:记录敏感数据可能会将重要信息暴露给任何检查浏览器控制台的人。 3. **清洁度**:在控制台中填满日志可能会遮蔽开发人员或工具需要查看的重要警告或错误。 4. **专业性**:检查控制台的最终用户或客户看到数量众多的日志语句,可能会质疑代码的质量和精致度。 确保在提交代码到生产环境前移除所有 `console.logs`。 ## 命名 ### 变量命名 变量名称应该准确描述变量的用途或功能。 #### 泛化的名称问题 在编程中使用泛化的名称不是理想的,因为缺乏具体性,导致模糊性和降低代码的可读性。 这种名称无法传达变量或函数的目的,使得开发人员在不进行深入调查的情况下难以理解代码的意图。 这样会导致调试时间增加、错误的易发性提高以及在维护和协作中遇到困难。 同时,描述性的命名使代码自解释,更易于导航,提高代码质量和开发人员的生产力。 ```tsx // ❌ Bad, uses a generic name that doesn't communicate its // purpose or content clearly const [value, setValue] = useState(''); ``` ```tsx // ✅ Good, uses a descriptive name const [email, setEmail] = useState(''); ``` #### 变量名称中应避免使用的一些词 * 空壳 ### 事件处理 事件处理器名称应以 `handle` 开头,而 `on` 是用于命名组件属性事件的前缀。 ```tsx // ❌ Bad const onEmailChange = (val: string) => { // ... }; ``` ```tsx // ✅ Good const handleEmailChange = (val: string) => { // ... }; ``` ## 可选属性 避免为可选属性传递默认值。 **示例** 参见下面定义的 `EmailField` 组件: ```tsx type EmailFieldProps = { value: string; disabled?: boolean; }; const EmailField = ({ value, disabled = false }: EmailFieldProps) => ( ); ``` **用法** ```tsx // ❌ Bad, passing in the same value as the default value adds no value const Form = () => ; ``` ```tsx // ✅ Good, assumes the default value const Form = () => ; ``` ## 组件作为属性 尽可能将未实例化的组件作为属性传递,以便子组件可以自行决定需要传递哪些属性。 最常见的例子是图标组件: ```tsx const SomeParentComponent = () => ; // In MyComponent const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => { const theme = useTheme(); return (
) }; ``` 为了让 React 理解一个组件就是一个组件,需要使用 PascalCase,然后用 `` 来实例化它。 ## 属性传递:保持简约 在 React 中,属性传递是指通过多个组件层传递状态变量及其设置器,即使中介组件不使用它们。 虽然有时是必要的,过多的属性传递可能会导致: 1. **可读性降低**:在深嵌套组件结构中追踪属性的来源或使用位置可能会变得复杂。 2. **维护挑战**:一个组件的属性结构变化可能需要在多个组件中进行调整,即便它们不直接使用该属性。 3. **组件重用性降低**:一个仅接受众多属性用于向下传递的组件变得不那么通用,且在不同上下文中更难以重用。 如果您觉得使用了过多的属性传递,请参见[状态管理最佳实践](#state-management)。 ## 导入 导入时,选择指定的别名而不是指定完整或相对路径。 **句柄别名** ```js { alias: { "~": path.resolve(__dirname, "src"), "@": path.resolve(__dirname, "src/modules"), "@testing": path.resolve(__dirname, "src/testing"), }, } ``` **用法** ```tsx // ❌ Bad, specifies the entire relative path import { CatalogDecorator } from '../../../../../testing/decorators/CatalogDecorator'; import { ComponentDecorator } from '../../../../../testing/decorators/ComponentDecorator'; ``` ```tsx // ✅ Good, utilises the designated aliases import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator'; import { ComponentDecorator } from 'twenty-ui/testing'; ``` ## 模式验证 [Zod](https://github.com/colinhacks/zod) 是用于无类型对象的模式验证器: ```js const validationSchema = z .object({ exist: z.boolean(), email: z .string() .email('Email must be a valid email'), password: z .string() .regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'), }) .required(); type Form = z.infer; ``` ## 重大变更 在进行下一步之前,始终进行彻底的手动测试,以确保修改没有在其他地方造成干扰,因为测试尚未全面集成。