memos/web/src/store
Steven 4c1d1c70d1 refactor: rename workspace to instance throughout codebase
Remove work-related terminology by renaming "workspace" to "instance"
across the entire application. This change better reflects that Memos
is a self-hosted tool suitable for personal and non-work use cases.

Breaking Changes:
- API endpoints: /api/v1/workspace/* → /api/v1/instance/*
- gRPC service: WorkspaceService → InstanceService
- Proto types: WorkspaceSetting → InstanceSetting
- Frontend translation keys: workspace-section → instance-section

Backend Changes:
- Renamed proto definitions and regenerated code
- Updated all store layer methods and database drivers
- Renamed service implementations and API handlers
- Updated cache from workspaceSettingCache to instanceSettingCache

Frontend Changes:
- Renamed service client: workspaceServiceClient → instanceServiceClient
- Updated all React components and state management
- Refactored stores: workspace.ts → instance.ts
- Updated all 32 locale translation files

All tests pass and both backend and frontend build successfully.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 23:35:35 +08:00
..
README.md refactor: rename workspace to instance throughout codebase 2025-11-05 23:35:35 +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 refactor: rename workspace to instance throughout codebase 2025-11-05 23:35:35 +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: rename workspace to instance throughout codebase 2025-11-05 23:35:35 +08:00
instance.ts refactor: rename workspace to instance throughout codebase 2025-11-05 23:35:35 +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
store-utils.ts refactor: standardize MobX store architecture with base classes and utilities 2025-10-23 19:13:42 +08:00
user.ts refactor: rename workspace to instance throughout codebase 2025-11-05 23:35:35 +08:00
view.ts fix(web): make layout and direction settings reactive in UI 2025-10-28 07:58:15 +08:00

README.md

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
instanceStore instance.ts Instance 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