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: