memos/web/src/store
boojack d98ee36178
chore: standardize and improve API structure (#5224)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 22:36:51 +08:00
..
attachment.ts feat: enhance attachment store with MobX observables and actions 2025-10-23 19:55:44 +08:00
base-store.ts feat: enhance attachment store with MobX observables and actions 2025-10-23 19:55:44 +08:00
common.ts chore: standardize and improve API structure (#5224) 2025-11-05 22:36:51 +08:00
config.ts refactor: standardize MobX store architecture with base classes and utilities 2025-10-23 19:13:42 +08:00
index.ts refactor: standardize MobX store architecture with base classes and utilities 2025-10-23 19:13:42 +08:00
memo.ts refactor: standardize MobX store architecture with base classes and utilities 2025-10-23 19:13:42 +08:00
memoFilter.ts refactor: update markdown parser 2025-10-26 11:28:40 +08:00
README.md refactor: standardize MobX store architecture with base classes and utilities 2025-10-23 19:13:42 +08:00
store-utils.ts refactor: standardize MobX store architecture with base classes and utilities 2025-10-23 19:13:42 +08:00
user.ts refactor(api): migrate inbox functionality to user notifications 2025-10-31 08:33:09 +08:00
view.ts fix(web): make layout and direction settings reactive in UI 2025-10-28 07:58:15 +08:00
workspace.ts refactor: standardize MobX store architecture with base classes and utilities 2025-10-23 19:13:42 +08:00

Store Architecture

This directory contains the application's state management implementation using MobX.

Overview

The store architecture follows a clear separation of concerns:

  • Server State Stores: Manage data fetched from the backend API
  • Client State Stores: Manage UI preferences and transient state

Store Files

Server State Stores (API Data)

Store File Purpose
memoStore memo.ts Memo CRUD operations, optimistic updates
userStore user.ts User authentication, settings, stats
workspaceStore workspace.ts Workspace profile and settings
attachmentStore attachment.ts File attachment management

Features:

  • ✅ Request deduplication (prevents duplicate API calls)
  • ✅ Structured error handling with StoreError
  • ✅ Computed property memoization for performance
  • ✅ Optimistic updates (immediate UI feedback)
  • ✅ Automatic caching

Client State Stores (UI State)

Store File Purpose Persistence
viewStore view.ts Display preferences (sort, layout) localStorage
memoFilterStore memoFilter.ts Active search filters URL params

Features:

  • ✅ No API calls (instant updates)
  • ✅ localStorage persistence (viewStore)
  • ✅ URL synchronization (memoFilterStore - shareable links)

Utilities

File Purpose
base-store.ts Base classes and factory functions
store-utils.ts Request deduplication, error handling, optimistic updates
config.ts MobX configuration
common.ts Shared constants and utilities
index.ts Centralized exports

Usage Examples

Basic Store Usage

import { memoStore, userStore, viewStore } from "@/store";
import { observer } from "mobx-react-lite";

const MyComponent = observer(() => {
  // Access state
  const memos = memoStore.state.memos;
  const currentUser = userStore.state.currentUser;
  const sortOrder = viewStore.state.orderByTimeAsc;

  // Call actions
  const handleCreate = async () => {
    await memoStore.createMemo({ content: "Hello" });
  };

  const toggleSort = () => {
    viewStore.toggleSortOrder();
  };

  return <div>...</div>;
});

Server Store Pattern

// Fetch data with automatic deduplication
const memo = await memoStore.getOrFetchMemoByName("memos/123");

// Update with optimistic UI updates
await memoStore.updateMemo({ name: "memos/123", content: "Updated" }, ["content"]);

// Errors are wrapped in StoreError
try {
  await memoStore.deleteMemo("memos/123");
} catch (error) {
  if (error instanceof StoreError) {
    console.error(error.code, error.message);
  }
}

Client Store Pattern

// View preferences (persisted to localStorage)
viewStore.setLayout("MASONRY");
viewStore.toggleSortOrder();

// Filters (synced to URL)
memoFilterStore.addFilter({ factor: "tagSearch", value: "work" });
memoFilterStore.removeFiltersByFactor("tagSearch");
memoFilterStore.clearAllFilters();

Creating New Stores

Server State Store

import { StandardState, createServerStore } from "./base-store";
import { createRequestKey, StoreError } from "./store-utils";

class MyState extends StandardState {
  dataMap: Record<string, Data> = {};

  get items() {
    return Object.values(this.dataMap);
  }
}

const myStore = (() => {
  const base = createServerStore(new MyState(), {
    name: "myStore",
    enableDeduplication: true,
  });

  const { state, executeRequest } = base;

  const fetchItems = async () => {
    return executeRequest(
      createRequestKey("fetchItems"),
      async () => {
        const items = await api.fetchItems();
        state.setPartial({ dataMap: items });
        return items;
      },
      "FETCH_ITEMS_FAILED"
    );
  };

  return { state, fetchItems };
})();

Client State Store

import { StandardState } from "./base-store";

class MyState extends StandardState {
  preference: string = "default";

  setPartial(partial: Partial<MyState>) {
    Object.assign(this, partial);
    // Optional: persist to localStorage
    localStorage.setItem("my-preference", JSON.stringify(this));
  }
}

const myStore = (() => {
  const state = new MyState();

  const setPreference = (value: string) => {
    state.setPartial({ preference: value });
  };

  return { state, setPreference };
})();

Best Practices

✅ Do

  • Use observer() HOC for components that access store state
  • Call store actions from event handlers
  • Use computed properties for derived state
  • Handle errors from async store operations
  • Keep stores focused on a single domain

❌ Don't

  • Don't mutate store state directly - use setPartial() or action methods
  • Don't call async store methods during render
  • Don't mix server and client state in the same store
  • Don't access stores outside of React components (except initialization)

Performance Tips

  1. Computed Properties: Use getters for derived state - they're memoized by MobX
  2. Request Deduplication: Automatic for server stores - prevents wasted API calls
  3. Optimistic Updates: Used in updateMemo - immediate UI feedback
  4. Fine-grained Reactivity: MobX only re-renders components that access changed properties

Testing

import { memoStore } from "@/store";

describe("memoStore", () => {
  it("should fetch memos", async () => {
    const memos = await memoStore.fetchMemos({ filter: "..." });
    expect(memos).toBeDefined();
  });

  it("should cache memos", () => {
    const memo = memoStore.getMemoByName("memos/123");
    expect(memo).toBeDefined();
  });
});

Migration Guide

If you're migrating from old store patterns:

  1. Replace direct state mutations with setPartial():

    // Before
    store.state.value = 5;
    
    // After
    store.state.setPartial({ value: 5 });
    
  2. Wrap API calls with executeRequest():

    // Before
    const data = await api.fetch();
    state.data = data;
    
    // After
    return executeRequest("fetchData", async () => {
      const data = await api.fetch();
      state.setPartial({ data });
      return data;
    }, "FETCH_FAILED");
    
  3. Use StandardState for new stores:

    // Before
    class State {
      constructor() { makeAutoObservable(this); }
    }
    
    // After
    class State extends StandardState {
      // makeAutoObservable() called automatically
    }
    

Troubleshooting

Q: Component not re-rendering when state changes? A: Make sure you wrapped it with observer() from mobx-react-lite.

Q: Getting "Cannot modify state outside of actions" error? A: Use state.setPartial() instead of direct mutations.

Q: API calls firing multiple times? A: Check that your store uses createServerStore() with deduplication enabled.

Q: localStorage not persisting? A: Ensure your client store overrides setPartial() to call localStorage.setItem().

Resources