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
Utility Hooks
useViewport
Responsive design utilities with breakpoint detection.
import { useViewport, breakpoints } from '@april9au/stack9-react';
function ResponsiveLayout() {
const { width, isMobile, isTablet, isDesktop } = useViewport();
if (isMobile) {
return <MobileLayout />;
}
return (
<DesktopLayout
sidebarCollapsed={width < breakpoints.lg}
/>
);
}
Breakpoints:
xs: 480- Extra small devicessm: 576- Small devicesmd: 768- Medium devices (tablets)lg: 992- Large devices (desktops)xl: 1200- Extra large devicesxxl: 1600- Extra extra large devices
Returns:
width: number- Current viewport widthheight: number- Current viewport heightisMobile: boolean- Width< 768pxisTablet: boolean-768px <= width < 992pxisDesktop: boolean- Width>= 992px
useDismissibleControl
Manage dismissible UI elements with persistence.
import { useDismissibleControl } from '@april9au/stack9-react';
function DismissibleBanner() {
const { isDismissed, dismiss } = useDismissibleControl(
() => localStorage.setItem('banner-dismissed', 'true')
);
if (isDismissed) return null;
return (
<Banner>
<p>Welcome to our new features!</p>
<CloseButton onClick={dismiss} />
</Banner>
);
}
Parameters:
onDismiss: () => void- Callback when dismissed
Returns:
isDismissed: boolean- Dismissal statedismiss: () => void- Dismiss function
useMapSourceLabelValue
Transform data sources into label-value pairs for dropdowns.
import { useMapSourceLabelValue } from '@april9au/stack9-react';
function StatusDropdown({ data }) {
const options = useMapSourceLabelValue(
data,
'id', // value field
'title' // label field
);
return (
<Select options={options} />
);
}
Parameters:
source: any[]- Source data arrayvalueField: string- Field to use as valuelabelField: string- Field to use as label
Returns:
LabelValue[]- Array of{ label: string, value: any }objects
useUsersAndUserGroups
Fetch users and user groups for assignment dropdowns.
import { useUsersAndUserGroups } from '@april9au/stack9-react';
function AssigneeSelector() {
const {
users,
userGroups,
combined,
isLoading
} = useUsersAndUserGroups();
return (
<Select
loading={isLoading}
options={combined}
placeholder="Assign to user or group"
/>
);
}
Returns:
users: User[]- List of usersuserGroups: UserGroup[]- List of user groupscombined: LabelValue[]- Combined options for dropdownsisLoading: boolean- Loading state
Screen Query Hooks
useScreenQueryDefinition
Execute screen queries with dynamic parameters.
import { useScreenQueryDefinition } from '@april9au/stack9-react';
function CustomReport({ screenName, filters }) {
const {
data,
isLoading,
error,
refetch
} = useScreenQueryDefinition(screenName, filters);
return (
<div>
<FilterBar onChange={refetch} />
{isLoading && <Spinner />}
{error && <Alert type="error">{error.message}</Alert>}
{data && <ReportTable data={data} />}
</div>
);
}
Parameters:
screenName: string- Screen identifierparams?: object- Query parameters
Returns:
data: any- Query resultsisLoading: boolean- Loading stateerror: Error- Error object if query failedrefetch: () => void- Refetch function
useScreensRoute
Manage screen routing configuration.
import { useScreensRoute } from '@april9au/stack9-react';
function AppRouter() {
const routes = useScreensRoute([
{ path: '/', component: Home },
{ path: '/dashboard', component: Dashboard }
]);
return (
<Routes>
{routes.map(route => (
<Route
key={route.path}
path={route.path}
element={route.component}
/>
))}
</Routes>
);
}
Parameters:
defaultValue: S9ScreenRoute[]- Default routes
Returns:
S9ScreenRoute[]- Processed screen routes
Models & Types
AppConfig
Application configuration interface.
interface AppConfig {
apiUrl: string;
authUrl?: string;
appId: string;
environment: 'development' | 'staging' | 'production';
features?: {
[key: string]: boolean;
};
customSettings?: Record<string, any>;
}
LabelValue
Standard label-value pair for dropdowns and selects.
interface LabelValue {
label: string;
value: any;
disabled?: boolean;
children?: LabelValue[];
}
Coordinates
Geographic coordinate interface.
interface Coordinates {
latitude: number;
longitude: number;
accuracy?: number;
altitude?: number;
}
Globalization
Internationalization settings.
interface Globalization {
locale: string;
timezone: string;
dateFormat: string;
timeFormat: string;
currency: string;
numberFormat: string;
}
CustomUIComponentsProps
Props for custom UI components in screens.
interface CustomUIComponentsProps {
entityId?: number;
entityData?: any;
schema?: EntitySchema;
mode?: 'view' | 'edit' | 'create';
onSave?: (data: any) => Promise<void>;
onCancel?: () => void;
customProps?: Record<string, any>;
}
Components
AuthAdapterSelector
Component for selecting authentication adapter/method.
import { AuthAdapterSelector } from '@april9au/stack9-react';
function LoginPage() {
return (
<AuthAdapterSelector
adapters={['oauth', 'saml', 'local']}
onSelect={(adapter) => handleLogin(adapter)}
/>
);
}
Props:
adapters: string[]- Available authentication adaptersonSelect: (adapter: string) => void- Selection callback
Utilities
The package exports a Utils namespace with various utility functions:
import { Utils } from '@april9au/stack9-react';
// Date utilities
const formatted = Utils.formatDate(new Date());
const parsed = Utils.parseDate('2024-01-01');
// String utilities
const slugified = Utils.slugify('Hello World'); // 'hello-world'
const truncated = Utils.truncate('Long text...', 10);
// Object utilities
const cleaned = Utils.cleanObject({ a: null, b: 1 }); // { b: 1 }
const flattened = Utils.flatten({ a: { b: 1 } }); // { 'a.b': 1 }
// Array utilities
const unique = Utils.unique([1, 2, 2, 3]); // [1, 2, 3]
const grouped = Utils.groupBy(items, 'category');
Best Practices
1. Provider Hierarchy
Always maintain the correct provider hierarchy:
<AppProvider>
<AuthProvider>
<UIProvider>
<YourApp />
</UIProvider>
</AuthProvider>
</AppProvider>
2. Error Handling
Implement proper error handling in all data fetching:
function DataComponent() {
const { data, error, isLoading } = useScreens();
if (error) {
// Log to error tracking service
console.error('Failed to fetch screens:', error);
return <ErrorBoundary error={error} />;
}
if (isLoading) {
return <LoadingSkeleton />;
}
return <DataView data={data} />;
}
3. Performance Optimization
Use React.memo and useMemo for expensive computations:
const MemoizedComponent = React.memo(({ data }) => {
const processedData = useMemo(
() => expensiveTransformation(data),
[data]
);
return <View data={processedData} />;
});
4. Custom Hooks
Build custom hooks on top of Stack9 hooks for domain logic:
function useCustomerDashboard(customerId) {
const { data: customer } = useEntitySchema('customer', customerId);
const { tasks } = useEntityTasks('customer', customerId);
const { logs } = useEntityLogs('customer', customerId);
return {
customer,
recentActivity: logs?.slice(0, 5),
openTasks: tasks?.filter(t => !t.completed),
stats: calculateCustomerStats(customer)
};
}
Migration Guide
From v1.x to v2.x
- Update imports:
// Old
import { Stack9Provider } from '@april9au/stack9-react';
// New
import { AppProvider } from '@april9au/stack9-react';
- Update provider props:
// Old
<Stack9Provider apiUrl={url}>
// New
<AppProvider axiosFactory={axiosProvider} config={config}>
- Update hook usage:
// Old
const { api } = useStack9();
// New
const { entityService, queryService } = useStack9();
Troubleshooting
Common Issues
Issue: Hooks returning undefined
- Solution: Ensure component is wrapped in AppProvider
Issue: Authentication failures
- Solution: Check AuthProvider clientId and token refresh
Issue: Stale data after mutations
- Solution: Call
mutate()function after updates
Issue: Memory leaks in development
- Solution: This is often React StrictMode; test in production build
Support
For issues, feature requests, or questions:
- GitHub Issues: stack9-monorepo/issues
- Documentation: stack9.docs
- Community: Stack9 Discord