From 1d34a5ee7e7fa16d914e4664ccd2d23ee4dc9cc1 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Sun, 22 Jun 2025 16:01:40 -0400 Subject: [PATCH 01/16] initial commit --- docs/src/dev-guide/coding-guidelines.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index a76a45acc..fb94fcf0c 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -44,6 +44,12 @@ Examples: * `arrayIndexIdx` for a 0-based indexing variable. * `numEvents` for the total number of events. +## Zustand store actions +When implementing Zustand store actions, we follow these naming conventions: + +* Use `setX` for actions that simply change the value of a state. +* Use `updateX` for actions that do more than simply changing a value (e.g., perform additional logic, make API calls, update multiple states). + # React ## Omitting state variables from React Hooks From a8e70ecc8680ede3b5a74bfd357f0b5ad02a1f52 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:03:33 -0400 Subject: [PATCH 02/16] update Zustand store docs naming conventions & creation --- docs/src/dev-guide/coding-guidelines.md | 61 ++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index fb94fcf0c..668ea4412 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -44,12 +44,6 @@ Examples: * `arrayIndexIdx` for a 0-based indexing variable. * `numEvents` for the total number of events. -## Zustand store actions -When implementing Zustand store actions, we follow these naming conventions: - -* Use `setX` for actions that simply change the value of a state. -* Use `updateX` for actions that do more than simply changing a value (e.g., perform additional logic, make API calls, update multiple states). - # React ## Omitting state variables from React Hooks @@ -59,6 +53,61 @@ 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 + +## File Naming + +When creating Zustand stores, we follow these naming conventions: + +* The store file name should be `xxxStore.ts`, where xxx is the name of the store in camelCase. +* If the file is too large and needs to be sliced, set the folder name to `xxxStore` and the store +creation file name to `index.ts`. + +## Creation + +### Values and actions + +Zustand stores two types of data: values, which are state variables, +and actions, which are functions that modify these states. + +We split values and actions in two different interfaces during the definition: +`XxxValues` for values and `XxxActions` for actions, both in PascalCase. +Then we combine them with a type called `XxxState` for store creation. Here's a real world example: + +```ts +interface LogExportValues { + exportProgress: Nullable; +} + +interface LogExportActions { + setExportProgress: (newProgress: Nullable) => void; +} + +type LogExportState = LogExportValues & LogExportActions; +``` + +### Default values + +We define default values for state variables during the creation. +The default values should be in CAPITAL_SNAKE_CASE. Here's the continuation of the previous example: + +```ts +const LOG_EXPORT_STORE_DEFAULT: LogExportValues = { + exportProgress: null, +}; +``` + +Notice that we define `LOG_EXPORT_STORE_DEFAULT` using `LogExportValues` interface, which can help +type check the default values. + +### Actions Naming + +When implementing Zustand store actions, we follow these naming conventions: + +* Use `setXXX` for actions that simply change the value of a state. +* Use `updateXXX` for actions that do more than simply changing a value (e.g., perform additional +logic, make API calls, update multiple states). + [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 From 4598dc91b6e4d4f3620eac84dcd9f2a4f926cab8 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:47:48 -0400 Subject: [PATCH 03/16] add zustand slicing and store usage docs. --- docs/src/dev-guide/coding-guidelines.md | 65 ++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index 668ea4412..27d1e2779 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -67,12 +67,13 @@ creation file name to `index.ts`. ### Values and actions -Zustand stores two types of data: values, which are state variables, -and actions, which are functions that modify these states. +Zustand stores two types of data: state variables, i.e. values, and actions, which are functions that +modify these state variables. -We split values and actions in two different interfaces during the definition: -`XxxValues` for values and `XxxActions` for actions, both in PascalCase. -Then we combine them with a type called `XxxState` for store creation. Here's a real world example: +We split values and actions in two different interfaces during the definition: `XxxValues` for values and `XxxActions` +for actions, both in PascalCase. Then we combine them with a type called `XxxState` for store creation. This is + +Here's a real-world example: ```ts interface LogExportValues { @@ -104,9 +105,57 @@ type check the default values. When implementing Zustand store actions, we follow these naming conventions: -* Use `setXXX` for actions that simply change the value of a state. -* Use `updateXXX` for actions that do more than simply changing a value (e.g., perform additional -logic, make API calls, update multiple states). +* Use `setXxx` for actions that simply change the value of a state. +* Use `updateXxx` for actions that do more than simply changing a value (e.g., perform additional logic, make API calls, +update multiple states). + +## Feature-based slicing + +When a Zustand store file becomes too large, we should slice it based on features. Avoid grouping by type (e.g., all +values / actions in one object) — it's a common anti-pattern. + +## Store Usage + +There are three ways to access Zustand store values and actions: +* `get() & set()` +* `const yyy = useXxxStore((state) => state.yyy);` +* `const {yyy, zzz} = useXxxStore.getState();` + +### Inside zustand creation + +When accessing store values inside the store creation function, we use`set()` and `get()`. Here's an example: + +```ts +const useLogExportStore = create((get, set) => ({ + exportLogs: () => { + const logExportManager = new LogExportManager(); + set({logExportManager}); + + const {exportProgress} = get(); + // If name doesn't match, use colon to separate the variable name from the value. + set({exportProgress: EXPORT_LOGS_PROGRESS_VALUE_MIN}); + }, +})); +``` + + +### Inside React components + +#### State variables +`const vvv = useXxxStore((state) => state.vvv);` + +This will assure whenever `vvv` changes, the component will re-render. For variables that are not subscribing to changes, +use `const {vvv} = useXxxStore.getState()`; instead. This avoids unnecessary dependencies in react hooks. + +#### Actions +`const {aaa} = useXxxStore.getState();` + +Since actions don't change after initialization, we can access them in a non-reactive way. + +### Outside React components + +When accessing Zustand store values or actions outside React components, we use +`const {yyy, zzz} = useXxxStore.getState();`. [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 From 8c8be4d420a966749350009fd401110442e4c3a9 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:41:51 -0400 Subject: [PATCH 04/16] Apply suggestions from code review Co-authored-by: Junhao Liao --- docs/src/dev-guide/coding-guidelines.md | 87 ++++++++++++------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index 27d1e2779..2e53354f3 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -59,21 +59,19 @@ that corresponds to the `logEventNum` state variable. When creating Zustand stores, we follow these naming conventions: -* The store file name should be `xxxStore.ts`, where xxx is the name of the store in camelCase. -* If the file is too large and needs to be sliced, set the folder name to `xxxStore` and the store -creation file name to `index.ts`. +* Store files: `{name}Store.ts` (camelCase). +* Large stores: create `{name}Store/` folder with: + * `index.ts` - main store file that combines slices. + * `create{Name}{Feature}Slice.ts` - individual feature slices (e.g., `createQueryConfigSlice.ts`). -## Creation +## Store structure -### Values and actions +### Type definitions -Zustand stores two types of data: state variables, i.e. values, and actions, which are functions that -modify these state variables. - -We split values and actions in two different interfaces during the definition: `XxxValues` for values and `XxxActions` -for actions, both in PascalCase. Then we combine them with a type called `XxxState` for store creation. This is - -Here's a real-world example: +Split store types into three interfaces: +* `{Name}Values` - state variables +* `{Name}Actions` - action functions +* `{Name}State` or `{Name}Slice` - combined type ```ts interface LogExportValues { @@ -85,45 +83,47 @@ interface LogExportActions { } type LogExportState = LogExportValues & LogExportActions; -``` ### Default values -We define default values for state variables during the creation. -The default values should be in CAPITAL_SNAKE_CASE. Here's the continuation of the previous example: +* Create an object for initial state values. +* Type with `{Name}Values` interface for validation. ```ts const LOG_EXPORT_STORE_DEFAULT: LogExportValues = { exportProgress: null, }; -``` -Notice that we define `LOG_EXPORT_STORE_DEFAULT` using `LogExportValues` interface, which can help -type check the default values. +### Action naming -### Actions Naming +* `set{Property}` - simple state updates. +* `update{Property}` - complex logic, API calls, or multiple state updates. -When implementing Zustand store actions, we follow these naming conventions: - -* Use `setXxx` for actions that simply change the value of a state. -* Use `updateXxx` for actions that do more than simply changing a value (e.g., perform additional logic, make API calls, -update multiple states). +```ts +const useUserStore = create((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 file becomes too large, we should slice it based on features. Avoid grouping by type (e.g., all -values / actions in one object) — it's a common anti-pattern. +When a Zustand store file becomes too large, we should slice it based on features. Avoid slicing by type (e.g., all +values / actions in one object) - it's a common anti-pattern. -## Store Usage +## Store access patterns -There are three ways to access Zustand store values and actions: -* `get() & set()` -* `const yyy = useXxxStore((state) => state.yyy);` -* `const {yyy, zzz} = useXxxStore.getState();` +### Inside store creation -### Inside zustand creation - -When accessing store values inside the store creation function, we use`set()` and `get()`. Here's an example: +Use `get()` and `set()` to access the store's own states: ```ts const useLogExportStore = create((get, set) => ({ @@ -132,30 +132,25 @@ const useLogExportStore = create((get, set) => ({ set({logExportManager}); const {exportProgress} = get(); - // If name doesn't match, use colon to separate the variable name from the value. set({exportProgress: EXPORT_LOGS_PROGRESS_VALUE_MIN}); }, })); -``` ### Inside React components -#### State variables -`const vvv = useXxxStore((state) => state.vvv);` - -This will assure whenever `vvv` changes, the component will re-render. For variables that are not subscribing to changes, -use `const {vvv} = useXxxStore.getState()`; instead. This avoids unnecessary dependencies in react hooks. - #### Actions -`const {aaa} = useXxxStore.getState();` -Since actions don't change after initialization, we can access them in a non-reactive way. +Actions usually do not change after initialization, so always access them non-reactively: +```ts +const handleExportButtonClick = useCallback(() => { + const {exportLogs} = useLogExportStore.getState(); + exportLogs(); +}, []); ### Outside React components -When accessing Zustand store values or actions outside React components, we use -`const {yyy, zzz} = useXxxStore.getState();`. +Always use non-reactive access since reactive subscriptions do not work outside components. [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 From 22526a6a243dd2f2b0ffa73f714b7ce7ebab33df Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:48:39 -0400 Subject: [PATCH 05/16] patch previous commit --- docs/src/dev-guide/coding-guidelines.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index 2e53354f3..6a91576f8 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -83,6 +83,7 @@ interface LogExportActions { } type LogExportState = LogExportValues & LogExportActions; +``` ### Default values @@ -93,6 +94,7 @@ type LogExportState = LogExportValues & LogExportActions; const LOG_EXPORT_STORE_DEFAULT: LogExportValues = { exportProgress: null, }; +``` ### Action naming @@ -113,6 +115,7 @@ const useUserStore = create((set, get) => ({ }); }, })); +``` ## Feature-based slicing @@ -135,7 +138,7 @@ const useLogExportStore = create((get, set) => ({ set({exportProgress: EXPORT_LOGS_PROGRESS_VALUE_MIN}); }, })); - +``` ### Inside React components @@ -147,6 +150,7 @@ const handleExportButtonClick = useCallback(() => { const {exportLogs} = useLogExportStore.getState(); exportLogs(); }, []); +``` ### Outside React components From 2159be260a6410a8e0f30cc016a25b7130007d52 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:24:43 -0400 Subject: [PATCH 06/16] apply the rest of suggestions --- docs/src/dev-guide/coding-guidelines.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index 6a91576f8..ee681e6ea 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -152,6 +152,29 @@ const handleExportButtonClick = useCallback(() => { }, []); ``` +#### State values + +Choose access pattern based on usage: + +*Reactive access* - when the value is used in JSX or hook dependency arrays: +```ts +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: +```ts +// The progress should be printed only once when the component mounts. +useEffect(() => { + const {exportProgress} = useLogExportStore.getState(); + console.log(exportProgress); +}, []); +``` + ### Outside React components Always use non-reactive access since reactive subscriptions do not work outside components. From f92f375d682919e1b7fbf4e61d08b44010f1134c Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:35:04 -0400 Subject: [PATCH 07/16] move state values section --- docs/src/dev-guide/coding-guidelines.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index ee681e6ea..dbc376202 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -142,16 +142,6 @@ const useLogExportStore = create((get, set) => ({ ### Inside React components -#### Actions - -Actions usually do not change after initialization, so always access them non-reactively: -```ts -const handleExportButtonClick = useCallback(() => { - const {exportLogs} = useLogExportStore.getState(); - exportLogs(); -}, []); -``` - #### State values Choose access pattern based on usage: @@ -175,6 +165,16 @@ useEffect(() => { }, []); ``` +#### Actions + +Actions usually do not change after initialization, so always access them non-reactively: +```ts +const handleExportButtonClick = useCallback(() => { + const {exportLogs} = useLogExportStore.getState(); + exportLogs(); +}, []); +``` + ### Outside React components Always use non-reactive access since reactive subscriptions do not work outside components. From 56a46b5bca37900744c36a807915e6d9c70172ca Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:39:05 -0400 Subject: [PATCH 08/16] address coderabbit review --- docs/src/dev-guide/coding-guidelines.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index dbc376202..d1ab3fd31 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -69,6 +69,7 @@ When creating Zustand stores, we follow these naming conventions: ### Type definitions Split store types into three interfaces: + * `{Name}Values` - state variables * `{Name}Actions` - action functions * `{Name}State` or `{Name}Slice` - combined type @@ -147,8 +148,9 @@ const useLogExportStore = create((get, set) => ({ Choose access pattern based on usage: *Reactive access* - when the value is used in JSX or hook dependency arrays: + ```ts -const exportProgress = useLogExportStore((state) => state.exportProgress)); +const exportProgress = useLogExportStore((state) => state.exportProgress); // The progress should be printed when `exportProgress` updates. useEffect(() => { @@ -157,6 +159,7 @@ useEffect(() => { ``` *Non-reactive access* - when the value should not trigger re-renders or hook re-runs: + ```ts // The progress should be printed only once when the component mounts. useEffect(() => { @@ -168,6 +171,7 @@ useEffect(() => { #### Actions Actions usually do not change after initialization, so always access them non-reactively: + ```ts const handleExportButtonClick = useCallback(() => { const {exportLogs} = useLogExportStore.getState(); From 678c4cf7236e0be7cebd6d9ac5ca578e9677ed21 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:17:29 -0400 Subject: [PATCH 09/16] Apply suggestions from code review Co-authored-by: Junhao Liao --- docs/src/dev-guide/coding-guidelines.md | 27 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index d1ab3fd31..8ee7fdc1e 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -74,7 +74,9 @@ Split store types into three interfaces: * `{Name}Actions` - action functions * `{Name}State` or `{Name}Slice` - combined type -```ts +```{code-block} ts +:caption: Example: Log export store types +:emphasize-lines: 1,5,9 interface LogExportValues { exportProgress: Nullable; } @@ -91,7 +93,8 @@ type LogExportState = LogExportValues & LogExportActions; * Create an object for initial state values. * Type with `{Name}Values` interface for validation. -```ts +```{code-block} ts +:caption: Example: Log export store default values const LOG_EXPORT_STORE_DEFAULT: LogExportValues = { exportProgress: null, }; @@ -102,7 +105,9 @@ const LOG_EXPORT_STORE_DEFAULT: LogExportValues = { * `set{Property}` - simple state updates. * `update{Property}` - complex logic, API calls, or multiple state updates. -```ts +```{code-block} ts +:caption: Example: Log export store actions +:emphasize-lines: 2,5 const useUserStore = create((set, get) => ({ setName: (name) => { set({name}); @@ -129,12 +134,11 @@ values / actions in one object) - it's a common anti-pattern. Use `get()` and `set()` to access the store's own states: -```ts +```{code-block} ts +:caption: Example: Log export store access - inside store creation const useLogExportStore = create((get, set) => ({ exportLogs: () => { - const logExportManager = new LogExportManager(); - set({logExportManager}); - + // ... const {exportProgress} = get(); set({exportProgress: EXPORT_LOGS_PROGRESS_VALUE_MIN}); }, @@ -149,7 +153,8 @@ Choose access pattern based on usage: *Reactive access* - when the value is used in JSX or hook dependency arrays: -```ts +```{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. @@ -160,7 +165,8 @@ useEffect(() => { *Non-reactive access* - when the value should not trigger re-renders or hook re-runs: -```ts +```{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(); @@ -172,7 +178,8 @@ useEffect(() => { Actions usually do not change after initialization, so always access them non-reactively: -```ts +```{code-block} ts +:caption: Example: Log export store action access - non-reactive const handleExportButtonClick = useCallback(() => { const {exportLogs} = useLogExportStore.getState(); exportLogs(); From 8f96f8e0f226d4db91984fa2ccf8b5ffba885c5f Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:18:23 -0400 Subject: [PATCH 10/16] remove extra spaces --- docs/src/dev-guide/coding-guidelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index 8ee7fdc1e..e2128ed66 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -71,7 +71,7 @@ When creating Zustand stores, we follow these naming conventions: Split store types into three interfaces: * `{Name}Values` - state variables -* `{Name}Actions` - action functions +* `{Name}Actions` - action functions * `{Name}State` or `{Name}Slice` - combined type ```{code-block} ts From f5822d684b7eafe12a7a40d4850d3f00e0cdb8a4 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:08:20 -0400 Subject: [PATCH 11/16] Update examples Co-authored-by: Junhao Liao --- docs/src/dev-guide/coding-guidelines.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index e2128ed66..0eedf3290 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -135,12 +135,19 @@ values / actions in one object) - it's a common anti-pattern. Use `get()` and `set()` to access the store's own states: ```{code-block} ts -:caption: Example: Log export store access - inside store creation -const useLogExportStore = create((get, set) => ({ - exportLogs: () => { - // ... - const {exportProgress} = get(); - set({exportProgress: EXPORT_LOGS_PROGRESS_VALUE_MIN}); +: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}); + // ... }, })); ``` From d02feb6a0a44d3c50b3c2f8fec53bb61548b1601 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:17:25 -0400 Subject: [PATCH 12/16] address comment --- docs/src/dev-guide/coding-guidelines.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index 0eedf3290..a0d683405 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -72,7 +72,11 @@ Split store types into three interfaces: * `{Name}Values` - state variables * `{Name}Actions` - action functions -* `{Name}State` or `{Name}Slice` - combined type +* `{Name}State` - union of values and actions + +When store gets too large, we slice `{Name}State` to `{Name}{Feature}Slice`. +Then we unionize these slices to `{Name}State`. + ```{code-block} ts :caption: Example: Log export store types From 08e9a07c803e5766221e6f3c43b3da14245a3b18 Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:29:58 -0400 Subject: [PATCH 13/16] specify when to slice --- docs/src/dev-guide/coding-guidelines.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index a0d683405..5c1b329fb 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -74,9 +74,8 @@ Split store types into three interfaces: * `{Name}Actions` - action functions * `{Name}State` - union of values and actions -When store gets too large, we slice `{Name}State` to `{Name}{Feature}Slice`. -Then we unionize these slices to `{Name}State`. - +When store gets too large and exceeds eslint `max-lines-per-function limit`, we slice `{Name}State` +to `{Name}{Feature}Slice`. Then we unionize these slices to `{Name}State`. ```{code-block} ts :caption: Example: Log export store types From 19c795974ef93d9deefe88c1fea908a5fe5b0c0f Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:47:27 -0400 Subject: [PATCH 14/16] Apply suggestions from code review --- docs/src/dev-guide/coding-guidelines.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index 5c1b329fb..bfc7e3ef6 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -74,8 +74,9 @@ Split store types into three interfaces: * `{Name}Actions` - action functions * `{Name}State` - union of values and actions -When store gets too large and exceeds eslint `max-lines-per-function limit`, we slice `{Name}State` -to `{Name}{Feature}Slice`. Then we unionize these slices to `{Name}State`. +When the store becomes too large and exceeds the ESLint `max-lines-per-function` limit, split +`{Name}State` into feature-specific slices (`{Name}{Feature}Slice`) and then combine those slices +back into `{Name}State`. ```{code-block} ts :caption: Example: Log export store types From e5842c40bf5182078eb6c0a9adb3dcb39759c182 Mon Sep 17 00:00:00 2001 From: Junhao Liao Date: Fri, 12 Sep 2025 20:29:17 -0400 Subject: [PATCH 15/16] docs: Update Zustand store naming and structure guidelines for clarity and consistency --- docs/src/dev-guide/coding-guidelines.md | 55 ++++++++++++++++++------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index bfc7e3ef6..db00e83f2 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -59,10 +59,11 @@ that corresponds to the `logEventNum` state variable. When creating Zustand stores, we follow these naming conventions: -* Store files: `{name}Store.ts` (camelCase). +* Simple stores: `{name}Store.ts` (camelCase) - single file containing all state and actions. * Large stores: create `{name}Store/` folder with: - * `index.ts` - main store file that combines slices. * `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 structure @@ -71,12 +72,11 @@ When creating Zustand stores, we follow these naming conventions: Split store types into three interfaces: * `{Name}Values` - state variables -* `{Name}Actions` - action functions +* `{Name}Actions` - action functions (methods that update state) * `{Name}State` - union of values and actions -When the store becomes too large and exceeds the ESLint `max-lines-per-function` limit, split -`{Name}State` into feature-specific slices (`{Name}{Feature}Slice`) and then combine those slices -back into `{Name}State`. +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 @@ -94,8 +94,8 @@ type LogExportState = LogExportValues & LogExportActions; ### Default values -* Create an object for initial state values. -* Type with `{Name}Values` interface for validation. +* 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 @@ -106,8 +106,11 @@ const LOG_EXPORT_STORE_DEFAULT: LogExportValues = { ### Action naming -* `set{Property}` - simple state updates. -* `update{Property}` - complex logic, API calls, or multiple state updates. +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: Log export store actions @@ -129,8 +132,16 @@ const useUserStore = create((set, get) => ({ ## Feature-based slicing -When a Zustand store file becomes too large, we should slice it based on features. Avoid slicing by type (e.g., all -values / actions in one object) - it's a common anti-pattern. +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 +::: + +Each slice should be self-contained and represent a coherent unit of application functionality. ## Store access patterns @@ -162,7 +173,7 @@ const createViewFormattingSlice: StateCreator< Choose access pattern based on usage: -*Reactive access* - when the value is used in JSX or hook dependency arrays: +*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 @@ -174,7 +185,8 @@ useEffect(() => { }, [exportProgress]); ``` -*Non-reactive access* - when the value should not trigger re-renders or hook re-runs: +*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 @@ -201,6 +213,21 @@ const handleExportButtonClick = useCallback(() => { Always use non-reactive access since reactive subscriptions do not work 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 From 515406c7cdaab466bbfeb4085b1bd5cfe48c43ba Mon Sep 17 00:00:00 2001 From: Henry8192 <50559854+Henry8192@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:18:07 -0400 Subject: [PATCH 16/16] address review --- docs/src/dev-guide/coding-guidelines.md | 33 ++++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/src/dev-guide/coding-guidelines.md b/docs/src/dev-guide/coding-guidelines.md index db00e83f2..ff1b7496a 100644 --- a/docs/src/dev-guide/coding-guidelines.md +++ b/docs/src/dev-guide/coding-guidelines.md @@ -55,7 +55,10 @@ that corresponds to the `logEventNum` state variable. # Zustand -## File Naming +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: @@ -65,17 +68,19 @@ When creating Zustand stores, we follow these naming conventions: * `index.ts` - main store file that combines slices and exports the main store hook. * `types.ts` - defines all types / interfaces for the store. -## Store structure +## 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 +* `{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 +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 @@ -113,7 +118,7 @@ Use clear, consistent naming patterns: operations. ```{code-block} ts -:caption: Example: Log export store actions +:caption: Example: User store actions :emphasize-lines: 2,5 const useUserStore = create((set, get) => ({ setName: (name) => { @@ -133,18 +138,20 @@ const useUserStore = create((set, get) => ({ ## Feature-based slicing When a Zustand store grows too large, split it into slices based on features such as functional -areas. +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 +- 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 ::: Each slice should be self-contained and represent a coherent unit of application functionality. ## Store access patterns +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: @@ -169,6 +176,8 @@ const createViewFormattingSlice: StateCreator< ### 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: @@ -185,7 +194,7 @@ useEffect(() => { }, [exportProgress]); ``` -*Non-reactive access* - when the value should not trigger re-renders or hook re-runs, +*Non-reactive access* - when the value should not trigger re-renders or hook re-runs, typically for one-time reads: ```{code-block} ts @@ -211,7 +220,7 @@ const handleExportButtonClick = useCallback(() => { ### Outside React components -Always use non-reactive access since reactive subscriptions do not work outside 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