Skip to content
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1d34a5e
initial commit
Henry8192 Jun 22, 2025
a8e70ec
update Zustand store docs naming conventions & creation
Henry8192 Jun 26, 2025
4598dc9
add zustand slicing and store usage docs.
Henry8192 Jun 26, 2025
8c8be4d
Apply suggestions from code review
Henry8192 Jun 27, 2025
22526a6
patch previous commit
Henry8192 Jun 27, 2025
2159be2
apply the rest of suggestions
Henry8192 Jun 28, 2025
f92f375
move state values section
Henry8192 Jun 28, 2025
56a46b5
address coderabbit review
Henry8192 Jun 28, 2025
678c4cf
Apply suggestions from code review
Henry8192 Jun 30, 2025
8f96f8e
remove extra spaces
Henry8192 Jun 30, 2025
f5f00b7
Merge branch 'main' into zustand-store-docs
junhaoliao Jun 30, 2025
f5822d6
Update examples
Henry8192 Jun 30, 2025
d02feb6
address comment
Henry8192 Jul 8, 2025
08e9a07
specify when to slice
Henry8192 Jul 9, 2025
19c7959
Apply suggestions from code review
Henry8192 Jul 10, 2025
3dbb7ad
Merge branch 'main' into zustand-store-docs
Henry8192 Aug 11, 2025
c97fa29
Merge branch 'main' into zustand-store-docs
Henry8192 Aug 22, 2025
da469a6
Merge branch 'main' into zustand-store-docs
junhaoliao Sep 2, 2025
eb38e38
Merge branch 'main' into zustand-store-docs
junhaoliao Sep 12, 2025
e5842c4
docs: Update Zustand store naming and structure guidelines for clarit…
junhaoliao Sep 13, 2025
31faf0a
Merge branch 'main' into zustand-store-docs
Henry8192 Sep 17, 2025
515406c
address review
Henry8192 Sep 19, 2025
edfbb8c
Merge branch 'main' into zustand-store-docs
Henry8192 Oct 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions docs/src/dev-guide/coding-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,190 @@ To avoid including a state variable in a React Hook's dependency array, you can
the state variable with an additional `Ref` suffix. E.g., `logEventNumRef` is the reference variable
that corresponds to the `logEventNum` state variable.

# Zustand

Zustand is a state management tool, which allows creating global state stores that can be accessed
from anywhere. Please follow the guidelines below when using Zustand stores.

## File naming conventions

When creating Zustand stores, we follow these naming conventions:

* Simple stores: `{name}Store.ts` (camelCase) - single file containing all state and actions.
* Large stores: create `{name}Store/` folder with:
* `create{Name}{Feature}Slice.ts` - individual feature slices (e.g., `createQueryConfigSlice.ts`).
* `index.ts` - main store file that combines slices and exports the main store hook.
* `types.ts` - defines all types / interfaces for the store.

## Store conventions

Guidelines for defining types, default values, and action naming conventions in Zustand stores.

### Type definitions

Split store types into three interfaces:

* `{Name}Values`: state variables
* `{Name}Actions`: action functions (methods that update state)
* `{Name}State`: union of values and actions

For large stores, split `{Name}State` into feature-specific slices (`{Name}{Feature}Slice`) and
combine them back into `{Name}State` using TypeScript intersection types.

```{code-block} ts
:caption: Example: Log export store types
:emphasize-lines: 1,5,9
interface LogExportValues {
exportProgress: Nullable<number>;
}

interface LogExportActions {
setExportProgress: (newProgress: Nullable<number>) => void;
}

type LogExportState = LogExportValues & LogExportActions;
```

### Default values

* Create an object for initial state values using the `{Name}Values` interface for type safety.
* Use uppercase constant naming with `_DEFAULT` suffix

```{code-block} ts
:caption: Example: Log export store default values
const LOG_EXPORT_STORE_DEFAULT: LogExportValues = {
exportProgress: null,
};
```

### Action naming

Use clear, consistent naming patterns:

* `set{Property}` - simple state updates that directly assign a new value.
* `update{Property}` - complex logic involving API calls, multiple state updates, or asynchronous
operations.

```{code-block} ts
:caption: Example: User store actions
:emphasize-lines: 2,5
const useUserStore = create<UserState>((set, get) => ({
setName: (name) => {
set({name});
},
updateProfile: async (data) => {
set({isLoading: true});
const result = await api.updateProfile(data);
set({
profile: result,
isLoading: false
});
},
}));
```

## Feature-based slicing

When a Zustand store grows too large, split it into slices based on features such as functional
areas.

:::{warning}
Follow the principle of separation of concerns:
- Do: Slice by feature. e.g., query configuration, query results, or query controller.
- Don't: Slice by type. e.g., one file for all values or one file for all actions
:::
Comment on lines +143 to +147
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Fix list style and spacing inside admonition (MD004, MD032)

Use asterisks to match the rest of the doc and add a blank line before the list.

