React组件设计:重新认识受控与非受控组件

来源:http://www.chinese-glasses.com 作者:Web前端 人气:191 发布时间:2020-03-31
摘要:时间: 2019-10-23阅读: 114标签: 组件 时间: 2019-05-12阅读: 332标签: react React 官网中对非受控组件与受控组件作了如图中下划线的边界定义。一经推敲,该定义是缺乏了些完整性和严谨性的,比

时间: 2019-10-23阅读: 114标签: 组件

时间: 2019-05-12阅读: 332标签: react

React 官网中对非受控组件与受控组件作了如图中下划线的边界定义。一经推敲, 该定义是缺乏了些完整性和严谨性的, 比如针对非表单组件(弹框、轮播图)如何划分受控与非受控的边界?又比如非受控组件是否真的如文案上所说的数据的展示与变更都由 dom 自身接管呢?

就像人们对更新移动应用程序和操作系统感到兴奋一样,开发人员也应该对更新框架感到兴奋。不同框架的新版本具有新特性和开箱即用的技巧。

在非受控组件中, 通常业务调用方只需传入一个初始默认值便可使用该组件。以 Input 组件为例:

下面是将现有应用程序从 React 15 迁移到 React 16 时应该考虑的一些好特性。

// 组件提供方function Input({ defaultValue }) { return input defaultValue={defaultValue} /}// 调用方function Demo() { return Input defaultValue={1} /}

错误处理

在受控组件中, 数值的展示与变更则分别由组件的state与setState接管。同样以 Input 组件为例:

React 16 引入了错误边界的新概念。

// 组件提供方function Input() { const [value, setValue] = React.useState(1) return input value={value} onChange={e = setValue(e.target.value)} /}// 调用方function Demo() { return Input /}

现在在React 16中,大家就能使用错误边界功能,而不用一发生错误就解除整个程序挂载了。把错误边界看成是一种类似于编程中try-catch语句的机制,只不过是由 React 组件来实现的。

有意思的一个问题来了,Input组件到底是受控的还是非受控的? 我们甚至还可以对代码稍加改动成Input defaultValue={1} /的最初调用方式:

错误边界是一种React组件。它及其子组件形成一个树型结构,能捕获JavaScript中所有位置的错误,记录下错误,并且还能显示一个后备界面,避免让用户直接看到组件树的崩溃信息。

// 组件提供方function Input({ defaultValue }) { const [value, setValue] = React.useState(defaultValue) return input value={value} onChange={e = setValue(e.target.value)} /}// 调用方function Demo() { return Input defaultValue={1} /}

这里涉及到一种新的生命周期函数叫componentDidCatch(error, info)。无论什么样的类组件,只要定义了这个函数,就成为了一个错误边界。

尽管此时 Input 组件本身是一个受控组件, 但与之相对的调用方失去了更改 Input 组件值的控制权, 所以对调用方而言, Input 组件是一个非受控组件。值得一提的是,以非受控组件的使用方式去调用受控组件是一种反模式, 在下文中会分析其中的弊端。

class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // 你还可以将错误记录到错误报告服务中 logErrorToMyService(error, info); } render() { if (this.state.hasError) { // 可以渲染任何自定义回退界面 return h1Something went wrong./h1; } return this.props.children; }}

如何做到不管对于组件提供方还是调用方 Input 组件都为受控组件呢? 提供方让出控制权即可, 调整代码如下codesandbox:

也可以将其用作常规组件使用:

// 组件提供方function Input({ value, onChange }) { return input value={value} onChange={onChange} /}// 调用方function Demo() { const [value, setValue] = React.useState(1) return Input value={value} onChange={e = setValue(e.target.value)} /}
ErrorBoundary MyWidget //ErrorBoundary

经过上述代码的推演后, 概括如下: 受控以及非受控组件的边界划分取决于当前组件对于子组件值的变更是否拥有控制权。如若有则该子组件是当前组件的受控组件; 如若没有则该子组件是当前组件的非受控组件。

componentDidCatch()方法的工作原理类似于JavaScriptcatch{}块,但它适用于组件。只有类组件可以是错误边界。实际上,在大多数情况下,你都希望声明一次错误边界组件,然后在整个应用程序中使用它。

职能范围

请注意,错误边界只会捕获位于它们之下的组件中的错误。错误边界无法捕获到自身的错误。如果错误边界渲染错误消息失败,错误将被传播到上方最接近的错误边界。这也类似于 JavaScript 中的 catch{}块。

基于调用方对于受控组件拥有控制权这一认知, 因此受控组件相较非受控组件能赋予调用方更多的定制化职能。这一思路与软件开发中的开放/封闭原则有异曲同工之妙, 同时让笔者受益匪浅的Inversion of Control也是类似的思想。

有了错误边界,即使某个组件的结果有错误,整个React程序挂载也不会被解除。只有出错的那个组件会显示一个后备界面,而整个程序仍然完全正常运行。点击查看在线事例

借助受控组件的赋能, 以 Input 组件为例, 比如调用方可以更为自由地对值进行校验限制, 又比如在值发生变更时执行一些额外逻辑。

关于错误边界更多的内容可查看官网。

