react button 加颜色_编写有弹性的react组件

当大家开始学习 React 时,常常问到风格指南。虽然呢,项目中应用一致的规则,是个不错的主意,但是很多规则挺随意的——所以 React 对这些并没有强烈的意见。

你可以使用不同的类型系统,使用函数声明或者箭头函数,也可以对你的属性按字母序或任何你愿意的其他顺序来排序。

这样的灵活性允许在项目中 整合 React 时,能应用已有的一些约定习惯。不过同时,这也会引发无休止的争论。

一些每个组件都应极力遵守的设计原则。但我不认为风格指南能很好地捕捉这些原则。接下来,我们先谈谈风格指南,然后再看看真的有用的设计原则。


别被虚幻的问题分散了注意力

在我们讨论组件的设计原则前,我想先简单说说风格指南。这不是个流行的观点,但有人得说出来!

在 JavaScript 社区中,有一些由 linter 强制约束的样式指南。我个人的观察是,他们会产生比他们价值更多的麻烦。我算不清有多少人向我展示过一些完全有效的代码,然后跟我说 “React 提示这有问题啊!”,但这其实是由他们的 lint 配置产生的!这就导致了三个问题:

  • 人们习惯于将 linter 看作是一个过分热心又吵闹的看门人,而不是一个有用的工具。 有用的警告被风格提示的海洋淹没了。因此,人们在调试时不看 linter 的提示,错过有用的信息。此外,之前不太写 JavaScript 的人群(例如,设计人员)也因此更难使用代码。
  • 对于某种模式,大家不太学着区分有效和无效的用法。例如,有一条流行的规则是,禁止在 componentDidMount 中调用 setState。但如果这个用法总是 “错的”,那 React 根本不会允许它!这就有一个合法的用例,那就是测量 DOM 节点布局——例如,定位 tooltip。我见过有人添加 setTimeout 来 “解决” 这条规则,这完全搞错了。
  • 最终,人们采用 “执法者心态”,对那些没带来有意义变化但在代码中易于发现的地方持批评态度。“你用了函数声明,但我们的项目用的是箭头函数。” 每次我有强烈意愿,想要强制执行类似的规则时,仔细想想就会发现,我把个人情绪投入到了这个规则中——然后又努力让这消失。这让我陷入虚假的成就感,而丝毫没有改进我的代码。

我这是在宣扬停止使用 linter 吗? 不!

通过良好的配置,linter 是一个很好的工具,它可以在 bug 出现前就能发现它们。但它对代码风格的关注过多,使其变得会分散注意力。


整理你的 Lint 配置

这是我建议你在周一要做的事。把你的团队叫到一起花半个小时时间,一条条过一下你们项目中启用的 lint 规则,接着问问自己:“这条规则有帮我找到过 bug 吗?” 如果不是,关掉这条规则。(你也可以用 eslint-config-react-app 从头开始创建,里面不含任何代码风格的规则)

至少,你的团队应该有一个流程,会去删除引起干扰的规则项。不要假设一年前你或别人添加到你的 lint 配置中的任何东西,都是“最佳实践”。保持质疑,找到答案。别让任何人告诉你,你不够聪明,不能选择 lint 规则。

那代码格式化呢? 用 Prettier 然后忘掉 “风格”。你完全不需要一个工具告诉你得在这加一个空格,如果有别的工具能为你修复它。用 linter 找 bug,而不是做 代 码 美 学

当然,某些方面来说,编码风格和格式没有直接关系,但在整个项目中存在不一致时还是很恼人。

然而,它们中的许多都太微妙了,无法通过一条 lint 规则捕捉到。这就是为什么说,在团队成员之间建立信任,在 wiki 或简短的设计指南里分享有用的知识,是非常重要的事了。

不是一切都值得自动化!从 实际阅读 中获得的见解,这种指南中的理由可能比遵循 “规则” 更有价值。

如果遵循严格的风格指南是一种分心,那到底什么才是重要的呢?

这就是这篇文章的主题。


编写有弹性的组件

不论多少缩进或按字母序排列,都不能修复糟糕的设计。因此,我不会专注于某些代码看起来如何,而是专注于如何让它工作。这有一些组件设计原则我认为是非常有用:

  1. 不阻断数据流
  2. 时刻准备渲染
  3. 没有单例组件
  4. 隔离本地状态

即使你不使用 React,对于存在单向数据流的任何 UI 组件模型,可能也能通过反复试验发现相同的原则。


原则 1:不阻断数据流

渲染中不要阻断数据流

当别人使用你的组件时,他们的预期是,不论传递属性如何变化, 组件都将反映这些变化:

// isOk 也许在 state 里,随时会变化 <Button color={isOk ? 'blue' : 'red'} />

通常,这是 React 默认工作的方式。如果你在 Button 组件中使用 color,你会看到从上层为该渲染提供的值:

function Button({ color, children }) { return ( // ✅ `color` 永远是新的 <button className={'Button-' + color}> {children} </button> ); }

然而,学习 React 时常见的一个错误是,把 props 复制到 state:

class Button extends React.Component { state = { color: this.props.color }; render() { const { color } = this.state; // `color` 不更新了! return ( <button className={'Button-' + color}> {this.props.children} </button> ); } }

这也许看起来更直观,如果你使用过 React 之外的东西。 但是,通过将 prop 复制到 state,你忽略了对它的所有更新。

// 上面的实现在更新时无法正常工作了 <Button color={isOk ? 'blue' : 'red'} />

很少情况下,这样的行为 有意为之的,请确认将这样的属性取名为 initialColordefaultColor 来表明组件会忽略这个属性的改变。

但通常你会想在你的组件中 直接读取 props,避免复制 props(或从 props 中计算得到的值) 到 state:

function Button({ color, children }) { return ( // ✅ `color` 永远是新的! <button className={'Button-' + color}> {children} </button> ); }


计算值是另一个大家可能会将 props 复制到 state 的场景。举例来说,想象一下 按钮文字 的颜色是根据 color 属性通过昂贵计算得来:

class Button extends React.Component { state = { textColor: slowlyCalculateTextColor(this.props.color) }; render() { return ( <button className={ 'Button-' + this.props.color + ' Button-text-' + this.state.textColor // `color` 改变的时候就不更新了 }> {this.props.children} </button> ); } }

这个组件有 bug,它在 color 属性改变时无法重新计算 this.state.textColor。最简单的修复是把 textColor 的计算放到 render 方法中,然后把组件改为 PureComponent

class Button extends React.PureComponent { render() { const textColor = slowlyCalculateTextColor(this.props.color); return ( <button className={ 'Button-' + this.props.color + ' Button-text-' + textColor // ✅ 永远是新的 }> {this.props.children} </button> ); } }

问题解决了!现在当 props 改变时重新计算 textColor,但是在属性不变时,能避免重复进行昂贵计算。

然而,也许我们还能再优化一下。如果 children 改变了呢?很遗憾 textColor 在这种情况下会重复计算。我们第二次尝试可能是通过在 componentDidUpdate 中调用计算。

class Button extends React.Component { state = { textColor: slowlyCalculateTextColor(this.props.color) }; componentDidUpdate(prevProps) { if (prevProps.color !== this.props.color) { // 额外的重复渲染 this.setState({ textColor: slowlyCalculateTextColor(this.props.color), }); } } render() { return ( <button className={ 'Button-' + this.props.color + ' Button-text-' + this.state.textColor // ✅ 在最后一次渲染后是新的 }> {this.props.children} </button> ); } }

然而,这也意味着我们的组件在每次更新后,都有两次 render 调用。如果我们试图优化它,那也不理想。

你可以使用已不推荐的 componentWillReceiveProps 生命周期函数。然而,大家经常把 side effects 放这。这反过来又往往会给即将到来的并发渲染 特性像 Time Slicing 和 Suspense 带来问题。而更 “安全” 的 getDerivedStateFromProps 又有点难用。

让我们退一步。实际上,我们想要 memoization。我们有一些输入,除非输入发生变化,否则我们不想重新计算输出。

使用类,你可以使用帮助程序完成 memoization。但是,Hooks 更进了一步,提供了一种记忆昂贵计算的内置方法:

function Button({ color, children }) { const textColor = useMemo( () => slowlyCalculateTextColor(color), [color] // ✅ 除非 `color` 改变,不会重新计算 ); return ( <button className={'Button-' + color + ' Button-text-' + textColor}> {children} </button> ); }

这就是所有你需要的全部代码了!

在 class 组件中,你可以使用帮助程序类似 memoize-one。在函数组件中,useMemo Hook 提供了类似的功能。

现在我们看到优化昂贵的计算也不是将 props 复制到 state 的好理由。我们的渲染结果应该响应 props 的变化。


不要在 Side Effects 里阻断数据流

目前为止,我们讨论了关于如何在属性变化时保持渲染结果的一致。避免复制 props 到 state 是一部分。然而,side effects(例如获取数据)也是数据流重要的一部分

看看这个 React 组件:

class SearchResults extends React.Component { state = { data: null }; componentDidMount() { this.fetchResults(); } fetchResults() { const url = this.getFetchUrl(); // 获取数据... } getFetchUrl() { return 'http://myapi/results?query' + this.props.query; } render() { // ... } }

很多 React 组件都像这样——但如果看看,会发现这有个 bug。fetchResults 方法使用了 query 属性来做数据获取:

getFetchUrl() { return 'http://myapi/results?query' + this.props.query; }

但如果 query 变了呢?在这个组件中,什么都不会发生。这意味着组件的 side effects 没有响应它 props 的变化。这在 React 应用中是常见 bug 的根源。

为了修复这个组件,我们需要:

  • 查看 componentDidMount 和它所调用的所有方法
    • 在我们的例子中,就是 fetchResultsgetFetchUrl
  • 写下所有被他们使用的 props 和 state
    • 在我们的例子中,就是 this.props.query
  • 确保不论 props 如何变化,都重新执行 side effect
    • 我们可以通过 componentDidUpdate 方法实现

class SearchResults extends React.Component { state = { data: null }; componentDidMount() { this.fetchResults(); } componentDidUpdate(prevProps) { if (prevProps.query !== this.props.query) { // ✅ 重新获取数据 this.fetchResults(); } } fetchResults() { const url = this.getFetchUrl(); // 获取数据... } getFetchUrl() { return 'http://myapi/results?query' + this.props.query; // ✅ 更新也处理好了 } render() { // ... } }

现在我们的代码能响应所有的 props 变化了,甚至是 side effects。

然而,牢记不要再搞砸了,这真是个挑战。例如,也许又加了 currentPage 到 state,然后在 getFetchUrl 中使用了它:

class SearchResults extends React.Component { state = { data: null, currentPage: 0, }; componentDidMount() { this.fetchResults(); } componentDidUpdate(prevProps) { if (prevProps.query !== this.props.query) { this.fetchResults(); } } fetchResults() { const url = this.getFetchUrl(); // 获取数据... } getFetchUrl() { return ( 'http://myapi/results?query' + this.props.query + '&page=' + this.state.currentPage // 更新被忽略了 ); } render() { // ... } }

哎呀,我们的代码又有 bug 了,这是因为 side effect 没有响应 currentPage 的变化。

props 和 state 是 React 数据流的一部分。在这个数据流中,rendering 和 side effects 都应响应它们的变化,而不是忽略它们!

为了修复这个问题,可以重复上面的步骤:

  • 查看 componentDidMount 和它所调用的所有方法
    • 在我们的例子中,就是 fetchResultsgetFetchUrl
  • 写下所有被他们使用的 props 和 state
    • 在我们的例子中,就是 this.props.query this.state.currentPage
  • 确保不论 props 如何变化,都重新执行 side effect
    • 我们可以通过 componentDidUpdate 方法实现

让我们修复我们的组件,用以处理 currentPage 的更新吧:

class SearchResults extends React.Component { state = { data: null, currentPage: 0, }; componentDidMount() { this.fetchResults(); } componentDidUpdate(prevProps, prevState) { if ( prevState.currentPage !== this.state.currentPage || // ✅ 更新后重新获取数据 prevProps.query !== this.props.query ) { this.fetchResults(); } } fetchResults() { const url = this.getFetchUrl(); // 获取数据... } getFetchUrl() { return ( 'http://myapi/results?query' + this.props.query + '&page=' + this.state.currentPage // ✅ 更新被处理了 ); } render() { // ... } }

如果我们能够以某种方式自动捕捉到这些错误,那不是很好吗?难道没有什么 linter 可以帮助我们处理这事吗?


遗憾的是,自动检查类组件的一致性太困难了。任何方法都可以调用任何其他方法。静态分析来自 componentDidMountcomponentDidUpdate 的调用充满了误报。

但是,可能 可以设计一个 被静态分析的一致性 API。f="https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/">React useEffect Hook 就是这样 API 的一个例子:

function SearchResults({ query }) { const [data, setData] = useState(null); const [currentPage, setCurrentPage] = useState(0); useEffect(() => { function fetchResults() { const url = getFetchUrl(); // 数据获取... } function getFetchUrl() { return ( 'http://myapi/results?query' + query + '&page=' + currentPage ); } fetchResults(); }, [currentPage, query]); // ✅ 更新后重新获取 // ... }

我们将逻辑 放在 effect 中,这样可以更容易地看到它从 React 数据流中依赖了哪些值。这些值称为“依赖”,在我们的例子中它们是 [currentPage, query]

注意这个“effect 依赖”数组并不是一个新概念。在类中,我们必须遍历所有方法调用,来搜索这些“依赖项”。useEffect API 只是显式地使用了相同的概念。

反过来说,这可以让我们自动验证它们:

3fd548af043481941d4685182c34b07a.gif

(这个 demo 来自最新推荐的 exhaustive-deps lint 规则,这是 eslint-plugin-react-hooks 其中一部分。它将不久加入到 Create React App 中)

注意,无论是将组件编写为类还是函数,都必须为 effect 响应所有 props 和 state 的更新。

使用 class API,你必须自己考虑一致性,并验证对每个相关 prop 或 state 的更改是否该由 componentDidUpdate 处理。否则,组件对 prop 和 state 的更改不具有弹性。这甚至不是专属于 React 的问题。它适用于任何允许你单独处理 “创建” 和 “更新” 事件的 UI 库

useEffect API 通过鼓励一致性来翻转默认值。 这 可能在开始时觉得会不熟悉,但这样的结果是你的组件对逻辑更改更具弹性了。由于 “依赖关系” 现在是显式的,我们可以使用 lint 规则检验是否一致。 我们用 linter 来捕捉 bug!


不要在优化中阻断数据流

还有一种情况,你可能会意外忽略对 props 的更改。当你手动优化组件时,可能会发生这类错误。

注意,使用浅比较的优化方法(如 PureComponentReact.memo)与默认比较是安全的。

但是,如果你尝试通过编写自己的比较方法来 “优化” 组件,你可能会错误地忘记比较函数属性:

class Button extends React.Component { shouldComponentUpdate(prevProps) { // 没比较 this.props.onClick return this.props.color !== prevProps.color; } render() { const onClick = this.props.onClick; // 将不响应更新 const textColor = slowlyCalculateTextColor(this.props.color); return ( <button onClick={onClick} className={'Button-' + this.props.color + ' Button-text-' + textColor}> {this.props.children} </button> ); } }

一开始很容易错过这个错误,因为对于类,你通常会传递一个方法,所以它会有相同的身份:

class MyForm extends React.Component { handleClick = () => { // ✅ 总是同一个函数 // Do something } render() { return ( <> <h1>Hello!</h1> <Button color='green' onClick={this.handleClick}> Press me </Button> </> ) } }

所以我们的优化不会马上被破坏。但是,它将持续 “看到” 旧的 onClick 值,但其他 props 不会这样:

class MyForm extends React.Component { state = { isEnabled: true }; handleClick = () => { this.setState({ isEnabled: false }); // Do something } render() { return ( <> <h1>Hello!</h1> <Button color='green' onClick={ // Button 忽略了 onClick 的更新 this.state.isEnabled ? this.handleClick : null }> Press me </Button> </> ) } }

在这个例子中,应该禁用按钮点击事件——但这不会发生,因为 Button 组件忽略了对onClick 属性的任何更新。

如果函数标识本身依赖于可能随时间变化的东西,例如本例中的 draft.content,这可能会让人更加困惑:

drafts.map(draft => <Button color='blue' key={draft.id} onClick={ // Button 忽略了 onClick 的更新 this.handlePublish.bind(this, draft.content) }> Publish </Button> )

虽然 draft.content 可能随着时间的推移而改变,但是我们的 Button 组件忽略了对onClick 属性的改变,所以它继续看到 onClick 绑定方法的 “第一个版本” 与原始的draft.content

那怎么避免这个问题?

我建议避免手动实现 shouldComponentUpdate ,也要避免在 React.memo() 中使用自定义的比较方法。React.memo 中默认的浅比较会响应函数标识的更新:

function Button({ onClick, color, children }) { const textColor = slowlyCalculateTextColor(this.props.color); return ( <button onClick={onClick} className={'Button-' + color + ' Button-text-' + textColor}> {children} </button> ); } export default React.memo(Button); // ✅ 使用浅比较

在类中,PureComponent 也有类似的行为.

这确保了传递不同的函数作为 prop,始终能有效工作。

如果你坚持使用自定义的比较,请确保不跳过函数:

shouldComponentUpdate(prevProps) { // ✅ 比较 this.props.onClick return ( this.props.color !== prevProps.color || this.props.onClick !== prevProps.onClick ); }

正如之前提到的,在类组件中很容易错过这个问题,因为方法标识通常是稳定的(但并非总是如此——而这就是 debug 困难的地方)。有了Hooks,情况不同了:

  1. function 在每个渲染中都不同,所以你能马上发现这个问题
  2. 通过 useCallbackuseContext,你能 避免往下传递函数。这让你优化渲染时不用太担心函数的问题。

总结一下这部分,不要阻断数据流!

无论何时使用 props 和 state,请考虑如果它们发生变化会发生什么。在大多数情况下,组件不应以不同方式处理初始渲染和更新流程。这使它能够适应逻辑上的变化。

对于类,在生命周期方法中使用 props 和 state 时很容易忘记更新。Hooks 推动你做正确的事情——但是如果你不习惯于这样做,它会需要一些心理调整。


原则 2:时刻准备渲染

React 组件使你可以不用花太长时间就能编写渲染代码。你描述了在任何时刻 UI 应该 如何,接着 React 去完成。好好利用那个模型!

不要试图在组件行为中,假设任何不必要的时序信息。你的组件应该随时可以重新渲染。

违背这个原则的方式是什么样的?React 让这没那么容易发生——但你可以使用传统的componentWillReceiveProps 生命周期方法来实现它:

class TextInput extends React.Component { state = { value: '' }; // 每次父节点渲染时重置本地状态 componentWillReceiveProps(nextProps) { this.setState({ value: nextProps.value }); } handleChange = (e) => { this.setState({ value: e.target.value }); }; render() { return ( <input value={this.state.value} onChange={this.handleChange} /> ); } }

在这个例子中,我们将 value 放在 state 里,但我们 从 props 接收 value。每当我们 “接收新 props” 时,就会重置状态中的 value

这种模式的问题在于它完全依赖于偶然的时间。

也许今天这个组件的父级很少更新,我们的 TextInput 仅在重要事件发生时 “接收 props”,比如保存表单。

但明天你可能会向 TextInput 的父组件加一些动画。如果父组件经常重渲染,它将 “污染” 子组件的状态!你可以在 “你可能不需要派生状态” 中阅读有关此问题的更多信息。

所以该怎么修复呢?

首先,我们得调整一下心理,需要停止将 “接收 props” 视为与 “渲染” 不同的东西。 由父组件引起的重渲染不应与由本地状态更改引起的重渲染不同。组件应该具有弹性,能适应更少或更频繁地渲染,否则它们与特定父组件存在过多耦合。

(这个 demo 给我们展示了重渲染会如何毁坏脆弱的组件)

当你真正想从 props 派生 state 时,尽管有一些不同的解决方案,通常你应该使用一个完全受控制的组件:

// Option 1: Fully controlled component. function TextInput({ value, onChange }) { return ( <input value={value} onChange={onChange} /> ); }

或者使用一个不受控的组件,加上 key 来重置它:

// Option 2: Fully uncontrolled component. function TextInput() { const [value, setValue] = useState(''); return ( <input value={value} onChange={e => setValue(e.target.value)} /> ); } // 之后我们能通过更改 key 来重置内部 state: <TextInput key={formId} />

本节的内容是,组件不应该因为它或其父组件频繁地重渲染而坏掉。如果想避免使用传统的 componentWillReceiveProps 生命周期方法,React API 的设计,可以轻松实现这个目的。

要对你的组件进行压力测试,可以将这段代码临时添加到它的父组件:

componentDidMount() { // 之后别忘了删除这行! setInterval(() => this.forceUpdate(), 100); }

可别把这段代码保留在项目里——这只是用来检查父组件重渲染的频率超出预期时,会发生什么的快速方法。它不应该损坏子组件!


你可能会想:“那我在 props 改变时重置 state,再使用 PureComponent 来避免不必要的重渲染”。

这段代码应该能工作,对吧?

// 应该阻止不必要的重渲染... 对吧?class TextInput extends React.PureComponent { state = { value: '' }; // 在父组件更新时重置本地 state componentWillReceiveProps(nextProps) { this.setState({ value: nextProps.value }); } handleChange = (e) => { this.setState({ value: e.target.value }); }; render() { return ( <input value={this.state.value} onChange={this.handleChange} /> ); } }

乍一看,这个组件似乎解决了父级重渲染时 “污染” state 的问题。毕竟,如果 props 是相同的,我们会跳过更新——所以 componentWillReceiveProps 不会被调用。

但是这都是幻觉。这个组件对实际的 prop 更改仍然不具备弹性。例如,如果我们添加另一个经常变化的 prop,就像用来做动画的 style,我们还是会 “丢失” 内部状态:

<TextInput style={{opacity: someValueFromState}} value={ // componentWillReceiveProps in TextInput // resets to this value on every animation tick. value } />

所以这个方法仍然存在缺陷。我们可以看到各种优化,例如 PureComponentshouldComponentUpdateReact.memo,它们不应该用于控制行为。只有提高性能的场景下,去使用它们。如果删除优化就会破坏某个组件,那么它就太脆弱了。

这里的解决方案和之前提到的一样。不要将 “接受 props” 视作特殊的事件。避免 “同步” props 和 state。大部分情况下,每个值都应该是完全控制的(通过 props),或者完全不受控制的(在本地 state 里)。可以的话,避免派生 state。当然,时刻准备渲染!


原则 3:没有单例组件

有时我们假设某个组件只会显示一次,如导航栏。在一段时间内这也许是对的,然而,这种假设导致的设计问题,常常会在后期显现。

例如,你可能需要在路由变化后,两个 Page 组件切换实现动画——前一个 Page 和下一个 Page。它们都需要在动画期间被加载。但是,你可能会发现,每个组件都假定它是屏幕上唯一的 Page

要重现这类问题也很容易,试试渲染你的应用两次:

ReactDOM.render( <> <MyApp /> <MyApp /> </>, document.getElementById('root') );

到处点击看看。(你可能需要为这个实验调整一些 CSS 样式)

你的应用仍然正常运行吗?或者你是否看到奇怪的崩溃和错误?偶尔对复杂组件进行压力测试是个好主意,可以确保组件存在多个拷贝时不会相互冲突。

我自己写过几次有问题的代码,例如在 componentWillUnmount 中执行全局状态 “清理”:

componentWillUnmount() { // 重置 Redux store 里的一些东西 this.props.resetForm();}

当然,如果页面上有两个这样的组件,卸载其中一个组件可能会破坏另一个组件。在 mount 后重置 “全局” 状态也不好:

componentDidMount() { // 重置 Redux store 里的一些东西 this.props.resetForm();}

在这情况下, 挂载 第二个 form 就会破坏前一个。

这些模式是检测组件是否脆弱的好指标。显示 隐藏 一颗树,不应该破坏树之外的组件。

无论你是否有计划将这个组件渲染两次,从长远来看,解决这些问题是值得的。它将引导你进行更具弹性的设计。


原则 4:隔离本地状态

假设有一个 POST 社交组件,它有一个 Comment 列表(可扩展)和一个 NewComment 输入框。

React 组件可能有本地状态。但是哪个状态真的是自己的呢?帖子内容本身是否为本地状态?评论列表呢?或者评论流的记录?或评论框里输入的值?

如果你习惯于将所有内容都放入 “状态管理”,那么可能很难回答这个问题。所以这有一个简单的决定方式。

如果你不确定某个状态是否属于本地,请问自己:“如果此组件呈现两次,交互是否应反映在另一个副本中?” 只要答案为“否”,那你就找到本地状态了。

例如,假设我们两次渲染相同的 Post。让我们看一下哪些内容会更新。

  • 发布内容 :我们希望在一棵树中编辑帖子,在另一棵树中也更新。因此,它可能不是 Post 组件的本地状态。(相反,帖子内容可能存在于 Apollo、Relay 或 Redux等缓存中)
  • 评论列表 :这与帖子内容类似。我们希望在一棵树中添加一条新注释,以反映在另一棵树中。理想情况下,我们会为它使用某种缓存,而 不应该是 我们 Post 的本地状态。
  • 被展开的评论:如果在一棵树中展开评论也会在另一棵树中展开,这有点奇怪。在这种情况下,我们与特定的 Comment UI 元素 进行交互,而不是抽象的 “评论实体”。因此,“是否已展开” 应该Comment 的本地状态。
  • 输入的新评论的值:如果在一个输入框中输入评论,也会更新到另一个树中的输入框,这也有点奇怪。除非输入框明确地组合在一起,否则通常人们期望它们是独立的。所以输入值 应该NewComment 组件的本地状态。

我不建议对这些规则进行教条式的解释。当然,在一个简单的应用程序中,你可能希望使用本地状态来处理所有事情,包括 “缓存”。我只是谈论在第一原则下,理想状态的用户体验。

别把该本地的状态全局化了。这涉及到我们的 “弹性” 主题:组件之间发生的意外同步更少。作为奖励,这修复了一大类性能问题。当你的 state 在正确的地方时,“过度渲染” 都不成问题了。


回顾

让我们再一次回顾一下这些原则:

  1. 不阻断数据流 props 和 state 可能会更新,组件应该处理好这些更新,不论什么时候。
  2. 时刻准备渲染 一个组件不应该被或多或少的渲染而损坏。
  3. 没有单例组件 即使组件只渲染一次,但通过设计让它渲染两次也不会被破坏,是更好了。
  4. 隔离本地状态 想想哪个状态是特定 UI 展示下的本地状态——并且除非必要,不要将该状态提升到更高的地方。

这些原则能帮助你编写针对更新而优化的组件。这让添加,更改和删除那些组件更容易。

重要的是,一旦我们的组件具有了弹性,那就可以再回过头,到是否应按字母排序的紧迫困境中去了。

编写有弹性的组件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React函数组件是一种纯粹的JavaScript函数,它接收一个props对象作为参数并返回一个React元素。以下是编写React函数组件的步骤: 1. 导入React库 首先要在代码中导入React库。我们可以使用ES6的import语句导入React库,也可以使用CommonJS的require语句导入React库。 ```javascript import React from 'react'; ``` 或者 ```javascript const React = require('react'); ``` 2. 创建函数组件 创建一个函数组件,它接收一个props对象作为参数,并返回一个React元素。你可以将这个组件的函数定义放在一个单独的文件中,或者在同一个文件中定义多个组件。 ```javascript function MyComponent(props) { return ( <div> <h1>{props.title}</h1> <p>{props.description}</p> </div> ); } ``` 3. 导出函数组件 将函数组件导出,以便其他组件可以使用它。 ```javascript export default MyComponent; ``` 4. 使用函数组件 在其他组件中使用函数组件,传递props对象作为参数。 ```javascript import React from 'react'; import MyComponent from './MyComponent'; function App() { return ( <div> <MyComponent title="Hello, World!" description="This is my first React component." /> </div> ); } export default App; ``` 这就是编写React函数组件的基本步骤。要注意的是,函数组件只能接收props对象作为参数,并且不能使用状态(state)或生命周期方法。如果需要使用状态或生命周期方法,应该使用类组件

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值