Echo 最佳实践
本文档提供了使用 Echo 状态管理库的最佳实践和推荐模式,帮助您构建高效、可维护的应用。
状态设计原则
1. 模块化状态
为不同的功能领域创建独立的 Echo 实例,而不是使用单一的全局状态:
typescript
// ✅ 好的做法:模块化状态
const userStore = new Echo({
/* 用户相关状态 */
});
const cartStore = new Echo({
/* 购物车相关状态 */
});
const settingsStore = new Echo({
/* 应用设置相关状态 */
});
// ❌ 不好的做法:单一全局状态
const globalStore = new Echo({
user: {
/* 用户相关状态 */
},
cart: {
/* 购物车相关状态 */
},
settings: {
/* 应用设置相关状态 */
},
});
模块化状态的优势:
- 更好的关注点分离
- 减少不必要的组件重渲染
- 更容易理解和维护
- 可以为不同模块选择不同的存储策略
2. 扁平化状态结构
尽量保持状态结构扁平,避免深层嵌套:
typescript
// ✅ 好的做法:扁平化结构
const userStore = new Echo({
userId: null,
userName: "",
userEmail: "",
userPreferences: {
theme: "light",
notifications: true,
},
});
// ❌ 不好的做法:深层嵌套
const userStore = new Echo({
user: {
id: null,
profile: {
name: "",
contact: {
email: "",
},
},
preferences: {
theme: "light",
notifications: true,
},
},
});
扁平化结构的优势:
- 更容易更新特定字段
- 更好的性能(避免深层比较)
- 更容易使用选择器
3. 规范化复杂数据
对于复杂的关系数据,使用规范化结构:
typescript
// ✅ 好的做法:规范化数据
const todoStore = new Echo({
todos: {
byId: {
"todo-1": { id: "todo-1", text: "学习 Echo", completed: false },
"todo-2": { id: "todo-2", text: "写文档", completed: true },
},
allIds: ["todo-1", "todo-2"],
},
lists: {
byId: {
"list-1": { id: "list-1", name: "工作", todoIds: ["todo-1"] },
"list-2": { id: "list-2", name: "个人", todoIds: ["todo-2"] },
},
allIds: ["list-1", "list-2"],
},
});
// ❌ 不好的做法:嵌套关系数据
const todoStore = new Echo({
lists: [
{
id: "list-1",
name: "工作",
todos: [{ id: "todo-1", text: "学习 Echo", completed: false }],
},
{
id: "list-2",
name: "个人",
todos: [{ id: "todo-2", text: "写文档", completed: true }],
},
],
});
规范化数据的优势:
- 避免数据重复
- 更容易更新特定实体
- 更好的性能(特别是对于大型数据集)
存储模式选择
根据数据特性选择合适的存储模式:
临时存储 (temporary)
适用于:
- 会话级临时数据
- 不需要持久化的 UI 状态
- 表单中间状态
typescript
const uiStateStore = new Echo({
sidebarOpen: false,
activeTab: "home",
modalVisible: false,
}).temporary();
LocalStorage (localStorage)
适用于:
- 用户偏好设置
- 主题配置
- 小型数据(< 5MB)
- 需要在页面刷新后保留的状态
typescript
const preferencesStore = new Echo({
theme: "light",
fontSize: "medium",
language: "zh-CN",
}).localStorage({
name: "user-preferences",
sync: true,
});
IndexedDB (indexed)
适用于:
- 大型数据集
- 复杂结构数据
- 需要高性能查询的数据
- 离线应用数据
typescript
const documentsStore = new Echo({
documents: {},
currentDocumentId: null,
}).indexed({
name: "documents",
database: "app-data",
object: "user-documents",
sync: true,
});
React 集成最佳实践
使用选择器优化性能
始终使用选择器来只订阅组件需要的状态部分:
typescript
function UserAvatar() {
// ✅ 好的做法:只订阅需要的数据
const avatarUrl = userStore.use((state) => state.profile.avatarUrl);
return <img src={avatarUrl} alt="用户头像" />;
}
function UserProfile() {
// ❌ 不好的做法:订阅整个状态
const state = userStore.use();
return (
<div>
<img src={state.profile.avatarUrl} alt="用户头像" />
<h2>{state.profile.name}</h2>
</div>
);
}
在组件卸载时清理订阅
当使用自定义订阅时,确保在组件卸载时清理:
typescript
function NotificationCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 添加订阅
const unsubscribe = notificationStore.subscribe((state) => {
setCount(state.notifications.length);
});
// 清理订阅
return () => unsubscribe();
}, []);
return <span>通知: {count}</span>;
}
使用 React.memo 减少重渲染
对于使用 Echo 的组件,考虑使用 React.memo 进一步优化性能:
typescript
const UserCard = React.memo(function UserCard({ userId }) {
const user = userStore.use((state) =>
state.users.find((u) => u.id === userId)
);
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
异步操作处理
使用状态标志跟踪异步操作
在状态中包含加载和错误标志:
typescript
const userStore = new Echo({
data: null,
loading: false,
error: null,
});
async function fetchUser(id) {
try {
// 设置加载状态
userStore.set({ loading: true, error: null });
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error("获取用户失败");
const data = await response.json();
// 更新数据并清除加载状态
userStore.set({ data, loading: false });
} catch (error) {
// 设置错误状态
userStore.set({ loading: false, error: error.message });
}
}
创建专用的异步操作函数
将异步逻辑封装在专用函数中,而不是直接在组件中处理:
typescript
// userService.js
export const userService = {
async fetchUser(id) {
try {
userStore.set({ loading: true, error: null });
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error("获取用户失败");
const data = await response.json();
userStore.set({ data, loading: false });
return data;
} catch (error) {
userStore.set({ loading: false, error: error.message });
throw error;
}
},
async updateUser(id, updates) {
// 类似的实现...
},
};
// 在组件中使用
function UserProfile({ userId }) {
const { data, loading, error } = userStore.use();
useEffect(() => {
userService.fetchUser(userId).catch(console.error);
}, [userId]);
// 渲染逻辑...
}
持久化和同步
等待初始化完成
在访问状态之前,始终等待初始化完成:
typescript
async function initializeApp() {
// 等待所有存储初始化
await Promise.all([
userStore.ready(),
settingsStore.ready(),
dataStore.ready(),
]);
// 现在可以安全地访问状态
console.log("用户:", userStore.current);
console.log("设置:", settingsStore.current);
// 渲染应用
renderApp();
}
正确处理链式调用
当使用链式调用时,确保在设置状态前等待初始化完成:
typescript
// ✅ 好的做法:等待初始化完成
async function loadProject(projectId, projectData) {
const store = new Echo({
/* 默认状态 */
});
// 先配置存储
store.indexed({
name: projectId,
database: "projects",
});
// 等待初始化完成
await store.ready();
// 然后设置状态
store.set(projectData, { replace: true });
return store;
}
// ❌ 不好的做法:没有等待初始化
function loadProject(projectId, projectData) {
return new Echo({
/* 默认状态 */
})
.indexed({
name: projectId,
database: "projects",
})
.set(projectData, { replace: true }); // 可能会被覆盖!
}
组织和扩展
创建自定义 Store 类
对于复杂应用,创建扩展 Echo 的自定义 Store 类:
typescript
class TodoStore extends Echo<TodoState> {
constructor() {
super({
todos: [],
filter: "all",
loading: false,
});
this.localStorage({
name: "todos",
sync: true,
});
}
// 添加业务逻辑方法
addTodo(text: string) {
if (!text.trim()) return;
this.set((state) => ({
todos: [
...state.todos,
{
id: Date.now().toString(),
text,
completed: false,
createdAt: new Date().toISOString(),
},
],
}));
}
toggleTodo(id: string) {
this.set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}));
}
deleteTodo(id: string) {
this.set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
}));
}
setFilter(filter: "all" | "active" | "completed") {
this.set({ filter });
}
// 添加计算属性
getFilteredTodos() {
const { todos, filter } = this.current;
switch (filter) {
case "active":
return todos.filter((todo) => !todo.completed);
case "completed":
return todos.filter((todo) => todo.completed);
default:
return todos;
}
}
// 异步方法
async fetchTodos() {
try {
this.set({ loading: true });
const response = await fetch("/api/todos");
const todos = await response.json();
this.set({ todos, loading: false });
} catch (error) {
console.error("获取待办事项失败:", error);
this.set({ loading: false });
}
}
}
// 创建单例实例
export const todoStore = new TodoStore();
使用工厂函数创建 Store
对于需要动态创建的 Store,使用工厂函数:
typescript
function createProjectStore(projectId: string) {
const store = new Echo({
details: null,
tasks: [],
members: [],
loading: false,
error: null,
});
store.indexed({
name: `project-${projectId}`,
database: "projects-db",
sync: true,
});
// 添加项目特定的方法
const projectApi = {
async fetchDetails() {
store.set({ loading: true });
try {
const response = await fetch(`/api/projects/${projectId}`);
const data = await response.json();
store.set({ details: data, loading: false });
} catch (error) {
store.set({ error: error.message, loading: false });
}
},
// 其他项目特定方法...
};
// 返回增强的 store
return Object.assign(store, projectApi);
}
// 使用
const projectStore = createProjectStore("project-123");
await projectStore.ready();
projectStore.fetchDetails();
调试和测试
添加调试中间件
创建一个简单的调试中间件来记录状态变化:
typescript
function createDebugStore<T>(name: string, defaultState: T) {
const store = new Echo<T>(defaultState);
if (process.env.NODE_ENV === "development") {
store.subscribe((state) => {
console.group(`[Echo Store: ${name}] 状态更新`);
console.log("新状态:", state);
console.groupEnd();
});
}
return store;
}
// 使用
const userStore = createDebugStore("User", { name: "", age: 0 });
为测试创建可重置的 Store
在测试中,确保每个测试用例都有干净的状态:
typescript
// store.js
export function createTestableStore() {
const store = new Echo({
count: 0,
data: null,
});
// 添加测试辅助方法
return {
...store,
resetForTest() {
store.set({ count: 0, data: null }, { replace: true });
},
};
}
// 在测试中使用
import { createTestableStore } from "./store";
describe("Counter tests", () => {
const store = createTestableStore();
beforeEach(() => {
store.resetForTest();
});
test("should increment counter", () => {
store.set((state) => ({ count: state.count + 1 }));
expect(store.current.count).toBe(1);
});
test("should decrement counter", () => {
store.set({ count: 5 });
store.set((state) => ({ count: state.count - 1 }));
expect(store.current.count).toBe(4);
});
});
性能优化
避免不必要的状态更新
确保只在值实际变化时才更新状态:
typescript
function updateUserPreference(key, value) {
const currentValue = userPreferencesStore.current[key];
// 只有当值实际变化时才更新
if (currentValue !== value) {
userPreferencesStore.set({ [key]: value });
}
}
批量更新相关状态
将相关的状态更新合并为一次更新:
typescript
// ✅ 好的做法:一次更新多个相关字段
function updateUserProfile(updates) {
userStore.set(updates);
}
// ❌ 不好的做法:多次单独更新
function updateUserProfile(updates) {
if (updates.name) userStore.set({ name: updates.name });
if (updates.email) userStore.set({ email: updates.email });
if (updates.avatar) userStore.set({ avatar: updates.avatar });
}
使用记忆化选择器
对于复杂的选择器,使用记忆化来避免不必要的重计算:
typescript
import { useMemo } from "react";
function TodoList() {
const { todos, filter } = todoStore.use();
// 使用 useMemo 记忆化过滤结果
const filteredTodos = useMemo(() => {
switch (filter) {
case "active":
return todos.filter((todo) => !todo.completed);
case "completed":
return todos.filter((todo) => todo.completed);
default:
return todos;
}
}, [todos, filter]);
return (
<ul>
{filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
总结
遵循这些最佳实践将帮助您充分利用 Echo 状态管理库的优势,构建高效、可维护的应用。关键要点:
- 设计良好的状态结构(模块化、扁平化、规范化)
- 选择合适的存储模式(临时、LocalStorage、IndexedDB)
- 使用选择器优化性能
- 正确处理异步操作
- 等待初始化完成
- 扩展 Echo 以满足特定需求
- 实施调试和测试策略
- 应用性能优化技术