2. Store factory#
There is no hand-rolled axios module per entity. All IO flows through a single PalmyraStoreFactory that serves two kinds of stores:
- Grid store — paginated list reads (sort, search, filters, offset/limit).
- Form store — single-record read / create / update / delete.
Pages don’t import URLs directly; they import an endpoint path from a central config and hand it to a template.
Configure the factory#
// src/wire/StoreFactory.ts
import { PalmyraStoreFactory } from '@palmyralabs/palmyra-wire';
import { toast } from 'react-toastify';
// Map HTTP failures to user-visible feedback. 401 kicks the session flow;
// 403 shows a permission-denied toast without tearing down the app.
let lastServerDownAt = 0;
const errorHandler = () => ({
onError: (status: number, _err: unknown) => {
switch (status) {
case 401: {
// tell any listener to clear local state, then redirect
window.dispatchEvent(new CustomEvent('session:expired'));
window.location.assign('/login');
return;
}
case 403:
toast.error("You don't have permission for this action.");
return;
case 500:
case 502:
case 503: {
const now = Date.now();
if (now - lastServerDownAt > 5000) { // 5 s cooldown
toast.error('Server is unreachable. Please retry.');
lastServerDownAt = now;
}
return;
}
}
}
});
const AppStoreFactory = new PalmyraStoreFactory({
baseUrl: '/api/palmyra',
errorHandlerFactory: errorHandler
});
export const useFormstore = (endPoint: string, options: any = {}, idKey?: string) =>
AppStoreFactory.getFormStore(options, endPoint, idKey);
export const useGridstore = (endPoint: string, options: any = {}, idKey?: string) =>
AppStoreFactory.getGridStore(options, endPoint, idKey);
export default AppStoreFactory;baseUrl: '/api/palmyra' matches the context path your backend publishes under (see backend step 1). Every endPoint you pass to the hooks is appended to this base.
Central endpoint config#
Keep all resource paths in one file. When the backend renames a handler’s @CrudMapping, only this module needs to change.
// src/config/ServiceEndpoints.ts
export const ServiceEndpoint = {
masterData: {
manufacturer: {
restApi: '/mstManufacturer',
byId: '/mstManufacturer/{id}',
},
product: {
restApi: '/mstProduct',
byId: '/mstProduct/{id}',
},
},
stockEntry: {
restApi: '/stockEntry',
byId: '/stockEntry/{id}',
},
login: {
loginApi: '/auth/login',
logoutApi: '/auth/logout',
},
};The byId path with {id} is the same shape the backend declared via secondaryMapping — the store substitutes {id} with the current record’s id automatically.
Usage shape#
Pages never see fetch or axios. They invoke the hooks directly or (more commonly) pass the endpoint into a grid/form template:
// list hook, imperative use
const store = useGridstore('/mstManufacturer');
const page = await store.queryData({ limit: 25, offset: 0, search: 'acme' });
// single-record read/create/update/delete
const form = useFormstore('/mstManufacturer/{id}', {}, 'id');
const user = await form.fetchData({ id: 42 });
await form.saveData({ id: 42, name: 'Acme Pharma' });In practice the next step’s SummaryGrid consumes the endpoint directly via options.endPoint and you never call these hooks by hand.