如何构建防弹 React 组件

2025-05-25

如何构建防弹 React 组件

介绍

React 是一个声明式框架。这意味着你无需描述需要修改哪些内容才能进入下一个状态(这可能需要一些时间),你只需描述每个可能状态下的 DOM 结构,然后 React 会自动处理状态间的转换。

从命令式思维模式转变为声明式思维模式相当困难,很多时候我发现代码中的错误或低效之处,是因为用户仍然停留在命令式思维模式。
在这篇博文中,我将深入探讨声明式思维模式,以及如何使用它来构建坚不可摧的组件。

命令式 vs 声明式:

看看这个例子:

每次点击按钮,值都会在true和之间切换。如果我们以命令式的false方式编写,它将如下所示:

toggle.addEventListener("click", () => {
  toggleState = !toggleState;
  // I have to manually update the dom 
  toggle.innerText = `toggle is ${toggleState}`;
});
Enter fullscreen mode Exit fullscreen mode

完整示例在这里

下面是用声明式代码写成的相同内容

  const [toggle, setToggle] = useState(false);
  // notice how I never explicitely have to update anything in the dom
  return (
    <button onClick={() => setToggle(!toggle)}>
      toggle is {toggle.toString()}
    </button>
  );
Enter fullscreen mode Exit fullscreen mode

完整示例在这里

在第一个示例中,每次想要更改isToggled值时,你都必须记住更新 dom,这很快就会导致 bug。而在 React 中,你的代码“可以正常工作”。

心态

你的新思维的核心应该是这句话:

您的观点应该表达为您的应用程序状态的纯函数。

或者,

视图 = f(应用程序状态)

或者,

视图 = f(应用程序状态)

您的数据通过一个函数,然后您的视图从另一端出来

React 的功能组件比其旧的类组件更接近这种思维模型。

这有点抽象,所以让我们将它应用到上面的切换组件中:

“切换”按钮应该表示为isToggled变量的纯函数。

或者

按钮 = f(isToggled)

或者

视觉解释

(从现在开始我将坚持使用数学符号,但它们基本上可以互换)

让我们扩展一下这个例子。假设isToggledtrue希望按钮是绿色的,否则,它应该是红色的。

初学者常犯的一个错误是写如下内容:

const [isToggled, setIsToggled] = useState(false);
const [color, setColor] = useState('green');

function handleClick(){
  setIsToggled(!toggle)
  setColor(toggle ? 'green' : 'red')
}

  return (
    <button style={{color}} onClick={handleClick}>
      toggle is {isToggled.toString()}
    </button>
  );
Enter fullscreen mode Exit fullscreen mode

如果我们用数学符号来表示,我们得到

按钮 = f(isToggled,颜色)

现在我们的是由application_state组成的,但如果仔细观察,我们会发现可以表示为的函数isToggledcolorcolorisToggled

颜色 = f(已切换)

或作为实际代码

const color = isToggled ? 'green' : 'red'
Enter fullscreen mode Exit fullscreen mode

这种类型的变量通常被称为derived state(因为color它是从中“衍生”出来的isToggled

最终,这意味着我们的组件仍然看起来像这样:

按钮 = f(isToggled)

如何在现实世界中利用这一点

在上面的例子中,即使不以数学符号的形式写出来,也很容易发现重复的状态。但随着应用变得越来越复杂,跟踪所有应用状态变得越来越困难,重复的状态开始出现。
这种情况的一个常见症状是大量的重新渲染和陈旧的值。

每当你看到一个复杂的逻辑时,花几秒钟思考一下你所拥有的所有可能的状态。

UI 中的状态说明

下拉菜单 = f(selectedValue、arrowDirection、isOpen、options、占位符)

然后你就可以快速整理出不必要的状态

arrowDirection = f(isOpen) -> arrowDirection 可以推导出

您还可以对组件中的状态以及作为 props 传入的内容进行排序。isOpen例如,通常不需要从下拉菜单外部访问。
由此我们可以知道,我们组件的 API 可能如下所示<dropdown options={[item1, item2]} selectedValue={null} placeholder='Favorite food' />

现在编写组件将会非常容易,因为你已经确切地知道它的结构了。现在你需要做的就是弄清楚如何将状态渲染到 dom 中。

再举一个例子

分页

乍一看,这似乎有很多状态,但如果仔细观察,我们就会发现大多数状态都可以推导出来:

isDisabled = f(selectedValue, range)
"..." position = f(selectedValue, range)
middle fields = f(selectedValue, range)
amount of fields = f(selectedValue, range)

所以最后剩下的只是

分页 = f(selectedValue,范围)

这是我的实现:

它功能强大、速度快并且相对容易阅读。

让我们更进一步,将路线更改为/${pageNumber}分页更新时。

你的答案可能看起来有点像这样:

const history = useHistory();
const [page, setPage] = useState(1);

function handleChange(newPage){
  setPage(newPage)
   history.push(`/${newPage}`);
}

useEffect(()=>{
  setPage(history.location.pathname.replace("/", ""))
},[])

  return (
    <div className="App">
      <Pagination value={page} range={12} onChange={handleChange} />
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

如果确实如此,那么我有一个坏消息:您的状态重复。

页码 = f(window.href)

pageNumber 不需要自己的状态,而是将状态存储在 url 中。下面是它的实现。

其他影响

我们新思维模式的另一个重要含义是,你应该停止思考生命周期。
因为你的组件只是一个接受某些状态并返回视图的函数,所以组件何时、何地以及如何被调用、挂载或更新都无关紧要。给定相同的输入,它应该始终返回相同的输出。这就是组件纯粹的含义这也是为什么钩子只使用 ```而不是``` / ''
的原因之一。useEffectcomponentDidMountcomponentDidUpdate

你的副作用也应该始终遵循这个数据流。假设你想在每次用户更改页面时更新数据库,你可以这样做:

 function handleChange(newPage) {
    history.push(`/${newPage}`);
    updateDatabase(newPage)
  }
Enter fullscreen mode Exit fullscreen mode

但实际上您并不想在用户点击时更新数据库,而是想在值发生变化时更新数据库。

useEffect(()=>{
  updateDatabase(newPage)
})
Enter fullscreen mode Exit fullscreen mode

就像您的观点一样,您的副作用也应该取决于您的状态。

更深入

目前 React 中这条规则有几个例外,其中最重要的一个就是数据获取。想想我们通常是如何获取数据的:

const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)

useEffect(()=>{
 setIsLoading(true)

  fetch(something)
   .then(res => res.json())
   .then(res => {
     setData(res)
     setIsLoading(false)
    })
},[])

return <div>{data ? <DataComponent data={data} /> : 'loading...'}</div>
Enter fullscreen mode Exit fullscreen mode

这里有很多重复的状态,isLoading并且data都依赖于我们的 fetch promise 是否已解析。
我们现在需要这样做,因为 React 还无法解析 promise。

Svelte这样解决这个问题:

{#await promise}
    <!-- promise is pending -->
    <p>waiting for the promise to resolve...</p>
{:then value}
    <!-- promise was fulfilled -->
    <p>The value is {value}</p>
{:catch error}
    <!-- promise was rejected -->
    <p>Something went wrong: {error.message}</p>
{/await}
Enter fullscreen mode Exit fullscreen mode

React 正在开发类似Suspense 的数据获取功能

另一个重点是动画。目前,以 60fps 的速度更新状态通常是不可能的。一个很棒的库以声明式的方式解决了这个问题,那就是React Spring。Svelte也为此提供了一个原生解决方案,如果 React 将来会考虑其他解决方案,我一点也不会感到惊讶。

最后的想法

每当

  • 你的应用经常无缘无故地重新渲染
  • 你必须手动保持同步
  • 你有陈旧价值观的问题
  • 你不知道如何构建复杂的逻辑

退一步,看看你的代码并在脑海中重复:

您的观点应该表达为您的应用程序状态的纯函数。

感谢阅读❤

如果您还没有那个“顿悟时刻”,我建议您构建分页或任何您能想到的组件,并严格按照上面概述的步骤进行操作。

如果你想深入了解这个话题,我推荐以下两篇文章:

如果您认为我还有需要解释清楚的地方,或者有任何问题/意见,请随时给我发推文或在这里发表评论。

文章来源:https://dev.to/jsco/how-to-build-bulletproof-react-components-mo7
PREV
JavaScript 新手应该做和不应该做的事情
NEXT
“不要对你的代码进行注释,它应该是自文档化的”。嗯……我不同意。