写点什么

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:579628

评论

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

好用的无损播放器:Audirvana v3.5.50中文激活版

真大的脸盆

Mac 软件 音乐播放 无损播放器

5年MacBook用户最常用的Mac软件推荐

Rose

苹果 mac软件下载

专业级音频制作软件:Logic Pro X中文激活版

真大的脸盆

Mac 音频制作 Mac 软件 音频处理

Git Commit Message 应该怎么写?

AlwaysBeta

git

ChatGPT研究(一)——AI平民化的里程碑

Chares

人工智能 机器学习 AIGC ChatGPT

IT 数字化转型

L3C老司机

数字化转型 产品研发 工程效能 项目交付 胜任力模型

极客时间运维进阶训练营第七周作业

忙着长大#

极客时间

Navicat Premium mac用于增强数据库管理系统的优势是什么?

Rose

数据库 Navicat Premium下载 Navicat Premium中文版 Navicat Premium破解版 Navicat Premium15

MacFamilyTree 10 Mac版百年族谱,轻松绘制

理理

苹果软件 MacFamilyTree 家族谱软件 MacFamilyTree 10

比尔·盖茨最新AI演讲:人工智能时代已经开启

Chares

人工智能 机器学习 微软 ChatGPT

pdf增强插件:Enfocus PitStop Pro 2022 中文激活版

真大的脸盆

Mac PDF Mac 软件 PDF编辑

如何设计艺术字效果和图标?

理理

苹果软件 字体设计 艺术字设计 Mac字体编辑器 字体下载

Sketch创建自定义快捷的方法 Sketch中文最新版

Rose

sketch Mac Sketch快捷键 Sketch下载

电商秒杀系统

Ryan

Spring事务失效场景

TaurusCode

Spring Boot 事务 java基础 事务失效 事务回滚

uniapp 生成微信小程序码

源字节1号

微信小程序 开源 软件开发

前端面试实录CSS篇(最近一周)

沉浸式趣谈

CSS 面试 前端

Apple M1芯片软件常见问题的解决方法

理理

常见问题 Apple M1 苹果系统

IntelliJ IDEA 2023.1 版本添加了包中类的列表功能

HoneyMoose

Spring 项目运行提示错误 Not a managed type

HoneyMoose

WordPress 插件 g5plus 修改属性

HoneyMoose

IntelliJ IDEA 2023主要更新 支持M1/intel/win

Rose

IntelliJ IDEA 2023下载 IntelliJ IDEA 2023破解 IntelliJ IDEA 2023最新

苹果壁纸软件推荐|Dynamic Wallpaper千余款壁纸任你选择!

理理

动态壁纸 壁纸下载 Mac壁纸app 苹果壁纸

简单的录屏软件:ScreenFlow v10.0.9汉化版

真大的脸盆

Mac Mac 软件 屏幕录制 录屏软件 屏幕录像

Jenkins 修改启动的端口

HoneyMoose

Mac大扫除!这些App帮你迅速清理不需要的内容,释放宝贵空间

Rose

Mac清理软件 电脑运行缓慢 Mac系统清了 苹果电脑卡机

CnosDB 2.0 Arrow Flight SQL使用指北

CnosDB

开源 时序数据库 CnosDB Arrow Flight SQL

VMware Fusion Pro 13密钥 VM虚拟机安装教程

Rose

VMware Fusion Pro 13 VM虚拟机破解版 Mac双系统

做低代码引擎有多难?OneCode五个版本心路历程

codebee

低代码 低代码开发 低代码平台 低代码报告

Go 语言数组和切片的区别

AlwaysBeta

Go 源码 数组 面试题 切片

WonderPen妙笔for Mac:致力于更好的文字创作

理理

Mac写作软件 WonderPen妙笔

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