// 组件提供方function Input({ value, onChange }) { return input value={value} onChange={onChange} /}// 调用方function Demo() { const [value, setValue] = React.useState(1) return Input value={value} onChange={e = // 只支持数值的变更 if (/D/.test(e.target.value)) return setValue(e.target.value)} /}

新的 render 返回类型:片段和字符串

因此综合基础组件扩展性与通用性的考虑, 受控组件的职能相较非受控组件更加宽泛, 建议优先使用受控组件来构建基础组件。

现在,在渲染时可以摆脱将组件包装在div中。

反模式 —— 以非受控组件的使用方式调用受控组件

你现在可以从组件的 render 方法返回元素数组。与其他数组一样,你需要为每个元素添加一个键以避免发出键警告:

首先何谓反模式? 笔者将其总结为增大隐性 bug 出现概率的模式, 该模式是最佳实践的对立经验。如若使用了反模式就不得不花更多的精力去避免潜在 bug。官网对反模式也有很好的概括总结。

render() { // No need to wrap list items in an extra element! return [ // Don't forget the keys :) li key="A"First item/li, li key="B"Second item/li, li key="C"Third item/li, ];}

缘何上文提到以非受控组件的使用方式去调用受控组件是一种反模式? 观察 Input 组件的第一行代码, 其将 defaultValue 赋值给 value, 这种将 props 赋值给 state的赋值行为在一定程度上会增加某些隐性 bug 的出现概率。

从React 16.2.0开始,它支持JSX的一个特殊片段语法,该语法不需要键。

比如在切换导航栏的场景中, 恰巧两个导航中传进组件的 defaultValue 是相同的值, 在导航切换的过程中便会将导航一中的 Input 的状态值带到导航二中, 这显然会让使用方感到困惑。codesandbox

render() { return (  ChildA / ChildB / ChildC / / );}
// 组件提供方function Input({ defaultValue }) { // 反模式 const [value, setValue] = React.useState(defaultValue); React.useEffect(() = { setValue(defaultValue); }, [defaultValue]); return input value={value} onChange={e = setValue(e.target.value)} /;}// 调用方function Demo({ defaultValue }) { return Input defaultValue={defaultValue} /;}function App() { const [tab, setTab] = React.useState(1); return (  {tab === 1 ? Demo defaultValue={1} / : Demo defaultValue={1} /} button onClick={() = (tab === 1 ? setTab(2) : setTab(1))} 切换 Tab /button / );}

支持返回字符串:

如何避免使用该反模式同时有效解决问题呢? 官方提供了两种较为优质的解法, 将其留给大家作为思考。

render() { return 'Look ma, no spans!';}

方法一:使用完全受控组件(更为推荐)方法二:使用完全非受控组件 + key

Portal

Portal 提供了一种将子节点渲染到父节点之外的 dom 节点。

ReactDOM.createPortal(child, container)

第一个参数 (child)是任何可渲染的 React子元素,例如元素,字符串或片段。 第二个参数(container)是 DOM 元素。

如何使用它

在 React15.X 版本中,我们只能讲子节点在父节点中渲染,基本用法如下:

render() { // React需要创建一个新的div来包含子节点 return ( div {this.props.children} /div );}

但是如果需要将子节点插入到父节点之外的dom呢,React15.x 及之前都没有提供这个功能的 API。可以使用 React16.0 中的 portal:

render() { // React不需要创建一个新的div去包含子元素,直接将子元素渲染到另一个 //dom节点中 //这个dom节点可以是任何有效的dom节点,无论其所处于dom树中的哪个位置 return ReactDOM.createPortal( this.props.children, domNode, );}

Portal 的一个典型用例是这样的:当父组件带有overflow:hidden或z-index样式时,你希望子组件在视觉上能够“突破”它的容器。例如,对话框、悬停卡和工具提示。点击查看在线事例

自定义 DOM 属性

React15 会忽略任何未知的 DOM 属性。React 会跳过它们,因为无法识别它们。

// 你的代码div mycustomattribute="something" /

React 15 将渲染一个空的 div:

// React 15 output:div /

在 React16 中,输出将如下所示(会显示自定义属性,并且完全不会被忽略)

// React 16 output:div mycustomattribute="something" /

在 state 中设置 null 避免重新渲染

有时候我们需要通过函数来判断组件状态更新是否触发重新渲染,在 React 16 中,我们可以通过调用setState时传入null来避免组件重新渲染,这也就意味着,我们可以在 setState 方法内部决定我们的状态是否需要更新,

const MAX_PIZZAS = 20;function addAnotherPizza(state, props) { // Stop updates and re-renders if I've had enough pizzas. if (state.pizza === MAX_PIZZAS) { return null; } // If not, keep the pizzas coming! :D return { pizza: state.pizza + 1, }}this.setState(addAnotherPizza);

更多相关信息请阅读这里

创建 ref

现在使用 React16 创建refs要容易得多。 为什么需要使用refs:

管理焦点、文本选择或媒体播放。触发动画。与第三方 DOM 库集成。

本文由10bet发布于Web前端,转载请注明出处:React组件设计:重新认识受控与非受控组件

关键词:

最火资讯