如果你不知道如何处理 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
评论