[DOC]
在工作中总结的 react 项目代码规范。有些是最佳实践,有些是自己喜欢的代码风格。
没有子元素的标签应该使用单标签闭合,并在末尾标签留一个空格
例
// bad
return <App></App>;
// good
return <App />;
当 JSX 有多行的时候,要用括号包裹 JSX 标签
例
// bad
return <div>
<span>要用括号包裹 JSX 标签<span>
<div>;
// good
return (
<div>
<span>要用括号包裹 JSX 标签<span>
<div>
);
标签内属性,要么都放在一行,要么每个属性各占一行
// bad
return (
<div className="some" onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
第一行有两个属性,后面的属性各占一行,难看
</div>
);
// good
return (
<div className="some"
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
美观
</div>
);
当使用列表渲染的时候,应该加 key,并且如果不确定是固定的列表不要以 index 作为 key
例
// bad
<ul>{list.map((item, index) => <li key={index}>{item.title}</li>)}</ul>
// good
<ul>{list.map((item) => <li key={item.id}>{item.title}</li>)}</ul>
不要以 a 标签 href="javascript:;" 的形式作为按钮,此种写法未来将在 React 中抛出错误,不论从语义性还是安全性考虑都应该使用 button
// bad
<a href="javascript:;" onClick={click}> 按钮1 </a>
// good
<button onClick={clickHandle}> 按钮2 </button>
不要在 render 方法内或者第一次渲染完成之前 修改 state
例:
// bad
class App extends Component {
public componentWillMount() {
this.setState({count: 2});
}
public render() {
return <div>{this.state.count}</div>;
}
}
// good
class App extends Component {
public componentDidMount() {
this.setState({count: 2});
}
public render() {
return <div>{this.state.count}</div>;
}
}
在注册销毁比较频繁的组件内,更新 state 之前先判断该组件是否已被卸载
例:
// 父组件
const Parent = () => {
const [showChild, setShowChild] = useState(true);
useEffect(() => {
setTimeout(() => {
setShowChild(false);
}, 1000);
}, []);
if (!showChild) {
return null;
}
return <Child />;
};
// bad
const Child = () => {
const [count, setCount] = useState(1);
useEffect(() => {
setTimeout(() => {
setCount(2); // 此时虽然 Child 组件已经被卸载,但该 setCount 仍然会被塞进任务队列,造成报错与性能问题
}, 2000);
}, []);
return <div>{count}</div>;
};
// good
export const useMethodWhileMounted = (method: (...args: any[]) => any) => {
const isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
});
return (...args: any[]): any => {
if (isMountedRef.current) {
return method(...args);
}
};
};
const Child = () => {
const [count, setCount] = useState(1);
const callback = useMethodWhileMounted((count) => setCount(count));
useEffect(() => {
setTimeout(() => {
callback(2);
}, 2000);
}, []);
return <div>{count}</div>
};
不要使用官方不推荐的生命周期
- componentWillMount
- componentWillReceiveProps,可以用
getDerivedStateFromProps代替 - componentWillUpdate
不要在 jsx 表达式内使用 map 渲染, 可读性差&不美观
例
const list = [1, 2, 3];
// bad render() {
return (
<ul>
{list.map((item) => (<li key={item.id}>{item.title}</li>))}
</ul>
);
// good
const renderItem = (item) => {
return <li key={item.id}>{item.title}</li>;
};
return (
<ul>
{list.map(renderItem)}
</ul>
);
使用 react.memo 包裹函数组件以提高性能,对应 class 组件应该尽量使用 PureComponent 而非 Component
例
// good
const SomeComponent = react.memo((props) => {
return (
<div>{props.data}</div>
);
});
在 class 组件中定义方法的时候,应该使用箭头函数
例
// so bad 每次 Button 都会重新渲染
class App extends PureComponent {
clickHandle() {
alert(this.state);
}
render() {
return <Button onClick={this.clickHandle.bind(this)}>{this.state}</Button>;
}
}
// bad 虽然引用没有问题,但代码太啰嗦
class App extends PureComponent {
constructor() {
this.clickHandle = this.clickHandle.bind(this);
}
clickHandle() {
alert(this.state);
}
render() {
return <Button onClick={this.clickHandle}>{this.state}</Button>;
}
}
// good 代码简洁 && clickHandle 实例方法引用不会变, App state 更新不会引发 Button 重绘
class App extends PureComponent {
clickHandle = () => {
alert(this.state);
}
render() {
return <Button onClick={this.clickHandle}>{this.state}</Button>;
}
}
应该用 renderProps 的形式使用 Context, 同时 value 应该做缓存
例
// bad 这种方式每次 state 发生变化整个组件树会重新渲染
const App = () => {
return (
<App.Provider value={{state, dispatch}}>
<Header />
<Content />
</App.Provider>
);
};
// good 因为 props.children 由外部传递进来,header 依赖的 state 发生变化不会引发 Content 的重绘
const AppProvider = () => {
const value = useMemo({state, dispatch}, [state]);
return (
<App.Provider value={value}>
{props.children}
</App.Provider>
);
};
const App = () => {
return (
<AppProvider>
<Header />
<Content />
</AppProvider>
);
};
基础组件应该尽量编写无状态受控组件
例
// bad 内部有状态管理 && 接受外部状态的改变,逻辑混乱
class Checkbox extend PureComponent {
getDerivedStateFromProps = (nextProps) => {
const nextChecked = nextProps.isChecked;
if (nextChecked !== this.state.isChecked) {
return {isChecked: nextChecked};
}
return null;
}
onChange = () => {
this.setState(!this.state.isChecked);
}
render() {
<input
type="checkbox"
checked={this.state.isChecked}
onChange={this.onChange}
/>
}
}
// good 没有内部状态,只接受 value && 向外暴露 change 回调
class Checkbox extend PureComponent {
render() {
<input
type="checkbox"
checked={this.props.isChecked}
onChange={this.props.onChange}
/>
}
}
如果需要将某一个方法传递给子组件,应该用 useCallback 包装
例
// bad 每次 Parent 更新状态 Child 也会跟着重新渲染,因为 callback 的引用变了,对于 Child 来说是一个新的函数
const Parent = () => {
const callback = (value) => alert(value);
return <Child callback={callback}/>;
};
// good
const Parent = () => {
const callback = useCallback((value) => alert(value), []);
return <Child callback={callback}/>;
};
如果 useCallback 里的 deps 较多或 deps 更新频率较频繁,应避免使用 callback
例:
// bad 依赖更新频繁会导致 foo 缓存失效。deps 过多增加了代码的复杂度,method 变得不可控,容易出现未知问题
const method = useCallback(() => {
// do something
}, [dep1, dep2, dep3, dep4]);
// good 利用对象引用来缓存函数, 引用不会被改变,而且在 callback 里可以拿到最新 state
const useMethods = (methods: {[key: string]: (...args: any[]) => any}) => {
const { current } = useRef({
methods,
proxyFunc: undefined,
});
current.methods = methods;
if (!current.proxyFunc) {
const proxyFunc = Object.create(null);
Object.keys(methods).forEach((key) => {
proxyFunc[key] = (...args: unknown[]) => current.methods[key].call(current.methods, ...args);
});
current.proxyFunc = proxyFunc;
}
return current.proxyFunc;
}
const methods = useMethods({
method1() {
// do something
},
});