This project adheres to YScope's contribution guidelines as well as the project-specific guidelines below.
When importing web worker files, use Vite's ?worker query suffix syntax:
import MainWorker from "../services/MainWorker.worker?worker";
const worker = new MainWorker();This special syntax tells Vite to transform the import as a web worker constructor. See Vite's web worker documentation for more details.
Name web worker files with the extension, .worker.ts. This is to:
- follow standard practices.
- allow eslint.config.mjs to ignore
.worker.tsfiles, suppressingeslint-plugin-import:import/defaulterrors caused by Vite's?workerimport syntax.
To differentiate variables that use different starting indexes (0 vs. 1), use the following naming convention:
- 0-based indexing variable names should end with the suffix
Idx. - 1-based indexing variable names should end with the suffix
Num.
Similarly, variables that represent a total number of items should be named with the prefix num.
Examples:
logEventNumfor a 1-based indexing variable.arrayIndexIdxfor a 0-based indexing variable.numEventsfor the total number of events.
To avoid including a state variable in a React Hook's dependency array, you can use a reference
(mirror) to hold the current value of the state variable. The reference should use the same name as
the state variable with an additional Ref suffix. E.g., logEventNumRef is the reference variable
that corresponds to the logEventNum state variable.
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.
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.
Guidelines for defining types, default values, and action naming conventions in Zustand stores.
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.
: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;
- Create an object for initial state values using the
{Name}Valuesinterface for type safety. - Use uppercase constant naming with
_DEFAULTsuffix
:caption: Example: Log export store default values
const LOG_EXPORT_STORE_DEFAULT: LogExportValues = {
exportProgress: null,
};
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.
: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
});
},
}));
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.
There are three ways to access Zustand stores, each with its own use cases.
Use get() and set() to access the store's own states:
: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});
// ...
},
}));
There are two access patterns depending on whether the access should be reactive or non-reactive.
Choose access pattern based on usage:
Reactive access - when the value is used in JSX or hook dependency arrays, causing re-renders:
: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:
: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 usually do not change after initialization, so always access them non-reactively:
:caption: Example: Log export store action access - non-reactive
const handleExportButtonClick = useCallback(() => {
const {exportLogs} = useLogExportStore.getState();
exportLogs();
}, []);
Always use non-reactive access since reactive subscriptions do not work for outside components.
: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",
});
};