React
命名规范
- 组件名称:
MyComponent
推荐使用大驼峰命名 - 属性名称:
onClick
React DOM 使用小驼峰命令来定义属性的名称,而不使用 HTML 属性名称的命名约定 - style 样式属性:
backgroundColor
采用小驼峰命名属性的 JavaScript 对象 - className名称:
test-main__content--blue
BEM命名规范(块:是什么、元素:是什么、修饰符:出外形行为状态等特征)
文件夹文件名
类型 | 规范 | 示例 |
---|---|---|
图片文件夹 | 小写开头驼峰 | src/assets/image/xxxXxx |
图片文件 | 小写中划线 | xxx-xxx.png |
公共组件components | 大写开头驼峰 | src/components/XxxXxx/index.tsx src/components/XxxXxx/index.less |
页面文件 | 大写开头驼峰 | src/pages/Xxx/index.tsx src/pages/Xxx/index.less src/pages/Xxx/components/XxxXxx/index.tsx |
全局状态 | 小写开头驼峰 | src/store/xxxXxx.ts |
常量 | 小写开头驼峰 | src/constants/xxxXxx.ts |
类型定义 | 小写开头驼峰 | src/types/xxxXxx.d.ts |
公共方法 | 小写开头驼峰 | src/utils/xxxXxx.ts |
单文件行数限制
单个文件最大不能超过500行,超过后需要拆分组件
函数组件和 class 类组件的使用场景
建议优先使用函数组件+hooks,组件开发
组件的代码顺序
组件应该有严格的代码顺序,这样有利于代码维护,我们推荐每个组件中的代码顺序一致性。
class Example extends Component {
// 静态属性
static defaultProps = {}
// 构造函数
constructor(props) {
super(props);
this.state={}
}
// 声明周期钩子函数
// 按照它们执行的顺序,部分钩子”过时”,不建议再使用。
// 1. componentWillMount(过时)
// 2. componentWillReceiveProps(过时)
// 3. getDerivedStateFromProps
// 4. shouldComponentUpdate
// 5. getSnapshotBeforeUpdate
// 6. componentDidMount
// 7. componentDidUpdate
// 8. componentWillUnmount
componentDidMount() { ... }
// 事件函数/普通函数
handleClick = (e) => { ... }
// 最后render 方法
render() { ... }
}
函数组件应将处理相同功能的代码,抽离到独立hook中,供组件使用
{
// 1. useRef
// 2. useState
// 3. useXxxHooks
// 4. useEffect 优先空依赖 - deps:[]
// 5. react其他内置hook
// 事件函数/普通函数,在useEffect外最好用useCallBack包一下
const handleClick = (e) => { ... }
// 最后返回render内容
}
跨组件通信
-
状态提升:如果多个组件需要反映相同的变化数据,建议将共享状态提升到最近的共同父组件中去;从而依靠自上而下的数据流,而不是尝试在不同组件间同步
state
。 -
「组件」推荐使用 Context,与项目降低耦合度:如果某个属性在组件树的不同层级的组件之间需要用到,我们应该使用
Context
提供在组件之间共享此属性的方式,而不是显式地通过组件树的逐层传递props
。 -
「页面」推荐使用 Redux:统一状态(与server交互数据、页面组件交互数据)管理,以业务模块作为命名空间划分,eg: auth权限, user用户 等。
Typescript
组件引入方式
函数组件
// 推荐使用✅ better
const WrapComponent: React.FC<ExtendedProps> = (props) => {
// return ...
};
// 直接使用
export default WrapComponent;
// 或者
export default function (props: React.PropsWithChildren<SpinProps>) {
// return ...
}
类组件
type IEProps {
Cp?: React.ComponentClass<{ id?: number }>;
}
type IEState { id: number; }
//推荐使用✅ better
class ClassCpWithModifier extends React.Component<IEProps, IEState> {
private gid: number = 1;
public state: Readonly<IEState> = { id: 1 };
render() { return this.state.id = 2; } // ts(2540)
}
两者均可使用的类型
React.ComponentType<P> = React.ComponentClass<P> | React.FunctionComponent<P>;
Element
onClick and onChange
// click 使用 React.MouseEvent 加 dom 类型的泛型
// HTMLInputElement 代表 input标签 另外一个常用的是 HTMLDivElement
const onClick = (e: React.MouseEvent<HTMLInputElement>) => {};✅ better
// onChange使用 React.ChangeEvent 加 dom 类型的泛型
// 一般都是 HTMLInputElement,HTMLSelectElement 可能也会使用
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {};✅ better
return (
<>
{'ProForm 设置泛型可以约定 onFinish 等接口的参数类型'}
<ProForm<DataType> />
{`
DataType 设置render 中行的类型,
Params 是参数的提交类型
ValueType 表示自定的 valueType 类型,ProTable 会自动进行合并
`}
<ProTable<DataType, Params, ValueType> />
<input onClick={onClick} onChange={onChange} />
</>
);
Forms and onSubmit
import * as React from 'react'
type changeFn = (e: React.FormEvent<HTMLInputElement>) => void
const App: React.FC = () => {
const [state, setState] = React.useState('')
const onChange: changeFn = e => {
setState(e.currentTarget.value)
}
// ✅ better
const onSubmit = (e: React.SyntheticEvent) => {
e.preventDefault()
const target = e.target as typeof e.target & {
password: { value: string }
} // 类型扩展
const password = target.password.value
}
return (
<form onSubmit={onSubmit}>
<input type="text" value={state} onChange={onChange} />
</form>
)
}
Hooks 使用
useState
//给定初始化值情况下可以直接使用
import { useState } from 'react';
// ...
const [val, toggle] = useState(false);
// val 被推断为 boolean 类型
// toggle 只能处理 boolean 类型
// 没有初始值(undefined)或初始 null
type AppProps = { message: string };
const App = () => {
const [data] = useState<AppProps | null>(null); // ✅ better
// const [data] = useState<AppProps | undefined>();
return <div>{data?.message}</div>;
};
setState 的更新可能是异步的
useEffect
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(() => {
const timer = setTimeout(() => {
/* do stuff */
}, timerMs);
// 可选
return () => clearTimeout(timer);
}, [timerMs]);
// ✅ 确保函数返回 void 或一个返回 void|undefined 的清理函数
return null;
}
//异步请求,处理方式:
// ✅ better
useEffect(() => {
(async () => {
const { data } = await ajax(params);
// todo
})();
}, [params]);
// 或者 then 也是可以的
useEffect(() => {
ajax(params).then(({ data }) => {
// todo
});
}, [params])
useRef
function TextInputWithFocusButton() {
// 初始化为 null, 但告知 TS 是希望 HTMLInputElement 类型
// inputEl 只能用于 input elements
const inputEl = React.useRef<HTMLInputElement>(null);
const onButtonClick = () => {
// TS 会检查 inputEl 类型,初始化 null 是没有 current 上是没有 focus 属性的
// 你需要自定义判断!
if (inputEl && inputEl.current) {
inputEl.current.focus();
}
// ✅ best
inputEl.current?.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useReducer
使用 useReducer 时,多多利用 Discriminated Unions 来精确辨识、收窄确定的 type 的 payload 类型。 一般也需要定义 reducer 的返回类型,不然 TS 会自动推导。
const initialState = { count: 0 };
// ❌ bad,可能传入未定义的 type 类型,或码错单词,而且还需要针对不同的 type 来兼容 payload
// type ACTIONTYPE = { type: string; payload?: number | string };
// ✅ good
type ACTIONTYPE =
| { type: 'increment'; payload: number }
| { type: 'decrement'; payload: string }
| { type: 'initial' };
function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload };
case 'decrement':
return { count: state.count - Number(action.payload) };
case 'initial':
return { count: initialState.count };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
<button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
</>
);
}
useContext
一般 useContext 和 useReducer 结合使用,来管理全局的数据流。
interface AppContextInterface {
state: typeof initialState;
dispatch: React.Dispatch<ACTIONTYPE>;
}
const AppCtx = React.createContext<AppContextInterface>({
state: initialState,
dispatch: (action) => action,
});
const App = (): React.ReactNode => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppCtx.Provider value={{ state, dispatch }}>
<Counter />
</AppCtx.Provider>
);
};
// 消费 context
function Counter() {
const { state, dispatch } = React.useContext(AppCtx);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
<button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
</>
);
}
自定义 Hooks
Hooks 的美妙之处不只有减小代码行的功效,重点在于能够做到逻辑与 UI 分离。做纯粹的逻辑层复用。 例子:当你自定义 Hooks 时,返回的数组中的元素是确定的类型,而不是联合类型。可以使用 const-assertions 。
//例子:当你自定义 Hooks 时,返回的数组中的元素是确定的类型,而不是联合类型。可以使用 const-assertions 。
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // 推断出 [boolean, typeof load],而不是联合类型 (boolean | typeof load)[]
}
//也可以断言成 tuple type 元组类型。
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as [
boolean,
(aPromise: Promise<any>) => Promise<any>
];
}
//如果对这种需求比较多,每个都写一遍比较麻烦,可以利用泛型定义一个辅助函数,且利用 TS 自动推断能力。
function tuplify<T extends any[]>(...elements: T) {
return elements;
}
function useArray() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return [numberValue, functionValue]; // type is (number | (() => void))[]
}
function useTuple() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {
}).current;
return tuplify(numberValue, functionValue); // type is [number, () => void]
}
泛型
当函数的入参和返回值都应该可以是任意类型时
function fn<T>(arg: T): T {
return arg
}
// 或
const fn1: <T>(arg: T) => T = arg => {
return arg
}
Partial
对象或接口属性变为可选
type Person = {
name:string,
age:number
}
type p = Partial<Person>
// type p = {
// name?: string;
// age?: number;
// }
Required
对象或接口属性变为必选
type Person = {
name:string,
age?:number
}
type p = Required<Person>
// type p = {
// name: string;
// age: number;
// }
Pick
提取类型的部分属性
type Person = {
name:string,
age:number
}
type A = Pick<Person,"name" | "xxx">
// type p = {
// name: string;
// }
Omit
忽略类型的部分属性
type Person = {
name:string,
age:number
}
type A = Omit<Person,"name" | "xxx">
// type p = {
// age: string;
// }
Readonly
所有属性设为只读
type Person = {
name:string,
age:number
}
type A = Readonly<Person>
// type p = {
// readonly name: string;
// readonly age: number;
// }
Exclude
从联合类型中去除指定的类型
Extract
从联合类型中提取指定的类型
NonNullable
从联合类型中去除 null 或者 undefined 的类型
Record
泛型约束
Type 和Interface的选用时机
-
在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口
-
在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type的约束性更强
-
type 类型不能二次编辑,而 interface 可以随时扩展
其他技巧
对象类型尽量使用Record<string, unknown> 代替{} 和object
type ObjectTypes = {
objBetter: Record<string, unknown>; // ✅ better,代替 obj: object
// 对于 obj2: {}; 有三种情况:
obj2Better1: Record<string, unknown>; // ✅ better 同上
obj2Better2: unknown; // ✅ any value
obj2Better3: Record<string, never>; // ✅ 空对象
/** Record 更多用法 */
dict1: {
[key: string]: MyTypeHere;
};
dict2: Record<string, MyTypeHere>; // 等价于 dict1
};
//好处:
//1.当你书写 home 值时,键入 h 常用的编辑器有智能补全提示;
//2.home 拼写错误成 hoem,会有错误提示,往往这类错误很隐蔽;
//3.收窄接收的边界。
函数类型不建议直接给 Function 类型,有明确的参数类型、个数与返回值类型最佳
type FunctionTypes = {
onSomething: Function; // ❌ bad,不推荐。任何可调用的函数
onClick: () => void; // ✅ better ,明确无参数无返回值的函数
onChange: (id: number) => void; // ✅ better ,明确参数无返回值的函数
onClick(event: React.MouseEvent<HTMLButtonElement>): void; // ✅ better
};
React Prop 类型
export declare interface AppProps {
children1: JSX.Element; // ❌ bad, 没有考虑数组类型
children2: JSX.Element | JSX.Element[]; // ❌ 没考虑字符类型
children3: React.ReactChildren; // ❌ 名字唬人,工具类型,慎用
children4: React.ReactChild[]; // better, 但没考虑 null
children: React.ReactNode; // ✅ best, 最佳接收所有 children 类型
functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点
style?: React.CSSProperties; // React style
onChange?: React.FormEventHandler<HTMLInputElement>; // 表单事件! 泛型参数即 `event.target` 的类型
}
使用查找类型访问组件属性类型
// Great
import Counter from './d-tips1'
type PropsNew = React.ComponentProps<typeof Counter> & {
age: number
}
const App: React.FC<PropsNew> = props => {
return <Counter {...props} />
}
export default App
Promise 类型
type IResponse<T> = {
message: string
result: T
success: boolean
}
async function getResponse(): Promise<IResponse<number[]>> {
return {
message: '获取成功',
result: [1, 2, 3],
success: true,
}
}
getResponse().then(response => {
console.log(response.result)
})
typeof/instanceof/in/is: 类型守卫用于类型区分
//typeof
function doSome(x: number | string) {
if (typeof x === 'string') {
// 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
console.log(x.subtr(1)); // Error: 'subtr' 方法并没有存在于 `string` 上
console.log(x.substr(1)); // ok
}
x.substr(1); // Error: 无法保证 `x` 是 `string` 类型
}
//instanceof
class Foo {
foo = 123;
common = '123';
}
class Bar {
bar = 123;
common = '123';
}
function doStuff(arg: Foo | Bar) {
if (arg instanceof Foo) {
console.log(arg.foo); // ok
console.log(arg.bar); // Error
}
if (arg instanceof Bar) {
console.log(arg.foo); // Error
console.log(arg.bar); // ok
}
}
doStuff(new Foo());
doStuff(new Bar());
//in
interface A {
x: number;
}
interface B {
y: string;
}
function doStuff(q: A | B) {
if ('x' in q) {
// q: A
} else {
// q: B
}
}
//is
function isString(test: any): test is string{
return typeof test === 'string';
}
function example(foo: number | string){
if(isString(foo)){
console.log('it is a string' + foo);
console.log(foo.length); // string function
}
}
example('hello world');
// is 为关键字的「类型谓语」把参数的类型范围缩小了,当使用了 test is string 之后,我们通过 isString(foo) === true 明确知道其中的参数是 string,而 boolean 并没有这个能力,这就是 is 关键字存在的意义.