写点什么

React 数据管理中最常见的模式和技巧

  • 2019-07-10
  • 本文字数:7504 字

    阅读完需:约 25 分钟

React数据管理中最常见的模式和技巧

如果你不知道如何处理 React 中的数据,那会非常危险。通过本指南,你可以学习获取、存储和检索数据的一些常见模式,从而避免代码混乱的陷阱。


React 的灵活性意味着你可以以许多不同的方式处理数据。本文将教你在 React 中获取、存储和检索数据的模式,从而使你免于维护复杂系统的压力。


我发现,React 中的大多数问题都可以通过一些非常简单的技术来解决。虽然有些情况下可能需要使用诸如 Redux(或其他花哨的东西)之类的成熟架构。但很多时候,React 内置的生命周期方法和本地状态都能实现这一点。在我看来,一个真正优秀的开发人员的标志是能够用尽可能少的 active 组件来解决问题。


在本文中,我将分享我所谓的“数据生存工具包”:我在日常工作中用于管理数据的最常见的模式和技巧。当你学会构建数据驱动 UI 所需的一切技能,从而提供良好的用户体验。

借助 Hooks 加载数据

由于大多数人都在构建“数据库应用程序”,加载数据可以说是最常见的任务之一。最终,你的数据来自何处决定了你的加载需求。这种模式在使用 REST API 或 RPC(远程过程调用)获取数据的应用程序中得到了最好的利用。


例如,如果你正在通过 GraphQL 加载数据,那么你可能很少使用这种技术(例如,当你需要以编程方式而不是页面加载方式获取数据时),而是依赖于 Apollo 之类的工具来加载数据。


import React, { useEffect, useState } from 'react';
function Posts() { const [posts, setPosts] = useState([]);
function getData() { fetch('https://jsonplaceholder.typicode.com/posts').then(async (fetchedPosts) => { const postsAsJson = await fetchedPosts.json(); setPosts(postsAsJson); }); }
useEffect(() => { getData(); const pollForData = setInterval(() => getData(), 5000); return () => { clearTimeout(pollForData); }; }, []);
return ( <div> <h4>Posts</h4> {posts.map(({ id, title, body }) => ( <div key={id}> <h3>{title}</h3> <p>{body}</p> </div> ))} </div> );}
export default Posts;
复制代码


这个示例相对较新,但归根结底只是重构以前的方法,你将通过调用 componentWillMount()或 componentDidMount()来使用 React 的新特性“Hooks” 。这里的想法很简单:当我们的组件加载到内存时,去获取它需要的数据并将其传递给 state。


为了实现这一点,我们调用 useEffect() hook 在组件加载时获取数据,然后设置一个轮询间隔,每 5 秒重新获取一次。”这里,useEffect()是必不可少的,因为功能组件中不允许存在副作用(例如,获取数据),这是由于它们会产生令人困惑的 Bug 和不一致的 UI。


这里的想法是,useEffect()允许我们通过 updatePosts()调用 useState() hook 的返回值更新功能组件的状态。为了防止 useEffect()在组件每次渲染时运行,我们传递一个空数组[]作为它的第二个参数(它可以包含一些值,以便在它们发生变化时有条件地触发 useEffect()——点击这里了解更多信息)。


为了消除组件卸载时的间隔,useEffect()接受一个“清理”函数的返回值(该函数的行为类似于类中的 componentWillUnmount())。最终的结果是,当组件初始加载时,我们的数据将被获取并传递给 state,然后每隔 5 秒再次读取一次,直到组件卸载。


我喜欢这种模式的原因是,它使初学者和老手都很容易理解数据获取过程。它还使诸如轮询/刷新这样保持数据最新的任务变得简单——不需要调用范围外的刷新函数或 Redux 操作。从技术上讲,这也很方便,因为它可以帮助我们避免把全局 state 搞乱——应该只有在绝对必要时才使用。

使用 State 自动保存

随着应用程序的演化,像“自动保存”这样的功能已经变得非常常见,用户会期待这样的特性。幸运的是,React 通过其代理 state 使我们可以轻松地实现这种特性。


我喜欢将输入变化直接写入 state(受控组件),然后在一定的延迟之后调用数据库的写操作。根据 UI 和涉及的数据量,对于单个字段,我将发送一个 PATCH,而对于其所属的整个对象,我将发送一个 PUT。


