[译]如何编写好的React

原文: How to write great React

How to write great React

写这篇文章源于一个问题:如果我可以提出一条建议,来帮助新开发人员编写好的React,那会是什么?

我的答案是:用编写整洁函数(clean functions)的规范来编写整洁的组件(clean components)。

为什么要专注于编写组件(Why focus on writing components)?

我们的目标是编写易于阅读,易于维护和易于扩展的React应用程序。

这涉及很多因素:体系架构(architecture),状态管理(state management),文件结构(file structure),代码格式(code formatting)等。

但是,我们应用程序的大部分(我们团队使用的大部分代码)都会是组件

如果你的组件都是干净简洁的,那么你的团队就可以更快地向前推进。

这样就可以保证得到一个很好的应用吗? 并不能,因为应用体系架构的其余部分可能是一团糟。

但是用好的组件(干净简洁的组件)更不容易搭建糟糕的架构。

那么我们该如何编写好的组件? 第一步:始终将它们视为函数(always treat them as functions)

组件作为函数(Components as functions)

一些React组件是函数:

const Button = ({ text, onClick }) => (
  <button onClick={onClick}>{text}</button>
)

其他组件不是函数,而是带有render方法的类:

class Button extends Component {
  render() {
    const { text, onClick } = this.props;
    return <button onClick={onClick}>{text}</button>;
  }
}

即使在第一种场景(函数组件)中,也很容易让人忽视组件作为函数这一点(it’s easy to stop thinking of components as functions)。我们开始将组件概念化为自己的实体(its own entity),接受不同于函数规则的约束。

对于类组件,人们甚至更容易忘记该组件的核心部分是render方法:一个返回UI片段(a segment of the UI)的函数。

当我们忘记将组件视为函数时,我们会创建庞大且难以推理(hard to reason about)的组件。这些组件做了过多的事情,接收过多的props,或者具有过多的条件,很难使用或是对其进行改进。这类组件总是会让人感到头疼。

始终将组件视为函数(无论它们是基于函数的还是基于类的)是编写好的React的第一步。

这就是为什么。

编写好的函数(Writing great functions)

让我们先放下React,问一个问题:什么才是好的函数?

Robert Martin 经典的《Clean Code》强调了五个因素:

  1. 小(Small)
  2. 只做一件事(Does one thing)
  3. 一个抽象层级(One level of abstraction)
  4. 少于三个参数(Less than three arguments)
  5. 描述性名称(Descriptive name)

我们依次讨论上述每一个规则,以及它们对我们的React组件意味着什么。

组件应该足够小(Your component should be small)

The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. — Clean Code
函数的第一规则是要短小。第二条规则是还要更短小。— Clean Code

小的函数更容易阅读。 没有人愿意使用一个500行代码的函数。 罗伯特·马丁(Robert Martin)认为函数基本不应该超过20行。

对于React组件,规则有一些不同,因为即使是一个简单的element,JSX也会占用更多行数。

对于你的组件主体(对于类组件,即render方法)50行代码是一个好的规则。

50行是您的组件主体的良好经验法则(对于类组件,即render方法)。 如果查看文件的总行数比较容易,那么绝大多数组件文件都不应超过250行。 低于100行是最理想的。

保证你的组件足够小。

Your component should be small

组件应该只做一件事(Your component should do one thing)

关于这个主题,我在我的文章Tiny Components: What could go wrong?中讲了很多。

简而言之,组件应当只做一件事情:基于一个理由而改变(one reason to change)

如果你决定切换菜单项的顺序而需要更改MenuList.jsx,那是一个好的行为。 但是,如果当调整了边栏的打开方式,却还需要更改MenuList.jsx,那就不好的行为方式。

将你的UI拆分成多个只处理一件事的小代码块(tiny chunks)。

Your component should do one thing

组件应当只有一个抽象层级(Your component should have one level of abstraction)

这是一个具有多个抽象层级(伪代码)的函数:

const loadThings = async () => {
    setIsLoading(true);
    const response = await fetchThings();
    setIsLoading(false);
    const { error, data } = response;
    if (error) {
        if (error.status === 404) {
            redirectTo('/404');
        } else if (error.status === 500) {
            redirectTo('/error');
        }
    } else {
        const thingsToUpdate = data.ids.reduce((map, id) => {
            map[id] = data.things[id];
            return map;
        }, {});
        updateThingsInState(thingsToUpdate);
    }
};

我们注意到,loadTings函数中,一部分功能被抽象为其他函数,包括设置加载状态以及从服务器获取响应。而另一些功能则没有被抽象出来,包括错误时重定向和更新状态。

Note that some things are abstracted away to other functions: setting the loading state and fetching the response from the server. Others are not: redirecting on error, and updating the things in state.

这里有一种更整洁的方法:

const handleResponse = (response) => {
    const { error, data } = response;
    if (error) {
        handleError(error);
    } else {
        updateThingsInState(data);
    }
};
const loadThings = async () => {
    setIsLoading(true);
    const response = await fetchThings();
    setIsLoading(false);
    handleResponse(response);
};

现在的loadThings函数通过逐行调用其他函数来处理与加载数据有关的任务,很容易阅读。 我们的新函数handleResponse同样很简单,只包含一个条件(containing a single condition)。这样整个函数就只有一个抽象层级了。

这是一个混合抽象(mixed-abstraction)的React组件:

const Dashboard = () => {
    return (
        <div className="Dashboard">
            <header>
                <h1>Too Little Abstraction Corp.</h1>
                <nav>
                    <a href="/about">About</a>
                    <a href="/mission">Mission</a>
                    <a href="/faq">FAQ</a>
                    <a href="/contact">Contact</a>
                </nav>
            </header>
            <ProductDescription />
            <EmailSubscriptionForm />
            <footer>
              <h2>Thanks for visiting!</h2>
            </footer>
        </div>
    )
}

一些标记(markup)被抽象为子组件(<ProductDescription />, <EmailSubscriptionForm />),但headerfooter却不没有。

这也是一个非常简单的例子:在没有规范的情况下,你会遇到将数十行(或数百行)HTML标签与React子组件混合在一起的组件。

Dashboard组件做了太多事情,有太多的理由来更改这个文件,而且由于缺乏抽象,代码变得很难阅读。

解决方案:

const Dashboard = () => {
    return (
        <div className="Dashboard">
            <Header />
            <ProductDescription />
            <EmailSubscriptionForm />
            <Footer />
        </div>
    )
}

这样就非常容易阅读了。除非需要再Dashboard组件中在添加子组件,否则你几乎再也不需要去修改这个文件。

每个子组件也可以根据需要共享和修改。当你修改<Header />时,也没有破坏<Footer />的风险。

混合抽象(Mixed abstraction)是一个容易陷入的陷阱,因为在当下它是有意义的(“我只是添加一点标记,它不需要被抽象为自己的组件!”)。但是随着时间的流逝,它会导致难以解析的复杂组件。

如果你尝试将组件大致保持在同一抽象层级上(除了一些不重要的例外,例如包装div,这是可以接受的),那这些组件将更加易于维护。

将组件限制为同一抽象层级。

Your component should have one level of abstraction

组件的参数(props)要尽量少(Your component should have only a few arguments (props))

The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.. — Clean Code
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。— Clean Code

是的,严格来说,React组件仅接收两个参数,即props和context。 但是props本质上是函数的参数,也应按上面的原则同样处理。

实际上,编写只带有一个或两个props的组件确实非常困难,特别是一些组件使用props是为了将props传递给子组件。

对于组件有一个更加宽松的规范。三个props会很好(fine),五个props是有代码异味的(code smell,指代码中可能导致深层次问题的症状,译者注),超过七个props会导致严重的危机(crisis).

恰当的构成(Proper composition)可以帮你避免通过多个组件传递props,尽可能尝试在组件树(component tree)中的最低点处对事件进行处理。

附带说明,boolean类型的props会增加不必要的复杂性。 Filip Danić关于这个主题写了一篇优秀的文章

将组件的props限制在三个以下。

Your component should have only a few arguments (props)

组件应具有描述性名称(Your component should have a descriptive name)

这一点似乎是最简单的,而且应该是!

实际上,你的组件很难命名,说明它做了太多事情。 回答“此组件的作用是什么?” 应该很简单,并用这个答案作为描述性名称。

如果开发人员在浏览你的app的组件树(component tree),那么他应该对每个组件的功能都有一个完整而清晰的了解。这一点没什么好惊喜的。

这是一个更好的规则:扪心自问,“如果我告诉用户这个组件的名称,她能在UI中找到和/或猜测出组件的功能吗?”

组件不应该有技术的、抽象的名称<TodoListItem>?很容易理解。<PortfolioLoader>?更抽象,但仍然直观。<UserViewModelInterface>?呃…

保证组件名称的具体和描述性。

Your component should have a descriptive name

最后的想法(Final thoughts)

你在编写组件时是否遵循了这些规范? 为什么遵循或者为什么没有遵循? 你还遵循了哪些其他规则?

如果你有任何想法,问题或建议,请在评论中告诉我。

谢谢阅读。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容