-:::{warning}
-Follow the principle of separation of concerns:
-- Do: Slice by feature. e.g., query configuration, query results, or query controller.
-- Don't: Slice by type. e.g., one file for all values or one file for all actions
-:::
+:::{warning}
+Follow the principle of separation of concerns:
+
+* Do: Slice by feature — e.g., query configuration, query results, or query controller.
+* Don't: Slice by type — e.g., one file for all values or one file for all actions.
+:::
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
:::{warning}
Follow the principle of separation of concerns:
- Do: Slice by feature. e.g., query configuration, query results, or query controller.
- Don't: Slice by type. e.g., one file for all values or one file for all actions
:::
:::{warning}
Follow the principle of separation of concerns:
* Do: Slice by feature — e.g., query configuration, query results, or query controller.
* Don't: Slice by type — e.g., one file for all values or one file for all actions.
:::
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

145-145: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


145-145: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


146-146: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

🤖 Prompt for AI Agents
In docs/src/dev-guide/coding-guidelines.md around lines 143 to 147, the
admonition uses hyphens and no blank line before the list which triggers
MD004/MD032; replace the hyphen list markers with asterisk markers to match the
rest of the document and insert a single blank line between the admonition intro
and the list so the list is properly separated and spaced.


Each slice should be self-contained and represent a coherent unit of application functionality.

## Store access patterns
Comment on lines +149 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Call out the known exception (viewStore) to avoid confusion

Earlier decisions favoured suppressing max‑lines for a tightly coupled viewStore rather than slicing. Add a short note so readers don’t over‑split cohesive stores.

 Each slice should be self-contained and represent a coherent unit of application functionality.
+
+:::{note}
+Exception: for highly cohesive stores (e.g., `viewStore`) where functions are tightly coupled,
+prefer suppressing the ESLint `max-lines-per-function` rule over forced slicing.
+:::

If you want, I can add the exact link to the prior decision for traceability.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Each slice should be self-contained and represent a coherent unit of application functionality.
## Store access patterns
Each slice should be self-contained and represent a coherent unit of application functionality.
:::{note}
Exception: for highly cohesive stores (e.g., `viewStore`) where functions are tightly coupled,
prefer suppressing the ESLint `max-lines-per-function` rule over forced slicing.
:::
## Store access patterns
🤖 Prompt for AI Agents
In docs/src/dev-guide/coding-guidelines.md around lines 149 to 151, add a short
note after the sentence about slices being self-contained to call out the known
exception for tightly coupled "viewStore" instances: state that viewStore may be
kept unsliced (suppressing max-lines) when it represents a cohesive, tightly
coupled read-model rather than forcing artificial slices, and optionally link to
the prior design decision for traceability; keep the note brief and directive so
readers don’t over-split cohesive stores.


There are three ways to access Zustand stores, each with its own use cases.

### Inside store creation

Use `get()` and `set()` to access the store's own states:

```{code-block} ts
:caption: Example: View format store access - inside store slice creation
:emphasize-lines: 3,5,10
const createViewFormattingSlice: StateCreator<
ViewState, [], [], ViewFormattingSlice
> = (set, get) => ({
updateIsPrettified: (newIsPrettified: boolean) => {
const {isPrettified} = get();
if (newIsPrettified === isPrettified) {
return;
}
// ...
set({isPrettified: newIsPrettified});
// ...
},
}));
```

### Inside React components

There are two access patterns depending on whether the access should be reactive or non-reactive.

#### State values

Choose access pattern based on usage:

*Reactive access* - when the value is used in JSX or hook dependency arrays, causing re-renders:

```{code-block} ts
:caption: Example: Log export store value access - reactive
const exportProgress = useLogExportStore((state) => state.exportProgress);

// The progress should be printed when `exportProgress` updates.
useEffect(() => {
console.log(exportProgress);
}, [exportProgress]);
```

*Non-reactive access* - when the value should not trigger re-renders or hook re-runs,
typically for one-time reads:

```{code-block} ts
:caption: Example: Log export store value access - non-reactive
// The progress should be printed only once when the component mounts.
useEffect(() => {
const {exportProgress} = useLogExportStore.getState();
console.log(exportProgress);
}, []);
```

#### Actions

Actions usually do not change after initialization, so always access them non-reactively:

```{code-block} ts
:caption: Example: Log export store action access - non-reactive
const handleExportButtonClick = useCallback(() => {
const {exportLogs} = useLogExportStore.getState();
exportLogs();
}, []);
```

### Outside React components

Always use non-reactive access since reactive subscriptions do not work for outside components.

```{code-block} ts
:caption: Example: An error handler that accesses the Notification store outside of any component
:emphasize-lines: 3,4,9
const handleErrorWithNotification = (e: unknown) => {
// ...
const {postPopUp} = useNotificationStore.getState();
postPopUp({
level: LOG_LEVEL.ERROR,
message: message,
timeoutMillis: DO_NOT_TIMEOUT_VALUE,
title: "Action failed",
});
};
```

[eslint-config-mjs]: https://github.com/y-scope/yscope-log-viewer/blob/main/eslint.config.mjs
[vite-worker-query-suffix]: https://vite.dev/guide/features.html#import-with-query-suffixes
[yscope-guidelines]: https://docs.yscope.com/dev-guide/contrib-guides-overview.html