React 编码规范

2,583 阅读6分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

规则来源于

  1. eslint-plugin-react 提供的规则,选择出有利于我们当前项目的部分;
  2. React 社区中的讨论。

愿望

  1. 每一条规则都应有详尽的解释、示例代码,能直观看到规则的作用、得知使用该规则的原因;
  2. 每一条规则都可以使用 eslint 进行自动化检查;
  3. 已有项目中完全没有违反的规则无需列出。

JSX

[强制] 没有子节点的组件使用自闭合语法(react/self-closing-comp)

解释:

JSX与HTML不同,所有元素均可以自闭合。

// Bad
<Foo></Foo>
<div></div>

// Good
<Foo />
<div />

[强制] 保持起始和结束标签在同一层缩进(react/jsx-wrap-multilines)

解释:

代码样式。

// Bad
class Message {
    render() {
        return <div>
            <span>Hello World</span>
        </div>;
    }
}

// Good
class Message {
    render() {
        return (
            <div>
                <span>Hello World</span>
            </div>;
        );
    }
}

[强制] 自闭合标签的/>前添加一个空格(react/jsx-space-before-closing)

解释:

代码样式。

// Bad
<Foo bar="bar"/>
<Foo bar="bar"  />

// Good
<Foo bar="bar" />

API

[强制] 禁止为继承自 PureComponent 的组件编写 shouldComponentUpdate 实现(react/no-redundant-should-component-update)

解释:

在 React 的实现中,PureComponent 并不直接实现 shouldComponentUpdate,而是添加一个 isReactPureComponent 的标记,由 CompositeComponent 通过识别这个标记实现相关的逻辑。因此在 PureComponent 上自定义 shouldComponentUpdate 并无法享受 super.shouldComponentUpdate 的逻辑复用,也会使得这个继承关系失去意义。

相关的 issue:github.com/facebook/re…

补充:

  1. 为了避免组件无效的 render 导致的性能开销使用 PureComponet 和 memo 是好的,但请记住它们也是存在开销的,请勿滥用。

相关的 issue:github.com/facebook/re…

  1. 使用 react-redux 中 connect 方法包裹的组件没有继承 PureComponent 或使用 memo 方法的必要。

相关的代码:github.com/reduxjs/rea…

  1. 可使用 React Developer Tools 中开启 Highlight updates when components render 设置项观察组件渲染情况。

[强制] 禁止使用 String 类型的 Refs(react/no-string-refs)

解释:

它已过时并可能会在 React 未来的版本被移除。

补充:

相关 issue:github.com/facebook/re…

String 类型的 Refs 存在的问题:

  1. 它要求 React 跟踪当前渲染的组件,这会导致 React 的执行稍微慢一些。
  2. 它不能像大多数人期望的那样使用“渲染回调(render callback)”模式。
class MyComponent extends Component {
  renderRow = (index) => {
    // 这将无法工作,ref 将被添加到 DataTable 上,而非 MyComponent 上:
    return <input ref={'input-' + index} />;

    // This would work though! Callback refs are awesome.
    return <input ref={input => this['input-' + index] = input} />;
  }
 
  render() {
    return <DataTable data={this.props.data} renderRow={this.renderRow} />
  }
}
  1. 它是不可组合的,也就是说,如果库在传递的子对象上放一个 ref,用户就不能再放一个 ref。回调引用是可组合的。

[强制] 避免使用不安全的生命周期函数 componentWillMount、componentWillReceiveProps、componentWillUpdate(react/no-unsafe)

解释:

以上生命周期函数被 React 官方视为是不安全的,公司 react 规范中推荐使用 constructor 代替 componentWillMount,具体生命周期迁移参考『迁移过时的生命周期』。

备注:

React 官方视为以上生命周期不安全的原因是:对于 Concurrent 模式(实验性)这个未来特性,避免在 willMount / willUpdate 等生命周期挂钩中产生副作用非常重要。因为 React 当前的渲染分为 reconcile 和 commit 两个阶段,reconcile 阶段可以被高权重用户事件中端导致重复执行,由于以上生命周期函数在此阶段中被调用,导致这些生命周期函数存在被重复调用的可能。

React RFC 0006-static-lifecycle-methods:github.com/reactjs/rfc…

React v16.9.0 更新日志:react.docschina.org/blog/2019/0…

迁移过时的生命周期:react.docschina.org/blog/2018/0…

Concurrent 模式介绍 (实验性):react.docschina.org/docs/concur…