对于延迟,我喜欢使用我几年前学到的这个延迟函数——它很简单,并且很好地实现了它的目的:


const delay = (() => {  let timer = 0;  return (callback, ms) => {    clearTimeout(timer);    timer = setTimeout(callback, ms);  };})();
复制代码


这个函数的基本前提是它自动清除 setTimeout()。因此,当它被调用时,如果它在超时之前被再次调用,它会清除自己以避免 JavaScript 调用堆栈溢出的问题。在指定的毫秒延迟之后,该函数就像一个常规的 setTimeout 一样执行它所包含的代码(即在用户停止输入 3000 毫秒后再调用)。


class UserProfile extends React.Component {  state = {};
handleLiveUpdate = (event) => { const { name, value } = event; const { updateProfile } = this.props;
this.setState({ [name]: value }, () => { // 在handleLiveUpdate调用完成3秒之后,调用数据库更新 delay(() => { // 示例1:通过GraphQL mutation更新数据库 updateProfile({ variables: { [name]: value, }, });
// 示例2:通过Meteor方法更新数据库 Meteor.call('users.updateProfile', { [name]: value }, (error) => { if (error) { alert(error.reason); } });
// 示例3:通过HTTP PATCH更新数据库 fetch('https://app.com/api/users', { method: 'PATCH', body: JSON.stringify({ [name]: value }), }); }, 3000); }); };
render() { return ( <form> <h4>User Profile</h4> <div> <label htmlFor="firstName">First Name</label> <input type="text" name="firstName" value={this.state.firstName} onChange={this.handleLiveUpdate} /> </div> <div> <label htmlFor="lastName">Last Name</label> <input type="text" name="lastName" value={this.state.lastName} onChange={this.handleLiveUpdate} /> </div> <div> <label htmlFor="emailAddress">Email Address</label> <input type="email" name="emailAddress" value={this.state.emailAddress} onChange={this.handleLiveUpdate} /> </div> </form> ); }}
复制代码


这里的思想是,当用户更改要自动保存的表单时,我们立即设置 state。然后,在“后台”,我们使用 delay()函数表明“在他们停止输入之后,将当前状态保存到数据库中”。数据库部分取决于应用程序如何处理数据。


因为我花了很多时间在Meteor和GraphQL中使用我维护的样板文件Pup,所以我要么使用 Meteor 的 Meteor.call() 将数据发送到服务器进行存储,要么使用 GraphQL调用一个mutation。如果我正在构建一个移动应用程序,我将依赖 fetch()或类似 axios()这样的库来与我的 REST API 通信。

使用本地存储持久化未保存的数据

UX 最糟糕的一种情况是有大型表单却未备份。作为一个用户,没有什么比填写一个大表单并意外点击了刷新后发现所有的内容都没有了更令人沮丧的了。


幸运的是,解决这个问题的技巧非常简单(使用与上面的自动保存示例类似的方法)。这种模式要求我们的数据依赖于组件的状态,允许我们维持用户数据被持久化的假象,而不需要访问数据库。


import React from 'react';import store from 'store';
class UserProfile extends React.Component { constructor(props) { super(props); this.state = store.get('myApp.userProfile') || {}; }
handleUpdate = (event) => { const { name, value } = event;
this.setState({ [name]: value }, () => { delay(() => { store.set('myApp.userProfile', this.state); }, 500); }); };
render() { return ( <form> <h4>User Profile</h4> <div> <label htmlFor="firstName">First Name</label> <input type="text" name="firstName" value={this.state.firstName} onChange={this.handleUpdate} /> </div> <div> <label htmlFor="lastName">Last Name</label> <input type="text" name="lastName" value={this.state.lastName} onChange={this.handleUpdate} /> </div> <div> <label htmlFor="emailAddress">Email Address</label> <input type="email" name="emailAddress" value={this.state.emailAddress} onChange={this.handleUpdate} /> </div> </form> ); }}
复制代码


这看起来应该很熟悉。这里的所有内容都与自动保存的方法相同,除了一些小细节。


首先,我们引入了一个库存储,它将帮助我们跨浏览器访问本地存储(或一个用户可用的类似的浏览器缓存)。我们对库的使用仅限于两个调用:第一次加载组件/页面时以及用户更改数据时。


第一次发生在组件的 constructor()函数中。在这里,我们将设置与本地存储键当前值相关的默认状态值(这里的 myApp.userProfile 是我们选择在本地存储中存储数据的键的名称)。我们希望 store.get()返回一个对象,其中包含反映 UI 所需状态值的属性。


  handleUpdate = (event) => {    const { name, value } = event;
this.setState({ [name]: value }, () => { delay(() => { store.set('myApp.userProfile', this.state); }, 500); }); };
复制代码


为了将这些值存入本地存储,我们在组件上使用 handleUpdate 方法,该方法与自动保存模式使用相同的方法。在这里,我们立即将用户的输入保存到 state,然后在短暂的延迟之后,通过 store.set(‘myApp.userProfile’, this.state)更新本地存储值。


这里有两个细节:要注意,我们将延迟大幅降低到 500ms。这是因为访问本地存储要比访问服务器的开销小很多。还要注意,当我们更改任何输入时,我们将当前的 this.state 值整体地保存到本地存储。这对于性能来说开销很小,而且还为我们避免了杂乱的 constructor()加载,它会为每个单独的字段调用 store.get() 。

通过 Refs 进入子组件

React 一个让人苦乐参半的特性是嵌套子组件的能力。这是一种苦乐参半的做法,因为虽然它使组合变得容易——在一个聚合 UI 中将多个组件组合在一起——但它也会使从子组件获取数据之类的任务成为一件苦差事。


我喜欢利用的一个简单技巧是通过 refs 访问子组件。虽然更常见的做法是通过 props 将函数传递给子组件,由子组件负责跟踪父组件上的子组件状态(或者至少在其内部状态发生变化时通知父组件),但这可能会变得混乱而麻烦。


相反,如果我们只关心子组件当前的内部状态,那么 refs 会使我们的工作变得非常简单。让我们考虑一个工作申请的例子。我们有一些基本的表单字段和两个列表:候选人的优点和缺点。


import React from 'react';class List extends React.Component {  state = {    items: [],  };
handleRemoveItem = (id) => { this.setState(({ items }) => ({ items: items.filter((item) => item.id !== id), })); };
handleAddItem = (event) => { event.persist(); // 使用event.persist(),这样我们就不会丢失下面嵌套函数中的React合成事件(synthetic event)。 this.setState(({ items }) => ({ // randomIdGenerator()在这里是作为一个例子,它并不存在。 items: [...items, { id: randomIdGenerator(), item: event.item.value }], })); };
render() { return ( <div> {this.state.items.map(({ id, item }) => ( <li key={id}> {item} <button onClick={() => this.handleRemoveItem(id)}> <i className="fas fa-remove" /> </button> </li> ))} <form onSubmit={this.handleAddItem}> <input type="text" name="item" /> <button type="submit">Add Item</button> </form> </div> ); }}
复制代码


为了管理这两个列表,我们使用了一个嵌套组件,它有自己的状态<List/>。在内部,组件为申请人提供一个输入,他们选择添加多少列表项就可以添加多少项。作为一个独立的组件,这不会给我们带来任何问题。


import React from 'react';import List from './path/to/List';
class JobApplication extends React.Component { state = { firstName: '', lastName: '', emailAddress: '', };
handleSubmitApplication = (event) => { const { submitApplication } = this.props;
// 执行提交的GraphQL mutation调用示例。 submitApplication({ variables: { ...this.state, strengths: this.strengths.state.items, weaknesses: this.weaknesses.state.items, }, }); };
render() { return ( <React.Fragment> <h1>Job Application</h1> <form onSubmit={this.handleSubmitApplication}> <div> <label htmlFor="firstName">First Name</label> <input type="text" name="firstName" value={this.state.firstName} onChange={this.handleUpdate} /> </div> <div> <label htmlFor="lastName">Last Name</label> <input type="text" name="lastName" value={this.state.lastName} onChange={this.handleUpdate} /> </div> <div> <label htmlFor="emailAddress">Email Address</label> <input type="email" name="emailAddress" value={this.state.emailAddress} onChange={this.handleUpdate} /> </div> <label>What are some of your strengths?</label> <List ref={strengths => (this.strengths = strengths)} /> <label>What are some of your weaknesses?</label> <List ref={weaknesses => (this.weaknesses = weaknesses)} /> <button type="submit">Submit Job Application</button> </form> </React.Fragment> ); }}
复制代码


当我们在父组件内部渲染<List/>组件时,事情可能会变得混乱。这里,<JobApplication />表示父组件。在 render()中,我们可以看到生成了两个<List/>实例。


传统上,我们可以向<List/>中添加一个名为 onUpdate()的 prop,在内部供<List/>调用来传递当前项。问题在于,必须同时在内部和父组件上跟踪列表的状态(或者让父级向子级提供其数据—不太好,除非我们需要从数据库加载数据)。


为了简化这一点,我们在<List/>的每个实例中添加一个 ref,并将其赋回我们的<JobApplication/>组件,要么是 this.strengths,要么是 this.weaknesses。这样做的好处是,我们现在可以从<JobApplication/>中直接访问这些组件。


当我们的申请人提交表单时,我们需要做的就是调用 this.strengths.state.items 或 this.weaknesses.state.items。

额外的好处:通过父组件控制子组件

你可能想知道,这是否意味着我也可以通过 refs 控制子组件?是的。但是,要小心,因为这会产生意想不到的副作用。例如,当涉及到更新和父组件数据相关的组件的内部状态时,最好通过 props 传递更改。


然而,有时候,这并不总是可行的或你想要的。例如,考虑上面的例子。假设在应用程序提交了之后要“重置”表单。因为我们的<List/>组件在内部维护它们的状态,这意味着我需要从<JobApplication/>操作它们的状态。


import React from 'react';import List from './path/to/List';
class JobApplication extends React.Component { state = { firstName: '', lastName: '', emailAddress: '', };
handleSubmitApplication = (event) => { const { submitApplication } = this.props;
// 执行提交的GraphQL mutation调用示例。 submitApplication({ variables: { ...this.state, strengths: this.strengths.state.items, weaknesses: this.weaknesses.state.items, }, }).then(() => { this.strengths.setState({ items: [] }); this.weaknesses.setState({ items: [] }); }); };
render() { return ( <React.Fragment> <h1>Job Application</h1> <form onSubmit={this.handleSubmitApplication}> <div> <label htmlFor="firstName">First Name</label> <input type="text" name="firstName" value={this.state.firstName} onChange={this.handleUpdate} /> </div> <div> <label htmlFor="lastName">Last Name</label> <input type="text" name="lastName" value={this.state.lastName} onChange={this.handleUpdate} /> </div> <div> <label htmlFor="emailAddress">Email Address</label> <input type="email" name="emailAddress" value={this.state.emailAddress} onChange={this.handleUpdate} /> </div> <label>What are some of your strengths?</label> <List ref={strengths => (this.strengths = strengths)} /> <label>What are some of your weaknesses?</label> <List ref={weaknesses => (this.weaknesses = weaknesses)} /> <button type="submit">Submit Job Application</button> </form> </React.Fragment> ); }}
复制代码


在这里,当我们的申请人提交表单时,一旦数据库返回“一切正常”,我们就调用 this.strengths.setState()和 this.weaknesess.setState()来重置它们的内部状态。


再次声明,要善用这个方法——我认为这是一个非正常用法,这意味着应该谨慎使用,并进行充分的实验/测试。

结论

在 React 中使用数据并不需要很复杂。事实上,使用 React 的乐趣之一是,它可以帮助你轻松生成交互式 UI。如果你已经下定决心要使用 React,那么考虑一下如何简化自己对它的使用是值得的。我在 React 应用程序中看到的问题通常涉及不必要的复杂数据模式。


英文原文:https://ponyfoo.com/articles/react-data-survival-kit


2019-07-10 19:5710126

评论

发布
暂无评论
发现更多内容

HarmonyOS NEXT智能车载应用开发实战

最新动态

一文读懂海外舆情:概念、价值与企业必修课

沃观Wovision

出海企业 海外舆情监控 海外舆情 海外舆情监测 出海舆情

直播|均降 40% 的 GPU 成本,大规模 Agent 部署和运维的捷径是什么?

阿里巴巴云原生

阿里云 Serverless 云原生 agent

区块链DID 系统的开发流程

北京木奇移动技术有限公司

区块链技术 软件外包公司 DID开发

(三)从分层架构到数据湖仓架构系列:数据仓库分层之贴源层和数据仓库层设计

白鲸开源

数据库 大数据 数据仓库 数据湖 白鲸开源

电商仓库批量出货推荐用哪款RFID隧道机?

斯科信息

RFID隧道机 CK-TP2A

Apache SeaTunnel 9 月动态:多模块修复 + 新功能上线,社区贡献成果亮眼

白鲸开源

大数据 开源 数据同步 数据集成 Apache SeaTunnel

大数据-138 ClickHouse MergeTree 实战详解|分区裁剪 × 稀疏主键索引 × marks 标记 × 压缩

武子康

大数据 flink 分布式 Clickhouse MergeTree

华为鸿蒙开发:掌握应用包名、图标、版本及权限配置

最新动态

出海企业要做好境外舆情监测应注意哪些问题?

沃观Wovision

舆情监测 海外舆情监测 境外舆情监测 境外舆情监控

怎么选择最好用的境外舆情监测软件?

沃观Wovision

舆情监测 舆情监测系统 海外舆情监测 舆情监测软件

全球舆情的AI赋能:智能监控平台如何实现风险预测

沃观Wovision

舆情监控 舆情监测 海外舆情监控 全球舆情监测 全球舆情监控

从“打标签”到“算行为”:抖音推荐系统的进化逻辑(附打分算法深度解析)

掘金安东尼

C++ 函数:重载、覆盖、隐藏

岭南过客

c++ C++ 函数

区块链 Web3 项目的开发费用

北京木奇移动技术有限公司

区块链开发 软件外包公司 web3开发

芜湖,千兆网络下载速率只有10MB秒,过的什么苦日子

BugShare

macos 网络 网盘 网速

2026全球舆情趋势报告:洞察跨区域议题与商业风险图谱

沃观Wovision

舆情监测 海外舆情监控 舆情监测网站 全球舆情监测 全球舆情监控

Apache DolphinScheduler 9 月进展:工作流/任务执行等问题修复,性能再升级

白鲸开源

大数据 开源 开源社区 技术分享 Apache DolphinScheduler

Abaqus购买指南:除了软件费用还有其他成本吗?达索授权代理商思茂信息

思茂信息

abaqus abaqus有限元仿真 达索系统 达索代理商

支持私有化本地部署|域名证书管理系统白皮书

37丫37

DevOps 运维自动化 自动化运维 域名系统 证书管理

从“目录电价”到“直供价格”:绿电直连电价机制全景图

西格电力

新能源产业 电力交易 电价预测 电力系统 绿电直连

监测到预测:下一代舆情监测软件的演进趋势与核心技术

沃观Wovision

舆情监控 舆情监测 舆情监测系统 海外舆情监测 舆情监测软件

如何通过海外舆情分析判断新市场的文化与政策?

沃观Wovision

企业出海 舆情监测 海外舆情监控 海外舆情 海外舆情监测

MyEMS破局光伏消纳:储能与负载的和谐协奏

开源能源管理系统

开源 能源管理系统

SD-WAN如何帮助企业降低网络运维成本?

光联世纪

如何做好境外舆情监测?五大策略解密

沃观Wovision

舆情监测 海外舆情监测 境外舆情监测 境外舆情监控 境外舆情

数据采集故障频发,中控技术靠SeaTunnel实现日均TB级核心数据同步任务0出错

白鲸开源

AI 技术分享 数据同步 Apache SeaTunnel 中控技术

去中心化金融(简称 DeFi)系统的开发流程

北京木奇移动技术有限公司

区块链开发 defi开发 软件外包公司

PalmPay 携手阿里云 RocketMQ,共建非洲普惠金融“高速通道”

阿里巴巴云原生

阿里云 Serverless RocketMQ 云原生

文书生成Agent+案卷评查Agent+归纳分析Agent,推动烟草行业向更高效、更精准的方向演进

中烟创新

React数据管理中最常见的模式和技巧_语言 & 开发_Ryan Glover_InfoQ精选文章