React 编程规范

139 阅读4分钟

[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
    },
});