(:з[__] Dan 宝在 stackoverflow 上的回答 - 在 React 中,我应该在 componentWillMount 还是 componentDidMount 中进行初始网络请求?stackoverflow.com/a/41612993

[强制] 禁止使用数组索引作为 key(react/no-array-index-key)

解释:

React 使用 key 来判断哪些元素已经改变、添加或删除。

补充:

在此多说一些,我想一定有人和我之前的一样对 key 存在这样的误解,key 的目的是为了更好的性能。不,这不是事实。

key 的主要目的是作为唯一标识,在 fiber diff 阶段能够正确地判断出元素改变、添加或删除状态,尤其是对自身拥有状态的组件,这非常重要。

而在此前提下,对于属性没有变更的组件,只有实现了 shouldComponentUpdate / memo 方法,才可以避免重复渲染。

相关 issue:github.com/facebook/re…

Understanding the key prop: stackoverflow.com/questions/2…

// Bad
function Posts() {
	const [list, setList] = useState([]);
	useEffect(() => {
		fetchPosts().then(setList);
	}, []);
	return list.map((post, index) => <input key={index} defaultValue={post.title} />);
}

// Good
function Posts() {
	const [list, setList] = useState([]);
	useEffect(() => {
		fetchPosts().then(setList);
	}, []);
	return list.map(post => <input key={post.id} defaultValue={post.title} />);
}


// Good - 如果没有 id 等唯一标识,可以由前端主动生成唯一标识
function Posts() {
	const [list, setList] = useState([]);
	useEffect(() => {
		fetchPosts().then(list => {
			for (const post of list) {
				post.id = SomeLibrary.generateUniqueID();
			}
			setList(list);
		});
	}, []);
	return list.map(post => <input key={post.id} defaultValue={post.title} />);
}

[建议] 避免在JSX的属性值中直接使用对象和函数表达式(react/jsx-no-bind)

解释:

PureComponent 使用 shallowEqual 对 props 和 state 进行比较来决定是否需要渲染,而在 JSX 的属性值中使用对象、函数表达式会造成每一次的对象引用不同,从而 shallowEqual 会返回 false,导致不必要的渲染。

补充:

函数组件中为避免子组件刷新,可使用 useCallback 和 useMemo 等 hook 来保持函数、对象类型的属性引用不变。

// Bad
class WarnButton {
    alertMessage(message) {
        alert(message);
    }
    render() {
        return <button type="button" onClick={() => this.alertMessage(this.props.message)}>提示</button>
    }
}

// Good
class WarnButton {
    @bind()
    alertMessage() {
        alert(this.props.message);
    }
    render() {
        return <button type="button" onClick={this.alertMessage}>提示</button>
    }
}

// Bad
function Foo() {
	async function handleOk() {
		try {
        	await fetch();
		} catch (error) {
			message.error('error');
			return;
		}
		message.success('success');
    }
	return <Modal onOk={handleOk} />;
}

// Good 
function Foo() {
	const handleOk = useCallback(async () => {
		try {
        	await fetch();
		} catch (error) {
			message.error('error');
			return;
		}
		message.success('success');
    }, []);

	return <Modal onOk={handleOk} />;
}

[建议] 禁止在函数组件上使用 defaultProps(react/require-default-props,ignoreFunctionalComponents: true)

解释:

defaultProps 在类中非常有用,因为 props 对象被传递给许多不同的方法,如生命周期、回调等。每一个都有自己的作用域。这导致使用 JS 默认参数变得困难,因为您必须在每个函数中反复确定相同的默认值。

class Foo {
  static defaultProps = {foo: 1};
  componentDidMount() {
    let foo = this.props.foo;
    console.log(foo);
  }
  componentDidUpdate() {
    let foo = this.props.foo;
    console.log(foo);
  }
  componentWillUnmount() {
    let foo = this.props.foo;
    console.log(foo);
  }
  handleClick = () => {
    let foo = this.props.foo;
    console.log(foo);
  }
  render() {
    let foo = this.props.foo;
    console.log(foo);
    return <div onClick={this.handleClick} />;
  }
}

但是,在函数组件中,实际上不需要这种模式,因为您可以只使用 JS 默认参数,并且通常这些值的所有使用的位置都在同一作用域内。

function Foo({foo = 1}) {
  useEffect(() => {
    console.log(foo);
    return () => {
      console.log(foo);
    };
  });
  let handleClick = () => {
    console.log(foo);
  };
  console.log(foo);
  return <div onClick={handleClick} />;
}

React 团队之后的计划为,当在没有 .prototype.isReactComponent 的组件上使用 defaultProps 时,createElement 将发出警告。这包括那些特殊的组件,如 forwardRefmemo

如果 props 整体进行传递,那么升级将变得困难,不过你总是可以在需要时对它进行重构。

function Foo({foo = 1, bar = "hello"}) { let props = {foo, bar}; //... } 补充:

React RFC 0000-create-element-changes:0000-create-element-changes.md/ github.com/reactjs/rfc…

[注意] 单页应用中应避免使用 location.href = 'url to jump' 进行应用内部跳转

解释:

location.href = 'url to jump' 会导致页面刷新导致重新请求静态资源。

// Bad
function Foo() {
	const handleClick = useCallback(() => {
		location.href = '/path/to/jump';
	});
	return <div onClick={handleClick}>点击</div>;
}


// Good
function Foo() {
    const history = useHistory();
	const handleClick = useCallback(() => {
        // 在 react-router 中使用其 history 对象进行跳转
		history.push('/path/to/jump');
	});
	return <div onClick={handleClick}>点击</div>;
}