Skip to main content

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 instance
  • config: AppConfig - Stack9 application configuration
  • children: 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 user
  • isAuthenticated: boolean - Authentication status
  • login: (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 instance
  • entityService: ApiEntityService - Entity CRUD operations
  • queryService: ApiQueryService - Query execution service
  • screenService: ApiScreenService - Screen configuration service
  • screenFolderService: ApiScreenFolderService - Screen folder management
  • userService: ApiUserService - User management service
  • appService: ApiAppService - Application service
  • appPermissionService: ApiAppPermissionService - Permission management
  • appAutomationCronJobService: ApiAppAutomationCronJobService - Automation service
  • taskService: ApiTaskService - Task management service
  • config: 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 configurations
  • error: Error - Error object if fetch failed
  • isLoading: boolean - Loading state
  • mutate: 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 configuration
  • screenName: string - Screen identifier
  • error: Error - Error object if fetch failed
  • isLoading: 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 definition
  • isLoading: boolean - Loading state
  • error: 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 search
  • searchField: string - Field to search in
  • valueField: string - Field to use as option value
  • labelField: string - Field to use as option label
  • filters?: object - Additional filter criteria
  • debounceMs?: number - Debounce delay (default: 300ms)

Returns:

  • options: LabelValue[] - Array of dropdown options
  • isLoading: boolean - Loading state
  • searchTerm: string - Current search term
  • setSearchTerm: Function - Update search term
  • selectedValue: any - Currently selected value
  • setSelectedValue: 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 identifier
  • entityID: number - Entity instance ID

Returns:

  • attachments: Attachment[] - List of attachments
  • isLoading: boolean - Loading state
  • uploadAttachment: (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 identifier
  • entityID: number - Entity instance ID

Returns:

  • comments: Comment[] - List of comments
  • isLoading: boolean - Loading state
  • addComment: (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 identifier
  • entityID: number - Entity instance ID

Returns:

  • logs: EntityLog[] - Array of audit log entries
  • isLoading: boolean - Loading state
  • error: 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 identifier
  • entityID: number - Entity instance ID

Returns:

  • tasks: Task[] - List of tasks
  • isLoading: boolean - Loading state
  • createTask: (data: TaskData) => Promise<void>
  • updateTask: (id: number, data: TaskData) => Promise<void>
  • deleteTask: (id: number) => Promise<void>
  • completeTask: (id: number) => Promise<void>

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 parameters
  • queryParams: 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 page
  • setCurrentPage: (page: number) => void
  • setPageSize: (size: number) => void
  • offset: 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 devices
  • sm: 576 - Small devices
  • md: 768 - Medium devices (tablets)
  • lg: 992 - Large devices (desktops)
  • xl: 1200 - Extra large devices
  • xxl: 1600 - Extra extra large devices

Returns:

  • width: number - Current viewport width
  • height: number - Current viewport height
  • isMobile: boolean - Width < 768px
  • isTablet: boolean - 768px <= width < 992px
  • isDesktop: 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 state
  • dismiss: () => 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 array
  • valueField: string - Field to use as value
  • labelField: 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 users
  • userGroups: UserGroup[] - List of user groups
  • combined: LabelValue[] - Combined options for dropdowns
  • isLoading: 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 identifier
  • params?: object - Query parameters

Returns:

  • data: any - Query results
  • isLoading: boolean - Loading state
  • error: Error - Error object if query failed
  • refetch: () => 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 adapters
  • onSelect: (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​

  1. Update imports:
// Old
import { Stack9Provider } from '@april9au/stack9-react';

// New
import { AppProvider } from '@april9au/stack9-react';
  1. Update provider props:
// Old
<Stack9Provider apiUrl={url}>

// New
<AppProvider axiosFactory={axiosProvider} config={config}>
  1. 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: