JSX

一、可选的 props 和空对象 {}

遵循单一目的组件哲学,避免过于复杂的多行组件,并尽可能地将组件分解。
假设有一个简单的 <userCard/> 组件,此组件的唯一用途是接收一个 user 对象并显示相应的用户数据。
代码如下:

  1. import React from 'react';
  2. import propsTypes from 'props-types';
  3. const UserCard = ({ user }) => {
  4. return (
  5. <ul>
  6. <li>{user.name}</li>
  7. <li>{user.age}</li>
  8. <li>{user.email}</li>
  9. </ul>
  10. )
  11. }
  12. UserCard.propsTypes = {
  13. user: propsTypes.object
  14. }
  15. UserCard.defaultTypes = {
  16. user: {}
  17. }

这个组件需要一个 user props 并且它是组件的唯一数据源,但是这个 props 不是必须的(没有设置 isRequired),所以设置了一个默认值为 {},以避免Can not access property ‘name’ of... errors 等错误。
那么,如果没有为 <UserCard> 组件在等待渲染时提供一个回退值 (fallback),并且 UserCard 没有任何数据也没有任何逻辑运行,那么就没有理由呈现这个组件。
那么,props 什么时候是必需的,什么时候不是必需的呢?
有时候只需要问问自己什么才是最有意义的。
假设有一个转换货币的组件 <CurrencyConverter/>,它有三个 props:

  • value - 要转换的数值。
  • givenCurrency - 正在转换的货币。
  • targetCurrency - 要转换成的货币。

然而,如果 value 值不足,那么进行任何转换都毫无意义,那时根本不需要呈现组件。
因此,value props 肯定是必需的。

二、条件在父组件中呈现

有时候在一个子组件中经常会看到类似如下代码:

  1. import React, { useState } from "react";
  2. import PropTypes from "prop-types";
  3. // 子组件
  4. export const UserCard = ({ user }) => {
  5. const keys = Object.keys(user)
  6. return (
  7. keys.length ?
  8. <ul>
  9. <li>{user.name}</li>
  10. <li>{user.age}</li>
  11. <li>{user.email}</li>
  12. </ul>
  13. : "No Data"
  14. );
  15. };

一个组件带有一些逻辑,却徒劳地执行,只是为了显示一个 spinner 或一条信息。
针对这种情况,请记住,在父组件内部完成此操作总是比在组件本身内部完成更为干净。
按着这个原则,子组件和父组件应该像这样:

  1. import React, { useState } from "react";
  2. import PropTypes from "prop-types";
  3. // 子组件
  4. export const UserCard = ({ user }) => {
  5. return (
  6. <ul>
  7. <li>{user.name}</li>
  8. <li>{user.age}</li>
  9. <li>{user.email}</li>
  10. </ul>
  11. );
  12. };
  13. UserCard.propTypes = {
  14. user: PropTypes.object.isRequired
  15. };
  16. // 父组件
  17. export const UserContainer = () => {
  18. const [user, setUser] = useState(null);
  19. // do some apiCall here
  20. return (
  21. <div>
  22. {user && <UserCard user={user} />}
  23. </div>
  24. );
  25. };

通过这种方式,将 user 初始化为 null,然后简单的运行一个 falsy 检测,如果 user 不存在,!user 将返回 true。
如果设置为 {} 则不然,必须通过 Object.keys() 检查对象 key 的长度,通过不创建新的对象引用,节省了一些内存空间,并且只有在获得了所需的数据之后,才渲染子组件 <UserCard/>
如果没有数据,显示一个 spinner 也会很容易做到。

  1. export const UserContainer = () => {
  2. const [user, setUser] = useState(null);
  3. // do some apiCall here
  4. return (
  5. <div>
  6. {user ? <UserCard user={user} /> : 'No data available'}
  7. </div>
  8. );
  9. };

子组件 <UserCard/> 只负责显示用户数据,父组件 <UserContainer/> 是用来获取数据并决定呈现什么的。这就是为什么父组件是显示回退值(fallback)最佳位置的原因。

三、不满足时,及时 return

即使使用的是正常的编程语言,嵌套也是一团糟,更不用说 JSX(它是 JavaScript、 HTML 的混合体)了。
可能经常会看到使用 JSX 编写的类似的代码:

  1. const NestedComponent = () => {
  2. // ...
  3. return (
  4. <>
  5. {!isLoading ? (
  6. <>
  7. <h2>Some heading</h2>
  8. <p>Some description</p>
  9. </>
  10. ) : <Spinner />}
  11. </>
  12. )
  13. }

该怎么做才是更合理的呢?

  1. const NestedComponent = () => {
  2. // ...
  3. if (isLoading) return <Spinner />
  4. return (
  5. <>
  6. <h2>Some heading</h2>
  7. <p>Some description</p>
  8. </>
  9. )
  10. }

处理 render 逻辑时,在处理是否有可用的数据,页面是否正在加载,都可以选择提前 return。
这样就可以避免嵌套,不会把 HTML 和 JavaScript 混合在一起,而且代码对于不同技术水平或没有技术背景的人来说也是可读的。

四、在 JSX 中尽量少写 JavaScript

JSX 是一种混合语言,可以写JS代码,可以写表达式,可以写 HTML,当三者混合起来后,使用 JSX 编写的代码可读性就会差很多。
虽然有经验的人可以理解组件内部发生了什么,但并不是每个人都能理解。

  1. const CustomInput = ({ onChange }) => {
  2. return (
  3. <Input onChange={e => {
  4. const newValue = getParsedValue(e.target.value);
  5. onChange(newValue);
  6. }} />
  7. )
  8. }

正在处理一些外部对 input 的一些输入,使用自定义处理程序解析该输入 e.target.value,然后将解析后的值传给 <CustomInput/> 组件接收的 onChange prop。虽然这个示例可以正常工作,但是会让代码变得很难理解。
在实际项目中会有更多的元素和更复杂的 JS 逻辑,所以将逻辑从组件中抽离出来,会使 return() 更清晰。

  1. const CustomInput = ({ onChange }) => {
  2. const handleChange = (e) => {
  3. const newValue = getParsedValue(e.target.value);
  4. onChange(newValue);
  5. };
  6. return (
  7. <Input onChange={handleChange} />
  8. )
  9. }

当在 render 中返回 JSX 时,不要使用内联的 JavaScript 逻辑。

五、useCallback & useEffect

随着 v16.8 Hooks 问世后,人们开始大量使用函数组件,当使用函数进行编写组件时,如果需要在内部执行 API 接口的调用,需要用到 useEffect 生命周期钩子。 :::info useEffect 用于处理组件中的 effect,通常用于请求数据,事件处理,订阅等相关操作。 ::: 在最初版本的文档指出,防止 useEffect 出现无限循环,需要提供空数组 [] 作为 useEffect 依赖项,将使钩子只能在组件的挂载和卸载阶段运行。因此,会看到在很多使用 useEffect 的地方将 [] 作为依赖项传入。 :::info 使用 useEffect 出现无限循环的原因是,useEffect 在组件 mount 时执行,但也会在组件更新时执行。因为在每次请求数据之后基本上都会设置本地的状态,所以组件会更新,因此 useEffect 会再次执行,因此出现了无限循环的情况。 ::: 然而,这种处理方式就会出现 react-hooks/exhaustive-deps 规则的警告,因此代码中常常会通过注释忽略此警告。

  1. // eslint-disable-next-line react-hooks/exhaustive-deps
  2. import React, { useState, useEffect } from 'react'
  3. import { fetchUserAction } from '../api/actions.js'
  4. const UserContainer = () => {
  5. const [user, setUser] = useState(null);
  6. const handleUserFetch = async () => {
  7. const result = await fetchUserAction();
  8. setUser(result);
  9. };
  10. useEffect(() => {
  11. handleUserFetch();
  12. // 忽略警告
  13. // eslint-disable-next-line react-hooks/exhaustive-deps
  14. }, []);
  15. if (!user) return <p>No data available.</p>
  16. return <UserCard data={user} />
  17. };

最初,很多人认为这个警告毫无意义,从而选择进行忽略,而不去试图探索它是如何产生的。
其实,有些人没有意识到,handleUserFetch() 方法在组件每次渲染的时候都会重新创建(组件有多少次更新就会创建多少次)。
这就是为什么需要在 useEffect 中调用的方法上使用 useCallback的原因。通过这种方式,可以防止 handleUserFetch() 方法重新创建(除非其依赖项发生变化) ,因此这个方法可以用作 useEffect 钩子的依赖项,而不会导致无限循环。
上边的例子应该这样重写:

  1. import React, { useState, useEffect, useCalllback } from 'react'
  2. import { fetchUserAction } from '../api/actions.js'
  3. const UserContainer = () => {
  4. const [user, setUser] = useState(null);
  5. // 使用 useCallback 包裹
  6. const handleUserFetch = useCalllback(async () => {
  7. const result = await fetchUserAction();
  8. setUser(result);
  9. }, []);
  10. useEffect(() => {
  11. handleUserFetch();
  12. }, [handleUserFetch]); /* 将 handleUserFetch 作为依赖项传入 */
  13. if (!user) return <p>No data available.</p>
  14. return <UserCard data={user} />
  15. };

handleUserFetch 作为 useEffect 的依赖项,并将它包裹在 useCallback 中。如果此方法使用外部参数,例如 userId (在实际开发中,可能希望获取特定的用户) ,则此参数可以作为 useCallback 的依赖项传入。只有 userId 发生变化时,依赖它的 handleUserFetch 才重写改变。

六、抽离独立逻辑

假设在组件中有一个方法,它可以处理组件的一些变量,并返回一个输出。
例如:

  1. const UserCard = ({ user }) => {
  2. const getUserRole = () => {
  3. const { roles } = user;
  4. if (roles.includes('admin')) return 'Admin';
  5. if (roles.includes('maintainer')) return 'Maintainer';
  6. return 'Developer';
  7. }
  8. return (
  9. <ul>
  10. <li>{user.name}</li>
  11. <li>{user.age}</li>
  12. <li>{user.email}</li>
  13. <li>{getUserRole()}</li>
  14. </ul>
  15. );
  16. }

这个方法和前一个例子中的方法一样,在组件每次渲染时都会重新创建,(但是没必要使用 useCallback 进行包裹,因为它没有被作为一个依赖项传入)。
组件内部定义的许多逻辑可以从组件中抽离,因为它的实现并不真正与组件相关。
改进后的代码:

  1. const getUserRole = (roles) => {
  2. if (roles.includes('admin')) return 'Admin';
  3. if (roles.includes('maintainer')) return 'Maintainer';
  4. return 'Developer';
  5. }
  6. const UserCard = ({ user }) => {
  7. return (
  8. <ul>
  9. <li>{user.name}</li>
  10. <li>{user.age}</li>
  11. <li>{user.email}</li>
  12. <li>{getUserRole(user.roles)}</li>
  13. </ul>
  14. );
  15. }

通过这种方式,可以在一个单独的文件中定义函数,并在需要时导入,进而可能会达到复用的目的。
早期将逻辑从组件中抽象出来,可以拥有更简洁的组件和易于重用的实用函数。

七、不要使用内联样式

:::info CSS 作用域在 React 中是通过 CSS-in-JS 的方案实现的,这引入了一个新的面向组件的样式范例,它和普通的 CSS 撰写过程是有区别的。另外,虽然在构建时将 CSS 提取到一个单独的样式表是支持的,但 bundle 里通常还是需要一个运行时程序来让这些样式生效。当能够利用 JavaScript 灵活处理样式的同时,也需要权衡 bundle 的尺寸和运行时的开销。— 来自 Vue 官网 ::: 以前,网页开发有一个原则,叫做「关注点分离」,主要是以下三种技术分离:

  • HTML 语言:负责网页的结构,又称语义层。
  • CSS 语言:负责网页的样式,又称
  • JavaScript 语言:负责网页的逻辑和交互,又称逻辑层或交互层。

对 CSS 来说,就是不要写内联样式(inline style),如下:

  1. <div style="width: 100%; height: 20px;">

但是组件化(Vue、React)流行以后,打破了这个原则,它要求把 HTML、CSS、JavaScript 写在一起。
使用 React 编写样式可以这么做:

  1. const style = {
  2. fontSize: "14px"
  3. }
  4. const UserCard = ({ user }) => {
  5. return (
  6. <ul style={style}>
  7. <li>{user.name}</li>
  8. <li>{user.age}</li>
  9. <li>{user.email}</li>
  10. <li>{getUserRole(user.roles)}</li>
  11. </ul>
  12. );
  13. }

React 这么做有利于组件的隔离,每个组件包含了所有需要用到的代码,不依赖外部,组件之间没有耦合,很方便复用。
这里,本文不建议在 React 中使用内联样式基于两点:

  1. 他会让HTML 结构变得臃肿。

2021-05-15-16-36-43-541065.png

  1. 如果样式过多,维护起来很麻烦,无法通过外部修改 CSS。
    1. const style1 = {
    2. fontSize: "14px"
    3. }
    4. const style2 = {
    5. fontSize: "12px",
    6. color: "red"
    7. }
    8. const style = {...}
    9. const UserCard = ({ user }) => {
    10. return (
    11. <ul style={style}>
    12. <li style={style2}>{user.name}</li>
    13. <li style={color: "#333"}>{user.age}</li>
    14. <li style={color: "#333"}>{user.email}</li>
    15. <li style={color: "#333"}>{getUserRole(user.roles)}</li>
    16. </ul>
    17. );
    18. }
    看到这里,有人可能会反驳:「可以使用 props 有条件地对 CSS 内嵌样式进行样式化」,这是可行的,然而,组件不应该只有 10 个处理 CSS 的 props,而不做其他事情。
    如果非要在组件中编写 CSS,建议使用 style-components CSS-in-JS 库。
    styled-components 编写的组件样式存在于 style 标签内,而且选择器名字是一串随机的哈希字符串,实现了局部 CSS 作用域的效果(scoping styles),各个组件的样式不会发生冲突。
    如果不借助管理 CSS 的类库,把 CSS 和 JS 混合在一起,如果做的好,可以有效的做到组件隔离。如果做的不好,这个组件不仅会变得臃肿难以理解,CSS 也会变得越来越难以维护。

    八、编写有效的 HTML

    很多人对 HTML 技术的关注度都是不够的,但是编写 HTML 和 CSS 仍然是前端工程师的必备工作。
    React 是有趣的,Hooks 也是有趣的,但最终关心的是渲染 HTML 和使它看起来更友好。
    对于 HTML 元素来说,如果它是一个按钮,它应该是一个 <button>,而不是一个可点击的 div;如果它不进行表单提交,那么它应该是 type="button";如果它应该基于文本大小自适应,它不应该有一个固定的宽度,等等。
    对一些人来说,这是最基本的,但对于一部分人来说,情况并非如此。
    经常会看到类似的表单提交代码: ```jsx import React, { useState } from ‘react’;

const Form = () => { const [name, setName] = useState(‘’);
const handleChange = e => { setName(e.target.value); } const handleSubmit = () => { // api call here } return (

) }

  1. 这个示例所做的事是在 `<input/>` 上更新 name 值,并且在 `<button/>` 上绑定 click 事件,通过点击调用 `handleSubmit` 来提交数据。<br />这个功能对于通过使用按钮进行提交的用户是可以的,但是对于使用 Enter 键提交表单的用户来说就不行了。<br />对于 form 表单支持 Enter 提交,可以这么做,无需对 Enter 进行监听:
  2. ```html
  3. <form onsubmit="myFunction()">
  4. Enter name: <input type="text">
  5. <input type="submit">
  6. </form>

详细 https://www.w3schools.com/jsref/event_onsubmit.asp
在 React 中 使用 onSubmit 是等效的:

  1. import React, { useState } from 'react';
  2. const Form = () => {
  3. const [name, setName] = useState('');
  4. const handleChange = e => {
  5. setName(e.target.value);
  6. }
  7. const handleSubmit = e => {
  8. e.preventDefault();
  9. // api call here
  10. }
  11. return (
  12. <form onSubmit={handleSubmit}>
  13. <input type="text" onChange={handleChange} value={name} />
  14. <button type="submit">
  15. Submit
  16. </button>
  17. </form>
  18. )
  19. }

现在,这个表单提交适用于任何触发场景,而不仅仅是一个只支持通过按钮点击的表单。

总结

  1. 遵循单一目的组件哲学,避免过于复杂的多行组件,并尽可能地将组件分解。
  2. 请记住,在父组件内部完成此操作总是比在组件本身内部完成更为干净。
  3. 当在 render 中返回 JSX 时,不要使用内联的 JavaScript 逻辑。
  4. 组件内部定义的许多逻辑可以从组件中抽离,因为它的实现并不真正与组件相关。
  5. 早期将逻辑从组件中抽象出来,可以拥有更简洁的组件和易于重用的实用函数。
  6. 如果不借助管理 CSS 的类库,把 CSS 和 JS 混合在一起,如果做的好,可以有效的做到组件隔离。如果做的不好,这个组件不仅会变得臃肿难以理解,CSS 也会变得越来越难以维护。
  7. React 是有趣的,Hooks 也是有趣的,但最终关心的是渲染 HTML 和使它看起来更友好。