简介
高效的数据获取和UI状态管理是Next.js应用中的关键挑战。如果没有一个结构化的处理方法,冗余的API调用会导致性能下降,混乱的状态管理会使代码复杂度增加。本指南探讨了如何使用状态设计模式来简化数据管理,防止不必要的请求,并改进UI的独立性,从而构建一个更为易维护和高效的應用程序。
状态模式状态模式是一种行为模式,它使对象能够根据其内部状态动态地改变其行为。通过将特定状态的逻辑封装在单独的类中,此模式促进了更清晰的关注点分离,使代码更易于维护和扩展。
要更好地理解状态模式,可以看看Refactoring Guru的指南,该指南提供了详尽的解释并附有示例。
我们的目标是...在这篇文章中,我们将探讨状态模式如何帮助简化数据加载、提升性能以及保持用户界面和逻辑的分离。通过将获取状态结构化为独立的对象,我们使代码更清晰、更模块化、更易管理。
让我们跳进去吧!
为什么要在 React/Next.js 中使用状态模式来获取数据?
在 React 和 Next.js 中获取数据可能变得具有挑战性,因为可能会出现多余的 API 调用、不必要的渲染以及复杂的状态管理问题。每次组件重新渲染可能会触发新的请求,导致性能下降和资源浪费。此外,处理不同的状态 — 加载、成功和失败 — 可能会导致代码臃肿且难以维护。
状态模式如何帮助我们解决问题状态模式通过将每种状态(加载、成功、失败)封装到单独的对象或类中,提供了一种结构化的处理方法。这样可以确保。
- 关注分离(分离关注点) — 将UI逻辑与数据获取逻辑分离,使组件更加清晰简洁。
- 可重用性和可维护性 — 状态转换由集中管理,提高了代码的可扩展性。
- 改善性能 — 通过控制数据获取的时机和方式,避免了重复的API调用。
- 灵活性 — 可以轻松扩展以支持新的状态或行为,而无需修改现有的逻辑。
通过采用这种模式,开发人员可以开发更高效、更易于预测和更可扩展的 React 或 Next.js 应用程序。
理解状态机设计模式是什么有限状态自动机
什么是状态模式?状态模式让对象可以随内部状态的不同而改变行为,比如红绿灯:
- 当红灯亮起时,汽车必须停下。
- 当黄灯亮起时,汽车准备停下。
- 当绿灯亮起时,汽车可以继续前行。
每个状态(红色、黄色、绿色)都要求不同的行为,系统在这几个状态之间平滑过渡。
状态模式的工作原理在 UI 开发中,状态模式有助于动态地管理不同状态。例如,在 React/Next.js 应用中获取数据的过程中,我们通常会处理这样的状态。
已定义的状态:
- 闲置 — 默认状态,当没有正在进行的数据获取任务时(等待开始获取数据)。
- 正在加载数据 — 在获取数据的过程中。
- 成功接收 — 当数据成功接收时。
- 出错 — 如果在获取数据过程中发生错误。
- 已取消获取 — 如果数据获取任务被用户手动中止或取消。
从一个状态到另一个状态的转变
- 从空闲状态开始,当没有进行获取操作时。
- 当发起一个获取操作时,进入加载状态。
- 如果获取成功,则变为成功状态。
- 如果获取失败,则变为错误状态。
- 如果获取被手动取消,则变为取消状态。
状态变化
UI开发中的状态模式(State Pattern)使用状态模式可以让UI状态管理更加有条理和容易理解:
- 封装状态逻辑 — 每个状态都有其特定的行为,这使得修改变得容易。
- 提升可读性和可维护性 — 避免在组件中分散复杂的
if-else
条件。 - 增强可重用性 — 状态可以在不同的组件和页面中重复利用。
应用这种模式后,UI 组件变得更加 更灵活、更可预测和更易管理,从而带来更流畅、更佳的用户体验。
定目标在以下内容中,我们明确实现的目标。当前我们有一个客户端组件在我们的 Next.js 应用程序中,该组件在每次渲染时触发获取 Lighthouse 报告。然而,在页面间导航时,加载进度会被重置,导致不必要的重复获取并影响用户体验。
为解决此问题,我们计划实现状态设计模式(State Design Pattern),以在路由变化时保持加载状态的一致性。这将帮助我们更高效地管理诸如“加载中”、“成功”、和“错误”等状态,从而提供更流畅和一致的用户体验。
每次加载时,客户端的组件都会重新获取数据。
如何在 Next.js 中实现状态模式(State Pattern) 定义状态接口和具体状态类我们将专注于以下状态:“idle”,“loading” 和 “success”。这些状态代表在 Next.js 应用程序中的数据获取流程的不同阶段,通过明确定义每个状态的接口,我们确保了对 UI 过渡和 API 响应的管理更加有条不紊。这样我们就能以结构化的方法来管理 UI 过渡和 API 响应。
需要落实的州
定义状态界面我们可以从为每个状态定义一个 TypeScript 的接口开始,确保类型安全并保持可维护性。
export interface LighthouseReportState {
/**
* 触发报告的加载并启动状态变化。
*/
load: () => void;
/**
* 返回报告的Promise对象。如果报告还未加载,则返回null。
*/
getReport: () => Promise<LighthouseReport | null>;
readonly status: Status;
}
export type Status = 'loading' | 'success' | 'idle';
export interface LighthouseReport {
// 数据模型详情请参见相关文档...
}
状态界面:此接口将作为UI组件和上下文之间的API,确保UI与状态系统之间的交互是标准化的。
这样可以确保用户界面与数据流逻辑保持独立性,同时又能平滑地与系统的当前状态互动。
定义状态类定义每个状态类(State 类)代表获取报告过程中的不同阶段,并称为LighthouseReportState
接口。
class IdleState implements LighthouseReportState {
get status(): Status {
return 'idle';
}
constructor(private readonly context: StateContext) {}
load() {
this.context.state = new LoadingState(this.context);
}
async getReport(): Promise<null> {
return null;
}
}
class LoadingState implements LighthouseReportState {
private readonly report: Promise<LighthouseReport | null>;
get status(): Status {
return 'loading';
}
constructor(private readonly context: StateContext) {
this.context.notifyProgress(0);
this.report = fetchLighthouseReport().then(report => {
this.context.state = new SuccessState(this.context, report);
return report;
}).catch(error => {
this.context.state = new IdleState(this.context);
console.warn('但是报告获取失败的话,就回到空闲状态,并且在控制台中警告失败原因。', error);
return null;
});
}
load() {
// 因为报告正在加载,所以这里不需要做任何操作。
}
async getReport(): Promise<LighthouseReport | null> {
return this.report;
}
}
class SuccessState implements LighthouseReportState {
get status(): Status {
return 'success';
}
constructor(
private readonly context: StateContext,
private readonly report: LighthouseReport
) {}
load() {
this.context.state = new LoadingState(this.context);
}
async getReport(): Promise<LighthouseReport> {
return this.report;
}
}
空闲状态
- 这是尚未请求任何报告时的默认状态。
- 将
status
设为'idle'
。 - 调用
load()
会使状态变为LoadingState
。
- 这个状态开始了抓取过程。
- 将状态设置为
'loading'
。 - 报告抓取是异步的,如果失败,状态会变成
IdleState
。 - 调用
load()
没有效果,因为它已经在抓取了。
加载成功(报告已加载完毕)
- 报告成功获取并已储存。
- 将
status
设定为'success'
。 - 调用
getReport()
返回报告。 - 调用
load()
将状态重置为加载状态,允许发起新的加载请求。
StateContext
对象负责管理状态转换。- 当请求获取报告时,
IdleState
变为LoadingState
。 - 如果获取成功,
LoadingState
变为SuccessState
。 - 如果获取失败,会退回到
IdleState
(错误情况则由后续的ErrorState
处理)。
StateContext
类作为核心状态管理器,确保任何时候都有适当的状态被激活。它支持在这些状态之间(IdleState
、LoadingState
、SuccessState
)流畅切换,将状态管理与 UI 和业务逻辑分离。
StateContext的主要职责:
- 保持当前状态 — 它保持了灯塔报告获取过程当前状态的引用。
- 允许状态转换 — 提供了
set state()
方法,用于在不同状态间动态切换。 - 封装初始状态 — 默认初始状态为
IdleState
。
// 定义一个状态上下文
export class StateContext {
// 设置状态
set state(state: LighthouseReportState) {
this._state = state;
}
// 获取状态
get state() {
return this._state;
}
// 私有状态初始化为IdleState
private _state: LighthouseReportState = new IdleState(this);
}
构造函数将 _state
初始化为 IdleState
,确保数据获取过程从空闲状态开始。这种设计确保每个状态都能独立运作,同时通过 StateContext
进行集中管理,使系统更易于维护和扩展性更强。
为了在 Next.js 项目中无缝地使用 状态模式(State Pattern),我们将利用 React 的 Context API, 集成 StateContext
。
LighthouseReportReactContext
提供了获取 Lighthouse 报告的过程的全局状态管理。
'use client';
import React from "react";
import { StateContext } from "@/app/state";
const stateContext = new StateContext();
export const LighthouseReportReactContext = React.createContext(stateContext);
2. 在组件中利用状态环境
现在,让我们创建一个使用 StateContext
来抓取 Lighthouse 报告内容的组件。
const LighthouseDashboard = () => {
const context = useContext(LighthouseReportReactContext);
const [report, setReport] = useState<LighthouseReport | null>(null);
const [loading, setLoading] = useState<boolean>(context.state.status === 'loading');
const updateReports = () => {
console.log('[LighthouseReportState] 设置报告状态:', context.state.status);
new Promise<LighthouseReport | null>(async (resolve) => {
resolve(context.state.getReport());
}).then(report => {
setReport(report);
}).catch(error => {
console.warn('[LighthouseReportState] 获取 Lighthouse 报告失败:', error);
});
};
useEffect(() => {
updateReports();
}, []);
return (
<div>
<div>
<StatusDisplay status={context.state.status}/>
</div>
<div>
<button
onClick={() => context.state.load()}
disabled={context.state.status === 'loading'}
>
<RefreshCcw className={`w-5 h-5 ${context.state.status === 'loading' ? 'animate-pulse' : ''}`}/> 重新加载
</button>
</div>
<div>
{loading && Array(4).fill(0).map((_, i) => (
<Skeleton key={i} className="h-24 w-full"/>
))}
{!loading && report && Object.entries(report).map(([key, score]) => (
<Card key={key} className="shadow-lg rounded-2xl p-4">
<CardHeader className="flex justify-between items-center">
<CardTitle className="">{key.charAt(0).toUpperCase() + key.slice(1)}</CardTitle>
{getScoreIcon(score)}
</CardHeader>
<CardContent className="">
<div className="flex justify-between text-sm font-medium">
<span className={getScoreColor(score)}>{score}%</span>
<span>100%</span>
</div>
<Progress value={score} className="mt-2"/>
</CardContent>
</Card>
))}
</div>
</div>
);
}
点击按钮会调用 StateContext
中当前状态的 load()
方法,触发状态的转换。由于 StateContext
动态地管理状态,这确保了组件在正确启动数据获取流程的同时不会,在已经处于加载状态时重复发起请求。
状态从空闲变为加载,然后变为成功,但UI没有更新。这是因为React无法自动检测到context.state
的变化。因此,React只会根据状态或属性的变化来重新渲染组件。我们直接修改了context.state
,React没有意识到这些更新,因此不会重新渲染。
React 不检测状态变化。
3. 在StateContext
中实现观察者模式来触发重新渲染内容
为了解决UI不响应状态变化的问题,我们将StateContext
更新,使其实现一个简单的观察者模式。这使得组件可以订阅状态更新,并在状态更新时自动触发重新渲染。
实施:
- 在
StateContext
中添加一个subscribe
方法,用于注册状态变化监听器。 - 当设置
state
时,每当状态更新时,通知订阅者。
export class StateContext {
private _state: LighthouseReportState = new IdleState(this);
private _stateSubject?: (state: LighthouseReportState) => void;
// 设置新的状态并通知订阅者
set state(state: LighthouseReportState) {
this._state = state;
this._stateSubject?.(state); // 通知订阅者
}
// 获取当前状态
get state() {
return this._state;
}
// 订阅状态
subscribe(callback: (state: LighthouseReportState) => void) {
this._stateSubject = callback; // 存储回调以在状态变化时通知订阅者
return () => {
this._stateSubject = undefined; // 取消订阅
};
}
}
4. 更新组件以响应状态的改变
现在我们在 StateContext
实现了观察者模式,下一步就是调整这个组件,让它能正确响应状态变化,并在状态更新时触发重渲染。
const LighthouseDashboard = () => {
const context = useContext(LighthouseReportReactContext);
const [report, setReport] = useState<LighthouseReport | null>(null);
const [loading, setloading] = useState<boolean>(context.state.status === 'loading');
const onStateChange = () => {
setloading(context.state.status === 'loading');
updateReports();
}
useEffect(() => {
const unsubscribe = context.subscribe(onStateChange);
return () => {
unsubscribe();
};
}, [context]);
// ... 其他代码
}
这里发生了什么事?
**onStateChange**
:当状态改变时调用此函数。它更新loading
状态,并触发updateReports
函数以加载数据。**useEffect**
:组件使用context.subscribe(onStateChange)
订阅状态变化。当组件卸载时,它会从上下文中断开订阅(清理)。**updateReports**
:此函数根据当前状态加载报告,然后将其存储在report
状态变量中。
政府在行动中
其他组件中重用状态值使用 React Context 和 State 设计模式的一个关键好处是,可以轻松地在多个组件间重用相同的状态。这确保了应用中数据的同步,消除了重复的数据获取和状态管理的需要。
在本节中,我们将展示如何复用 StateContext
组件来在组件间共享同步数据,例如在概览页面和其他页面之间。
比如说,我们有一个仪表板页面,页面中包含一个显示性能数据的LighthouseCard。Page
组件充当主布局,包含一个网格,网格内有各种卡片,其中就包括LighthouseCard
。
export default async function 页面组件() {
return (
<main>
<h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
一个仪表盘
</h1>
<div className="grid grid-cols-2 grid-rows-5 gap-4">
<LighthouseCard />
<E2ETestsCard />
</div>
</main>
);
}
LighthouseCard
使用 LighthouseReportReactContext
异步获取 Lighthouse 报告数据。它将报告更新到组件状态中,并将数据映射到一个数组 (reportData
),其中包括性能、可访问性、最佳实践和 SEO 的分数,每个分数都配有相应的图标。
这种方法确保了各组件间的数据同步,避免了重复获取数据,并利用共享的StateContext
。
const LighthouseCard = () => {
const context = useContext(LighthouseReportReactContext);
const [report, setReport] = useState<LighthouseReport | null>(null);
useEffect(() => {
new Promise<LighthouseReport | null>(async (resolve) => {
resolve(await context.state.getReport());
}).then(report => {
setReport(report);
}).catch(error => {
console.warn('获取 Lighthouse 报告失败:', error);
});
}, []);
const reportData = [
{ key: 'performance', report: report?.performance, label: '性能表现', icon: Gauge },
{ key: 'accessibility', report: report?.accessibility, label: '可访问性', icon: Accessibility },
{ key: 'bestPractices', report: report?.bestPractices, label: '最佳实践', icon: ShieldCheck },
{ key: 'seo', report: report?.seo, label: 'SEO', icon: Search },
];
return(
// ... 使用状态数据的用户界面
)
}
该界面具有一个采用网格布局的仪表板,显示关键指标,如性能、可访问性、最佳做法和SEO得分。数据异步从共享状态获取,确保所有组件保持同步和最新。
重用
试一试:在应用中实现错误和取消操作的状态吧。作为现有实现的扩展,尝试加入 错误 和 取消 状态,以更好地应对异常情况。目标是确保在数据获取出错或请求被取消时,应用能有恰当的反应。
- 实现一个错误状态,用于捕获并显示数据获取过程中可能出现的任何问题,例如网络错误或响应错误。
- 添加一个取消状态,以处理数据获取被用户中断或取消的情况,确保良好的用户感受。
- 更新用户界面以反映这些新状态,在请求失败或被取消时显示适当的提示或备用内容。
这个挑战将帮助你提高应用程序的稳定性,并确保它能够更好地应对各种实际应用场景。
试着实现一下这些 States。
结尾
在这篇文章中,我们探讨了如何使用状态模式与React Context来管理和同步数据获取的状态。我们了解了如何创建一个可扩展且可重用的状态管理解决方案,将UI与数据流分开,从而使代码更清晰、更易于维护。此外,我们将状态扩展以处理各种状态,比如idle、loading和success。
一个关键的收获是,可以将状态抽象化,使其能够用于其他数据获取场景。通过在获取函数中实现依赖注入,相同的状态管理逻辑可以应用于不同的数据来源,从而使它更加灵活和适应性强。
感谢您的阅读!谢谢阅读!希望这份指南能帮助您更好地了解如何在React中运用设计模式,来创建更健壮和高效的React应用。如果您有任何想法或疑问,随时分享您的想法——我很想听听您的想法,并不断完善我的内容!
共同學習,寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章