代码简洁之道:编写干净的 React Components & JSX

发表于 3年以前  | 总阅读数:543 次

不同团队编写出来的 React 代码也不尽相同,水平有个有高低,就像十个读者就有十种哈姆雷特,但是以下八点可能是你编写 React 代码的基本准则。

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

遵循单一目的组件哲学,避免过于复杂的多行组件,并尽可能地将组件分解。

假设我们有一个简单的 <userCard/> 组件,此组件的唯一用途是接收一个 user 对象并显示相应的用户数据。

代码如下:

import React from 'react';
import propsTypes from 'props-types';

const UserCard = ({ user }) => {
  return (
    <ul>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
    </ul>
  )
}

UserCard.propsTypes = {
  user: propsTypes.object
}
UserCard.defaultTypes = {
  user: {}
}

这个组件需要一个 user props 并且它是组件的唯一数据源,但是这个 props 不是必须的(没有设置 isRequired),所以设置了一个默认值为 {},以避免 Can not access property‘ name’ of... errors 等错误。

那么,如果没有为 <UserCard> 组件在等待渲染时提供一个回退值 (fallback),并且 UserCard 没有任何数据也没有任何逻辑运行,那么就没有理由呈现这个组件。

那么,props 什么时候是必需的,什么时候不是必需的呢?

有时候你只需要问问自己什么才是最有意义的。

假设我们有一个转换货币的组件 <CurrencyConverter/>,它有三个 props

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

然而,如果我们的 value 值不足,那么进行任何转换都毫无意义,那时根本不需要呈现组件。

因此,value props 肯定是必需的。

二、 条件在父组件中呈现

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

import React, { useState } from "react";
import PropTypes from "prop-types";
// 子组件
export const UserCard = ({ user }) => {
  const keys = Object.keys(user)
  return (
    keys.length ?
      <ul>
        <li>{user.name}</li>
        <li>{user.age}</li>
        <li>{user.email}</li>
      </ul>
    : "No Data"
  );
};

我们看到一个组件带有一些逻辑,却徒劳地执行,只是为了显示一个 spinner 或一条信息。

针对这种情况,请记住,在父组件内部完成此操作总是比在组件本身内部完成更为干净。

按着这个原则,子组件和父组件应该像这样:

import React, { useState } from "react";
import PropTypes from "prop-types";

// 子组件
export const UserCard = ({ user }) => {
  return (
    <ul>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
    </ul>
  );
};

UserCard.propTypes = {
  user: PropTypes.object.isRequired
};

// 父组件
export const UserContainer = () => {
  const [user, setUser] = useState(null);
  // do some apiCall here
  return (
    <div>
      {user && <UserCard user={user} />}
    </div>
  );
};

通过这种方式,我们将 user 初始化为 null,然后简单的运行一个 falsy 检测,如果 user 不存在,!user 将返回 true

如果设置为 {} 则不然,我们必须通过 Object.keys() 检查对象 key 的长度,通过不创建新的对象引用,我们节省了一些内存空间,并且只有在获得了所需的数据之后,我们才渲染子组件 <UserCard/>

如果没有数据,显示一个 spinner 也会很容易做到。

export const UserContainer = () => {
  const [user, setUser] = useState(null); 
  // do some apiCall here
  return (
    <div>
      {user ? <UserCard user={user} /> : 'No data available'}
    </div>
  );
};

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

三、不满足时,及时 return

即使我们使用的是正常的编程语言,嵌套也是一团糟,更不用说 JSX(它是 JavaScript、 HTML 的混合体)了。

你可能经常会看到使用 JSX 编写的类似的代码:

const NestedComponent = () => {
  // ...
  return (
    <>
      {!isLoading ? (
        <>
          <h2>Some heading</h2>
          <p>Some description</p>
        </>
      ) : <Spinner />}
    </>
  )
}

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

const NestedComponent = () => {
  // ...
  if (isLoading) return <Spinner />
  return (
    <>
      <h2>Some heading</h2>
      <p>Some description</p>
    </>
  )
}

我们处理 render 逻辑时,在处理是否有可用的数据,页面是否正在加载,我们都可以选择提前 return

这样我们就可以避免嵌套,不会把 HTML 和 JavaScript 混合在一起,而且代码对于不同技术水平或没有技术背景的人来说也是可读的。

四、在 JSX 中尽量少写 JavaScript

JSX 是一种混合语言,可以写JS代码,可以写表达式,可以写 HTML,当三者混合起来后,使用 JSX 编写的代码可读性就会差很多。

虽然有经验的人可以理解组件内部发生了什么,但并不是每个人都能理解。

const CustomInput = ({ onChange }) => {
  return (
    <Input onChange={e => {
      const newValue = getParsedValue(e.target.value);
      onChange(newValue);
    }} />
  )
}

我们正在处理一些外部对 input 的一些输入,使用自定义处理程序解析该输入 e.target.value,然后将解析后的值传给 <CustomInput/> 组件接收的 onChange prop。虽然这个示例可以正常工作,但是会让代码变得很难理解。

在实际项目中会有更多的元素和更复杂的 JS 逻辑,所以我们将逻辑从组件中抽离出来,会使 return() 更清晰。

const CustomInput = ({ onChange }) => {
  const handleChange = (e) => {
    const newValue = getParsedValue(e.target.value);
    onChange(newValue);
  };
  return (
    <Input onChange={handleChange} />
  )
}

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

五、userCallback & userEffect

随着 v16.8 Hooks 问世后,人们开始大量使用函数组件,当使用函数进行编写组件时,如果需要在内部执行 API 接口的调用,需要用到 useEffect 生命周期钩子。

useEffect 用于处理组件中的 effect,通常用于请求数据,事件处理,订阅等相关操作。

在最初版本的文档指出,防止 useEffect 出现无限循环,需要提供空数组 [] 作为 useEffect 依赖项,将使钩子只能在组件的挂载和卸载阶段运行。因此,我们会看到在很多使用 useEffect 的地方将 [] 作为依赖项传入。

使用 useEffect 出现无限循环的原因是,useEffect 在组件 mount 时执行,但也会在组件更新时执行。因为我们在每次请求数据之后基本上都会设置本地的状态,所以组件会更新,因此 useEffect 会再次执行,因此出现了无限循环的情况。

然而,这种处理方式就会出现 react-hooks/exhaustive-deps 规则的警告,因此代码中常常会通过注释忽略此警告。

// eslint-disable-next-line react-hooks/exhaustive-deps 
import React, { useState, useEffect } from 'react'

import { fetchUserAction } from '../api/actions.js'

const UserContainer = () => {
  const [user, setUser] = useState(null);

  const handleUserFetch = async () => {
    const result = await fetchUserAction();
    setUser(result);
  };

  useEffect(() => {
    handleUserFetch();
    // 忽略警告
    // eslint-disable-next-line react-hooks/exhaustive-deps 
  }, []);

  if (!user) return <p>No data available.</p>

  return <UserCard data={user} />
};

最初,很多人认为这个警告毫无意义,从而选择进行忽略,而不去试图探索它是如何产生的。

其实,有些人没有意识到,handleUserFetch() 方法在组件每次渲染的时候都会重新创建(组件有多少次更新就会创建多少次)。

关于 react-hooks/exhaustive-deps 详细的讨论,可以看下这个 issue。

useCallback 的作用在于利用 memoize 减少无效的 re-render,来达到性能优化的作用。

这就是为什么我们需要在 useEffect 中调用的方法上使用 useCallback的原因。通过这种方式,我们可以防止 handleUserFetch() 方法重新创建(除非其依赖项发生变化) ,因此这个方法可以用作 useEffect 钩子的依赖项,而不会导致无限循环。

上边的例子应该这样重写:

import React, { useState, useEffect, useCalllback } from 'react'

import { fetchUserAction } from '../api/actions.js'

const UserContainer = () => {
  const [user, setUser] = useState(null);

  // 使用 useCallback 包裹
  const handleUserFetch = useCalllback(async () => {
    const result = await fetchUserAction();
    setUser(result);
  }, []);

  useEffect(() => {
    handleUserFetch();
  }, [handleUserFetch]); /* 将 handleUserFetch 作为依赖项传入 */

  if (!user) return <p>No data available.</p>

  return <UserCard data={user} />
};

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

六、抽离独立逻辑

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

例如:

const UserCard = ({ user }) => {
  const getUserRole = () => {
    const { roles } = user;
    if (roles.includes('admin')) return 'Admin';
    if (roles.includes('maintainer')) return 'Maintainer';
    return 'Developer';
  }

  return (
    <ul>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
      <li>{getUserRole()}</li>
    </ul>
  );
}

这个方法和前一个例子中的方法一样,在组件每次渲染时都会重新创建,(但是没必要使用 useCallback 进行包裹,因为它没有被作为一个依赖项传入)。

组件内部定义的许多逻辑可以从组件中抽离,因为它的实现并不真正与组件相关。

改进后的代码:

const getUserRole = (roles) => {
  if (roles.includes('admin')) return 'Admin';
  if (roles.includes('maintainer')) return 'Maintainer';
  return 'Developer';
}

const UserCard = ({ user }) => {
  return (
    <ul>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
      <li>{getUserRole(user.roles)}</li>
    </ul>
  );
}

通过这种方式,可以在一个单独的文件中定义函数,并在需要时导入,进而可能会达到复用的目的。

早期将逻辑从组件中抽象出来,可以让我们拥有更简洁的组件和易于重用的实用函数。

七、不要使用内联样式

CSS 作用域在 React 中是通过 CSS-in-JS 的方案实现的,这引入了一个新的面向组件的样式范例,它和普通的 CSS 撰写过程是有区别的。另外,虽然在构建时将 CSS 提取到一个单独的样式表是支持的,但 bundle 里通常还是需要一个运行时程序来让这些样式生效。当你能够利用 JavaScript 灵活处理样式的同时,也需要权衡 bundle 的尺寸和运行时的开销。-- 来自 Vue 官网

以前,网页开发有一个原则,叫做「关注点分离」,主要是以下三种技术分离:

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

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

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

但是组件化(Vue、React)流行以后,打破了这个原则,它要求把 HTML、CSS、JavaScript 写在一起。

使用 React 编写样式可以这么做:

const style = {
  fontSize: "14px"
}
const UserCard = ({ user }) => {
  return (
    <ul style={style}>
      <li>{user.name}</li>
      <li>{user.age}</li>
      <li>{user.email}</li>
      <li>{getUserRole(user.roles)}</li>
    </ul>
  );
}

React 这么做有利于组件的隔离,每个组件包含了所有需要用到的代码,不依赖外部,组件之间没有耦合,很方便复用。

这里,本文不建议在 React 中使用内联样式基于两点:

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

2 . 如果样式过多,维护起来很麻烦,无法通过外部修改 CSS。

const style1 = {
  fontSize: "14px"
}
const style2 = {
  fontSize: "12px",
  color: "red"
}
const style = {...}
const UserCard = ({ user }) => {
  return (
    <ul style={style}>
      <li style={style2}>{user.name}</li>
      <li style={color: "#333"}>{user.age}</li>
      <li style={color: "#333"}>{user.email}</li>
      <li style={color: "#333"}>{getUserRole(user.roles)}</li>
    </ul>
  );
}

看到这里,有人可能会反驳:「你可以使用 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";如果它应该基于文本大小自适应,它不应该有一个固定的宽度,等等。

对一些人来说,这是最基本的,但对于一部分人来说,情况并非如此。

我们经常会看到类似的表单提交代码:

import React, { useState } from 'react';

const Form = () => {
  const [name, setName] = useState('');  
  const handleChange = e => {
    setName(e.target.value);
  }
  const handleSubmit = () => {
    // api call here
  } 
  return (
    <div>
      <input type="text" onChange={handleChange} value={name} />
      <button type="button" onClick={handleSubmit}>
        Submit
      </button>
    </div>
  )
}

这个示例所做的事是在 <input/> 上更新 name 值,并且在 <button/> 上绑定 click 事件,通过点击调用 handleSubmit 来提交数据。

这个功能对于通过使用按钮进行提交的用户是可以的,但是对于使用 Enter 键提交表单的用户来说就不行了。

对于 form 表单支持 Enter 提交,可以这么做,无需对 Enter 进行监听:

<form onsubmit="myFunction()">
  Enter name: <input type="text">
  <input type="submit">
</form>

详细 https://www.w3schools.com/jsref/event_onsubmit.asp

在 React 中 使用 onSubmit 是等效的:

import React, { useState } from 'react';

const Form = () => {
  const [name, setName] = useState('');

  const handleChange = e => {
    setName(e.target.value);
  }

  const handleSubmit = e => {
    e.preventDefault();
    // api call here
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleChange} value={name} />
      <button type="submit">
        Submit
      </button>
    </form>
  )
}

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

总结

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

参考:

  1. https://itnext.io/write-clean-er-components-jsx-1e70491baded
  2. https://zhuanlan.zhihu.com/p/56975681(介绍 useCallback)
  3. https://stackoverflow.com/questions/59682819/how-to-solve-react-hooks-exhaustive-deps-warning-when-dealing-with-functions-in(react-hooks-exhaustive-deps)
  4. https://zhuanlan.zhihu.com/p/103522819(介绍 CSS-in-Js)

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Jyko1nzfapyOs8CRJVdV4A

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236869次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  6873次阅读
 目录