Stack9 React Package
The @april9au/stack9-react package provides React hooks, providers, and components for building Stack9 frontend applications. It serves as the core React integration layer for Stack9, offering data fetching hooks, authentication management, and application context providers.
Installation
npm install @april9au/stack9-react
Package Structure
@april9au/stack9-react
├── components/ # UI components
├── hooks/ # React hooks for data fetching and state management
├── models/ # TypeScript interfaces and types
├── providers/ # React context providers
└── utils/ # Utility functions
Providers
AppProvider
The root provider that initializes Stack9 services and makes them available throughout your application.
import { AppProvider } from '@april9au/stack9-react';
import { axiosProvider, stack9Config } from './config';
function App() {
return (
<AppProvider axiosFactory={axiosProvider} config={stack9Config}>
{/* Your app components */}
</AppProvider>
);
}
Props:
axiosFactory: (config: AppConfig) => AxiosInstance- Factory function to create Axios instanceconfig: AppConfig- Stack9 application configurationchildren: ReactNode- Child components
Provides:
- Access to all Stack9 SDK services (entity, query, screen, user services)
- Centralized API fetcher instance
- Application configuration
AuthProvider
Manages authentication state and provides authentication utilities.
import { AuthProvider } from '@april9au/stack9-react';
function App() {
return (
<AuthProvider
clientId="your-client-id"
onError={(err) => console.error(err)}
>
{/* Your authenticated app */}
</AuthProvider>
);
}
Props:
clientId: string- OAuth client identifier (optional for backoffice)onError: (error: Error) => void- Error handler callback
Context Interface (AuthContextInterface):
user: User | null- Current authenticated userisAuthenticated: boolean- Authentication statuslogin: (credentials: LoginCredentials) => Promise<void>logout: () => Promise<void>refreshToken: () => Promise<void>
useStack9 Hook
Access Stack9 services and configuration from any component.
import { useStack9 } from '@april9au/stack9-react';
function MyComponent() {
const {
entityService,
queryService,
screenService,
config
} = useStack9();
// Use services to interact with Stack9 API
}
Returns:
fetcher: ApiFetcher- API fetcher instanceentityService: ApiEntityService- Entity CRUD operationsqueryService: ApiQueryService- Query execution servicescreenService: ApiScreenService- Screen configuration servicescreenFolderService: ApiScreenFolderService- Screen folder managementuserService: ApiUserService- User management serviceappService: ApiAppService- Application serviceappPermissionService: ApiAppPermissionService- Permission managementappAutomationCronJobService: ApiAppAutomationCronJobService- Automation servicetaskService: ApiTaskService- Task management serviceconfig: AppConfig- Application configuration
useStack9Auth Hook
Access authentication context and utilities.
import { useStack9Auth } from '@april9au/stack9-react';
function Profile() {
const { user, isAuthenticated, logout } = useStack9Auth();
if (!isAuthenticated) {
return <div>Please log in</div>;
}
return (
<div>
Welcome, {user.name}!
<button onClick={logout}>Logout</button>
</div>
);
}
Data Fetching Hooks
useScreens
Fetch all available screens with caching via SWR.
import { useScreens } from '@april9au/stack9-react';
function Navigation() {
const { data, error, isLoading, mutate } = useScreens();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading screens</div>;
return (
<nav>
{data?.map(screen => (
<Link key={screen.id} to={screen.path}>
{screen.title}
</Link>
))}
</nav>
);
}
Returns:
data: Screen[]- Array of screen configurationserror: Error- Error object if fetch failedisLoading: boolean- Loading statemutate: Function- SWR mutate function for cache invalidation
useScreenDetailByPath
Fetch screen configuration by route path.
import { useScreenDetailByPath } from '@april9au/stack9-react';
function DynamicScreen({ route }) {
const {
screen,
screenName,
error,
isLoading
} = useScreenDetailByPath(route);
if (isLoading) return <LoadingScreen />;
if (error) return <ErrorScreen error={error} />;
return <RenderScreen config={screen} />;
}
Parameters:
route: string- The route path to fetch screen for
Returns:
screen: ScreenConfig- Complete screen configurationscreenName: string- Screen identifiererror: Error- Error object if fetch failedisLoading: boolean- Loading state
useEntitySchema
Fetch entity schema definition for dynamic form generation.
import { useEntitySchema } from '@april9au/stack9-react';
function EntityForm({ entityName, entityId }) {
const { schema, isLoading, error } = useEntitySchema(entityName, entityId);
if (isLoading) return <Spinner />;
if (error) return <Alert type="error">{error.message}</Alert>;
return (
<Form schema={schema}>
{/* Generate form fields based on schema */}
</Form>
);
}
Parameters:
entityName: string- Name of the entity (e.g., 'customer', 'order')entityId?: number- Optional entity ID for edit mode
Returns:
schema: EntitySchema- Entity schema definitionisLoading: boolean- Loading stateerror: Error- Error object if fetch failed
useDropdownSearch
Search and filter dropdown options with debouncing.
import { useDropdownSearch } from '@april9au/stack9-react';
function SearchableDropdown() {
const {
options,
isLoading,
searchTerm,
setSearchTerm,
selectedValue,
setSelectedValue
} = useDropdownSearch(
'customers', // entity name
'name', // search field
'id', // value field
'name', // label field
{ status: 'active' } // additional filters
);
return (
<Select
showSearch
loading={isLoading}
value={selectedValue}
onSearch={setSearchTerm}
onChange={setSelectedValue}
options={options}
/>
);
}
Parameters:
entityName: string- Entity to searchsearchField: string- Field to search invalueField: string- Field to use as option valuelabelField: string- Field to use as option labelfilters?: object- Additional filter criteriadebounceMs?: number- Debounce delay (default: 300ms)
Returns:
options: LabelValue[]- Array of dropdown optionsisLoading: boolean- Loading statesearchTerm: string- Current search termsetSearchTerm: Function- Update search termselectedValue: any- Currently selected valuesetSelectedValue: Function- Update selected value
Entity Management Hooks
useEntityAttachments
Manage file attachments for entities.
import { useEntityAttachments } from '@april9au/stack9-react';
function AttachmentManager({ entityKey, entityId }) {
const {
attachments,
isLoading,
uploadAttachment,
deleteAttachment,
downloadAttachment
} = useEntityAttachments(entityKey, entityId);
const handleUpload = async (file) => {
await uploadAttachment(file);
// Attachment uploaded and list refreshed
};
return (
<div>
<FileUploader onUpload={handleUpload} />
<AttachmentList
items={attachments}
onDelete={deleteAttachment}
onDownload={downloadAttachment}
/>
</div>
);
}
Parameters:
entityKey: string- Entity type identifierentityID: number- Entity instance ID
Returns:
attachments: Attachment[]- List of attachmentsisLoading: boolean- Loading stateuploadAttachment: (file: File) => Promise<void>deleteAttachment: (id: number) => Promise<void>downloadAttachment: (id: number) => void
useEntityComments
Manage comments/notes for entities.
import { useEntityComments } from '@april9au/stack9-react';
function CommentsSection({ entityKey, entityId }) {
const {
comments,
isLoading,
addComment,
updateComment,
deleteComment
} = useEntityComments(entityKey, entityId);
const handleSubmit = async (text) => {
await addComment({ text, isPrivate: false });
};
return (
<div>
<CommentForm onSubmit={handleSubmit} />
<CommentList
comments={comments}
onEdit={updateComment}
onDelete={deleteComment}
/>
</div>
);
}
Parameters:
entityKey: string- Entity type identifierentityID: number- Entity instance ID
Returns:
comments: Comment[]- List of commentsisLoading: boolean- Loading stateaddComment: (data: CommentData) => Promise<void>updateComment: (id: number, data: CommentData) => Promise<void>deleteComment: (id: number) => Promise<void>
useEntityLogs
Fetch audit logs for entity changes.
import { useEntityLogs } from '@april9au/stack9-react';
function AuditLog({ entityKey, entityId }) {
const { logs, isLoading, error } = useEntityLogs(entityKey, entityId);
if (isLoading) return <Spinner />;
return (
<Timeline>
{logs?.map(log => (
<Timeline.Item key={log.id}>
<div>{log.action} by {log.user}</div>
<div>{log.timestamp}</div>
<pre>{JSON.stringify(log.changes, null, 2)}</pre>
</Timeline.Item>
))}
</Timeline>
);
}
Parameters:
entityKey: string- Entity type identifierentityID: number- Entity instance ID
Returns:
logs: EntityLog[]- Array of audit log entriesisLoading: boolean- Loading stateerror: Error- Error object if fetch failed
useEntityTasks
Manage tasks associated with entities.
import { useEntityTasks } from '@april9au/stack9-react';
function TaskList({ entityKey, entityId }) {
const {
tasks,
isLoading,
createTask,
updateTask,
deleteTask,
completeTask
} = useEntityTasks(entityKey, entityId);
const handleCreateTask = async () => {
await createTask({
title: 'Follow up with customer',
dueDate: new Date(),
assignee: currentUser.id
});
};
return (
<div>
<Button onClick={handleCreateTask}>Add Task</Button>
<TaskList
tasks={tasks}
onComplete={completeTask}
onUpdate={updateTask}
onDelete={deleteTask}
/>
</div>
);
}
Parameters:
entityKey: string- Entity type identifierentityID: number- Entity instance ID
Returns:
tasks: Task[]- List of tasksisLoading: boolean- Loading statecreateTask: (data: TaskData) => Promise<void>updateTask: (id: number, data: TaskData) => Promise<void>deleteTask: (id: number) => Promise<void>completeTask: (id: number) => Promise<void>
Navigation & Routing Hooks
useRouteAndQueryParams
Extract route parameters and query string parameters.
import { useRouteAndQueryParams } from '@april9au/stack9-react';
function DetailPage() {
const { routeParams, queryParams } = useRouteAndQueryParams();
// Route: /customers/:id?tab=orders&filter=active
// routeParams = { id: '123' }
// queryParams = { tab: 'orders', filter: 'active' }
return (
<CustomerDetail
customerId={routeParams.id}
activeTab={queryParams.tab}
filter={queryParams.filter}
/>
);
}
Returns:
routeParams: Record<string, string>- Route parametersqueryParams: Record<string, string>- Query string parameters
usePaginationByRoute
Manage pagination state synchronized with URL.
import { usePaginationByRoute } from '@april9au/stack9-react';
function PaginatedList() {
const {
currentPage,
pageSize,
setCurrentPage,
setPageSize,
offset
} = usePaginationByRoute(20); // default page size
const { data } = useQuery({
offset,
limit: pageSize
});
return (
<>
<List items={data.items} />
<Pagination
current={currentPage}
pageSize={pageSize}
total={data.total}
onChange={setCurrentPage}
onShowSizeChange={(_, size) => setPageSize(size)}
/>
</>
);
}
Parameters:
defaultLimit: number- Default page size (default: 10)
Returns:
currentPage: number- Current page number (1-based)pageSize: number- Items per pagesetCurrentPage: (page: number) => voidsetPageSize: (size: number) => voidoffset: number- Calculated offset for queries
useLocationHash
Manage URL hash for tab navigation and anchor links.
import { useLocationHash } from '@april9au/stack9-react';
function TabbedView() {
const { hash, setHash } = useLocationHash();
return (
<Tabs
activeKey={hash || 'details'}
onChange={setHash}
>
<TabPane key="details" tab="Details">
<DetailsPanel />
</TabPane>
<TabPane key="history" tab="History">
<HistoryPanel />
</TabPane>
</Tabs>
);
}
Returns:
hash: string- Current URL hash (without #)setHash: (hash: string) => void- Update URL hash