使用 lint 自动化格式工具
eslint、stylelint、prettier 等
import 顺序
添加 eslint 等配置,使导入按照一定规则分组。
JS
{'import/order': [2,{'newlines-between': 'always',groups: ['builtin','external','internal','parent','sibling','index','unknown','object','type',],alphabetize: {order: 'asc',caseInsensitive: true,},pathGroups: [{pattern: 'react*',group: 'external',position: 'before',},],},],}
命名规则
大驼峰-Component、interface、type 别名
TSX
// React componentconst BannersEditForm = () => {...}// Typescript interfaceinterface TodoItem {id: number;name: string;value: string;}// Typescript type aliastype TodoList = TodoItem[];
小驼峰-变量
TS
const getLastDigit = () => { ... }const userTypes = [ ... ]
文件夹和非组件文件小驼峰、组件大驼峰
TS
src / utils / form.ts;src / hooks / useForm.ts;src / components / banners / edit / Form.tsx;
避免 default export
默认导出不会将任何名称与所导出的项目相关联,这意味着可以在导入期间重新命名组件。这可能很灵活,但如果命名不一致,也很困惑。按照组件定义者的命名导入,可以减少歧义。
TS
// ❌export default MyComponent;// ✅export { MyComponent };export const MyComponent = ...;export type MyComponentType = ...;
组件的结构
TS
// 1. Imports - 导入结构,经可能减少代码import React, { PropsWithChildren, useState, useEffect } from 'react';// 2. Typestype ComponentProps = {someProperty: string;};// 3. Styles - with @mui use styled API or sx prop of the componentconst Wrapper = styled('div')(({ theme }) => ({color: theme.palette.white}));// 4. Additional variablesconst SOME_CONSTANT = 'something';// 5. Componentfunction Component({ someProperty }: PropsWithChildren<ComponentProps>) {// 5.1 Definitionsconst [state, setState] = useState(true);const { something } = useSomething();// 5.2 Functionsfunction handleToggleState() {setState(!state);}// 5.3 Effects// ❌React.useEffect(() => {// ...}, []);// ✅useEffect(() => {// ...}, []);// 5.5 Additional destructuresconst { property } = something;return (<div>{/* Separate elements if not closed on the same line to make the code clearer */}{/* ❌ */}<div><div><p>Lorem ipsum</p><p>Pellentesque arcu</p></div><p>Lorem ipsum</p><p>Pellentesque arcu</p></div><div><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesquearcu. Et harum quidem rerum facilis est et expedita distinctio.</p><p>Pellentesque arcu</p><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Pellentesquearcu. Et harum quidem rerum facilis est et expedita distinctio.</p></div>{/* ✅ */}<Wrapper><div><p>Lorem ipsum</p><p>Pellentesque arcu</p></div><p>Lorem ipsum</p><p>Pellentesque arcu</p></Wrapper><div><div><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Pellentesque arcu. Et harum quidem rerum facilis est et expeditadistinctio.</p><p>Pellentesque arcu</p><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Pellentesque arcu. Et harum quidem rerum facilis est et expeditadistinctio.</p></div></div></div>);}// 6. Exportsexport { Component };export type { ComponentProps };
使用 PropsWithChildren
TS
import React, { PropsWithChildren } from 'react';type ComponentProps = {someProperty: string;};// ✅function Component({someProperty,children}: PropsWithChildren<ComponentProps>) {// ...}
超过一行则提取函数
TS
// ❌<buttononClick={() => {setState(!state);resetForm();reloadData();}}/>// ✅<button onClick={() => setState(!state)} />// ✅const handleButtonClick = () => {setState(!state);resetForm();reloadData();}<button onClick={handleButtonClick} />
避免使用 index 作为 key 属性值
TS
// ❌const List = () => {const list = ['item1', 'item2', 'item3'];return (<ul>{list.map((value, index) => {return <li key={index}>{value}</li>;})}</ul>);};// ✅const List = () => {const list = [{ id: '111', value: 'item1' },{ id: '222', value: 'item2' },{ id: '333', value: 'item3' }];return (<ul>{list.map((item) => {return <li key={item.id}>{item.value}</li>;})}</ul>);};
使用 Fragment 标签
TS
// ❌const ActionButtons = ({ text1, text2 }) => {return (<div><button>{text1}</button><button>{text2}</button></div>);};// ✅const Button = ({ text1, text2 }) => {return (<><button>{text1}</button><button>{text2}</button></>);};
推荐解构 props
TS
// ❌const Button = (props) => {return <button>{props.text}</button>;};// ✅const Button = (props) => {const { text } = props;return <button>{text}</button>;};// ✅const Button = ({ text }) => {return <button>{text}</button>;};
逻辑 与 渲染分离
自定义 hooks
职责分离
TS
// ❌const ScreenDimensions = () => {const [windowSize, setWindowSize] = useState({width: undefined,height: undefined});useEffect(() => {function handleResize() {setWindowSize({width: window.innerWidth,height: window.innerHeight});}window.addEventListener('resize', handleResize);handleResize();return () => window.removeEventListener('resize', handleResize);}, []);return (<><p>Current screen width: {windowSize.width}</p><p>Current screen height: {windowSize.height}</p></>);};// ✅const useWindowSize = () => {const [windowSize, setWindowSize] = useState({width: undefined,height: undefined});useEffect(() => {function handleResize() {setWindowSize({width: window.innerWidth,height: window.innerHeight});}window.addEventListener('resize', handleResize);handleResize();return () => window.removeEventListener('resize', handleResize);}, []);return windowSize;};const ScreenDimensions = () => {const windowSize = useWindowSize();return (<><p>Current screen width: {windowSize.width}</p><p>Current screen height: {windowSize.height}</p></>);};
避免大组件
TS
// ❌const SomeSection = ({ isEditable, value }) => {if (isEditable) {return (<Section><Title>Edit this content</Title><Content>{value}</Content><Button>Clear content</Button></Section>);}return (<Section><Title>Read this content</Title><Content>{value}</Content></Section>);};// ✅const EditableSection = ({ value }) => {return (<Section><Title>Edit this content</Title><Content>{value}</Content><Button>Clear content</Button></Section>);};const DetailSection = ({ value }) => {return (<Section><Title>Read this content</Title><Content>{value}</Content></Section>);};const SomeSection = ({ isEditable, value }) => {return isEditable ? (<EditableSection value={value} />) : (<DetailSection value={value} />);};
尽可能状态组合管理
TS
// ❌const [username, setUsername] = useState('');const [password, setPassword] = useState('');// ✅const [user, setUser] = useState({});
使用 boolean 属性简写
TS
// ❌<Form hasPadding={true} withError={true} />// ✅<Form hasPadding withError />
避免字符串属性加花括号
TS
// ❌<Title variant={"h1"} value={"Home page"} />// ✅<Title variant="h1" value="Home page" />
避免使用行内样式
TS
// ❌const Title = (props) => {return (<h1 style={{ fontWeight: 600, fontSize: '24px' }} {...props}>{children}</h1>);};// ✅const useStyles = (props) => {return useMemo(() => ({header: { fontWeight: props.isBold ? 700 : 400, fontSize: '24px' }}),[props]);};const Title = (props) => {const styles = useStyles(props);return (<h1 style={styles.header} {...props}>{children}</h1>);};
使用三目运算符
使用三目运算符代替 简单的
条件判断。
TS
const { role } = user;// ❌if (role === ADMIN) {return <AdminUser />;} else {return <NormalUser />;}// ✅return role === ADMIN ? <AdminUser /> : <NormalUser />;
使用枚举
TS
// ❌if (role === 'admin') {return <AdminUser />;}// ✅enum Roles {admin = 'admin',basic = 'basic'}if (role === Roles.admin) {return <AdminUser />;}
第三方库统一管理
不直接使用第三方库,尽量在一个地方统一管理。
TS
//src/lib/store.tsexport { useDispatch, useSelector } from 'react-redux';
TS
// src/lib/query.tsexport { useQuery, useMutation, useQueryClient } from 'react-query';
推荐声明表达式
TS
// ❌ imperative: dealing with internals of array iterationconst arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];let sum = 0;for (let i = 0; i < arr.length; i++) {sum += arr[i];}// ✅ declarative: we don't deal with internals of iterationconst arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];const sum = arr.reduce((acc, v) => acc + v, 0);
使用描述性的变量名称
TS
// ❌ Avoid single letter namesconst n = 'Max';// ✅const name = 'Max';// ❌ Avoid abbreviationsconst sof = 'Sunday';// ✅const startOfWeek = 'Sunday';// ❌ Avoid meaningless namesconst foo = false;// ✅const appInit = false;
避免参数过多
TS
// ❌function createPerson(firstName, lastName, height, weight, gender) {// ...}// ✅function createPerson({ firstName, lastName, height, weight, gender }) {// ...}// ✅function createPerson(person) {const { firstName, lastName, height, weight, gender } = person;// ...}
使用对象解构
TS
// ❌return (<><div> {user.name} </div><div> {user.age} </div><div> {user.profession} </div></>);// ✅const { name, age, profession } = user;return (<><div> {name} </div><div> {age} </div><div> {profession} </div></>);
推荐模版字符串
TS
// ❌const userName = user.firstName + ' ' + user.lastName;// ✅const userDetails = `${user.firstName} ${user.lastName}`;
小功能中使用隐式返回
TS
// ❌const add = (a, b) => {return a + b;};// ✅const add = (a, b) => a + b;
使用封装函数
TS
// ❌ directly using momemtimport moment from 'moment';const updateProduct = (product) => {const payload = {...product,// ❌ we are bound to the moment interface implementationupdatedAt: moment().toDate(),};return await fetch(`/product/${product.id}`, {method: 'PUT',body: JSON.stringify(payload),});};// ✅ creating the abstraction, a.k.a. helper function which wraps the functionality// utils/createDate.tsimport moment from 'moment';export const createDate = (): Date => moment().toDate();// updateProduct.tsimport { createDate } from './utils/createDate';const updateProduct = (product) => {const payload = {...product,// ✅ using the abstracted helper functionupdatedAt: createDate(),};return await fetch(`/product/${product.id}`, {method: 'PUT',body: JSON.stringify(payload),});};view raw
组件控制器使用 HOC
Helper function — utils/wrap.ts
TS
import { FC, createElement } from 'react';export const wrap =<Props extends object, ViewProps extends object>(View: FC<Partial<ViewProps>>,controllers: Array<(props: Props) => Partial<ViewProps> | null | void>) =>(args: Props) =>createElement(View,...controllers.map((useController) => useController(args) as Partial<ViewProps> | null).filter(Boolean));
Page controller — useTodoController.ts
TS
import { useMemo } from 'react';import { useTodoList } from '../../adapters/todoAdapter';import { useUserData } from '../../adapters/userAdapter';export const useTodoController = () => {const { user } = useUserData();const {todos: { isLoading, data }} = useTodoList(user.id);return useMemo(() => ({ isLoading, todos: data }), [isLoading, data]);};
Page — TodoList.tsx
TS
import { useTodoController } from './useTodoController';import { TodoItem } from './components/TodoItem';import { wrap } from '../../utils/wrap';import { Todo, TodoList as TodoListType } from '../../../domain/struct/todo';type TodoListProps = {todos: TodoListType;isLoading: boolean;};const TodoListComponent = ({ todos, isLoading }: TodoListProps) => {return isLoading ? (<>Loading...</>) : (<><ul className="tasks-list">{todos.map((todo: Todo) => (<TodoItem key={todo.id} todo={todo} />))}</ul></>);};export const TodoList = wrap(TodoListComponent, [useTodoController]);
React 代码习惯和最佳实践@Gaspar Nagy