- All should be exported at the bottom of the file (always)
To make code consistent, the following standardized structure for the store must be applied:
[store][name - of - feature - folder];
index.ts; // file containing hooks "useFeatureNameState" hook and initialization logic
models.ts; // defines the shape of the state
actions.ts; // contains reusable logic for store changes
selectors.ts; // contains reusable logic for state selectionFor cases where multiple actions need to be called from features, wrap these calls within an act, and put there:
[acts-folder]
-name-of-function.act.ts// [store]/[images]/models.ts file
import type { Transaction } from 'development-kit/utility-types';
type UploadImageState = Transaction;
export type { UploadImageState };Rules:
- Required for every use case.
- Use the
Statepostfix in the type name (e.g.,UploadImageState). - Always export as a
type.
// [store]/[images]/index.ts file
import { state } from 'development-kit/state';
import type { UploadImageState } from './models';
const useUploadImageState = state<UploadImageState>({ is: `idle` });
export { useUploadImageState };Rules:
- Required for every use case.
- Exports only created hook with name
useUploadImageStateand adds postfixStateat the end.
// [store]/[images]/actions.ts file
import { useUploadImageState } from '.';
import type { UploadImageState } from './models';
const resetImagesAction = (state: UploadImageState): void => {
if (state.is !== `idle`) useUploadImageState.swap(state);
};
export { resetImagesAction };Rules:
- Optional — create actions only if needed to avoid duplication.
- Use the
Actionpostfix in the function name. - Always define a return type.
- Actions are always synchronous (
sync) functions without side effects.
// [store]/[images]/selectors.ts file
import { useUploadImageState } from '.';
import type { UploadImageState } from './models';
const imageSelector = (state: UploadImageState): UploadImageState['images'] =>
state.images;
export { imageSelector };Rules:
- Selectors must always have the
Selectorpostfix in their name. - Explicitly define return types (e.g.,
UploadImageState['images']). - Use selectors to avoid duplication in state access logic.
// [acts]/upload-image.act.ts
import { getAPI, parseError } from 'api-4markdown';
import { readFileAsBase64 } from 'development-kit/file-reading';
import type { API4MarkdownDto } from 'api-4markdown-contracts';
import { useUploadImageState } from 'store/images';
import type { AsyncResult } from 'development-kit/utility-types';
const uploadImageAct = async (
image: File,
): AsyncResult<API4MarkdownDto<'uploadImage'>> => {
try {
useUploadImageState.swap({ is: `busy` });
const data = await getAPI().call(`uploadImage`)({
image: await readFileAsBase64(image),
});
useUploadImageState.swap({ is: `ok` });
return { is: `ok`, data };
} catch (rawError: unknown) {
const error = parseError(rawError);
useUploadImageState.swap({ is: `fail`, error });
return { is: `fail`, error };
}
};
export { uploadImageAct };Rules:
- Acts must use the
Actpostfix in their name. - Globally available functions encapsulating complex processes.
- Combine multiple steps, state updates, actions, side effects, and API interactions in a reusable function.
- Acts must always have a
return type. - Acts can be synchronous (
sync) or asynchronous (async). - If there is a situation where logic needs to be handled across different stores, it should be moved to a separate act.
The following rules should be applied:
- If a component is "local" and does not have many properties passed to it, consider defining its contract without an additional
type.
// A component used primarily for readability, to avoid large chunks of JSX
const SocialShare = ({ content }: { content: string }) => {};