Skip to content

Getting Started with PRS Frontend Development

This guide will help you set up your development environment and understand the basics of working with the PRS Frontend codebase based on the actual implementation.

Prerequisites

Before you begin, make sure you have the following installed:

  • Node.js (v16 or later)
  • npm (v8 or later)
  • Git
  • A modern code editor (VS Code recommended)
  • ESLint and Prettier extensions for your editor

Setting Up the Development Environment

1. Clone the Repository

Bash
git clone <repository-url>
cd prs-frontend

2. Install Dependencies

Using npm:

Bash
npm install

Or using yarn:

Bash
yarn install

3. Set Up Environment Variables

Copy the example environment file and update it with your local configuration:

Bash
cp .env.example .env.local

Edit the .env.local file with your API endpoint and other configuration options:

Text Only
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_ENV=development

4. Start the Development Server

Using npm:

Bash
npm run dev

Or using yarn:

Bash
yarn dev

The development server will start on http://localhost:5173 by default.

Project Structure

The PRS Frontend follows a feature-based architecture. Here's the actual structure of the codebase:

Text Only
src/
├── app/                  # Application setup
│   ├── provider.jsx      # App providers
│   ├── router.jsx        # Routing configuration
│   └── routes/           # Route components
│       ├── app/          # Main application routes
│       │   ├── dashboard/
│       │   ├── user/
│       │   ├── supplier/
│       │   ├── canvass/
│       │   ├── purchase-order/
│       │   └── ...
│       ├── auth/         # Authentication routes
│       └── ...
├── assets/               # Static assets
│   ├── icons/            # SVG icons
│   ├── images/           # Image assets
│   └── markdown/         # Markdown content
├── components/           # Shared components
│   ├── errors/           # Error components
│   ├── layouts/          # Layout components
│   └── ui/               # UI components
│       ├── Accordion/
│       ├── Button/
│       ├── Dropdown/
│       ├── Form/
│       ├── Modal/
│       ├── SearchBar/
│       ├── Table/
│       ├── Tabs/
│       └── ...
├── config/               # Configuration files
├── features/             # Feature modules
│   ├── dashboard/        # Dashboard feature
│   │   ├── api/          # API integration
│   │   └── components/   # Feature components
│   ├── user/
│   ├── supplier/
│   ├── company/
│   ├── department/
│   ├── project/
│   ├── canvass/
│   ├── purchase-order/
│   ├── delivery-receipt/
│   ├── invoice/
│   ├── payment-request/
│   ├── ofm/
│   ├── non-ofm/
│   ├── notification/
│   ├── attachments/
│   └── auth/
├── hoc/                  # Higher-order components
├── hooks/                # Custom hooks
├── lib/                  # Core libraries
│   ├── apiClient.js      # API client
│   ├── auth.jsx          # Authentication
│   └── reactQuery.js     # React Query config
├── schema/               # Validation schemas
├── store/                # Zustand stores
│   ├── authStore.js
│   ├── userStore.js
│   ├── permissionStore.js
│   ├── requisitionItemsStore.js
│   ├── canvassItemsStore.js
│   ├── rsTabStore.js
│   └── ...
└── utils/                # Utility functions
    ├── cn.js             # Class name utility
    ├── permissions.js    # Permission utilities
    ├── query.js          # Query parameter utilities
    ├── stringCleanup.js  # String manipulation utilities
    └── ...

Key Concepts

1. Feature-Based Organization

The codebase is organized by features rather than technical concerns. Each feature folder contains everything related to that feature, including components, API integration, and utilities.

Here's an actual example from the codebase:

JavaScript
// src/features/dashboard/api/get-requisitions.js
import { useQuery } from '@tanstack/react-query';
import { api } from '@lib/apiClient';
import { formatSortParams } from '@utils/query';
import {
  convertObjectToString,
  removeEmptyStringFromObject,
} from '@utils/stringCleanup';

export const getRequisitions = ({ page = 1, limit = 10, sortBy, filterBy, requestType }) => {
  return api.get('/v2/requisitions', {
    params: {
      page,
      limit,
      filterBy: convertObjectToString(removeEmptyStringFromObject(filterBy)),
      ...formatSortParams(sortBy),
      requestType,
    },
  });
};

export const useGetRequisitions = (
  { page, limit, sortBy, filterBy, requestType },
  config = {},
) => {
  return useQuery({
    queryKey: ['requisitions', { page, limit, sortBy, filterBy, requestType }],
    queryFn: () => getRequisitions({ page, limit, sortBy, filterBy, requestType }),
    keepPreviousData: true,
    ...config,
  });
};

2. Component Composition

UI components are built using composition, with smaller components combined to create more complex interfaces.

Here's an actual example from the codebase:

JSX
// From Dashboard.jsx
<div className="flex flex-col gap-4">
  <SearchBar
    searchItems={searchItems}
    onSearch={val => setSearchQuery(val)}
    subClassName="lg:grid-cols-6"
    setPage={setPage}
  />

  <Tabs tabs={tabs} />
  <div
    className={cn('block sm:flex items-center justify-end', {
      'justify-between': createPermission,
    })}
  >
    {createPermission && (
      <Button
        onClick={() => {
          clearRequisitionItemsStore();
          navigate('create');
        }}
        variant="submit"
        className="w-auto px-8"
        icon={NewDocument}
      >
        New Request
      </Button>
    )}

    <ActionButtons />
  </div>
  <Pagination
    total={requisitions?.total}
    setPage={setPage}
    setLimit={setLimit}
    hasLimit={true}
  >
    <Table
      headers={headers}
      data={requisitions?.data}
      tdDesign={tdDesign}
      isLoading={isLoading}
      onSort={setSort}
      currentSort={currentSort}
      infoDescription="Create a new request to add content"
    />
  </Pagination>
</div>

The Table component is a good example of composition, with custom cell rendering:

JSX
// Table cell rendering configuration from Dashboard.jsx
const tdDesign = {
  rsNumber: {
    render: ({ combine_rs_number, id, status }) =>
      status === 'draft' ? (
        <Link
          to={`/app/dashboard/create/${id}`}
          className="text-blue-500 underline underline-offset-4"
        >
          {combine_rs_number}
        </Link>
      ) : (
        <Link
          to={`/app/requisition-slip/${id}`}
          className="text-blue-500 underline underline-offset-4"
        >
          {combine_rs_number}
        </Link>
      ),
  },
  status: {
    css: 'min-w-48',
    render: Status, // Status component for rendering status pills
  },
  // Other cell renderers...
};

3. Data Fetching with React Query

React Query (Tanstack Query) is used for data fetching, caching, and state management. The application uses a consistent pattern for API integration.

Here's an actual example from the codebase:

JSX
// From Dashboard.jsx - Using React Query hooks
const { data: requisitions, isLoading } = useGetRequisitions(
  {
    page: currentPage,
    limit: currentLimit,
    sortBy,
    filterBy: searchQuery,
    requestType: requestType,
  },
  { enabled: getPermission },
);

const { data: users } = useGetUsers({
  page: 1,
  paginate: false,
  includeCurrentUser: true,
  sortBy: { firstName: 'ASC' },
});

const { data: companies } = useGetCompanies({
  page: 1,
  paginate: false,
  sortBy: { name: 'ASC' },
});

// Using the data in the component
return (
  <div className="flex flex-col gap-4">
    {/* ... */}
    <Pagination
      total={requisitions?.total}
      setPage={setPage}
      setLimit={setLimit}
      hasLimit={true}
    >
      <Table
        headers={headers}
        data={requisitions?.data}
        tdDesign={tdDesign}
        isLoading={isLoading}
        onSort={setSort}
        currentSort={currentSort}
        infoDescription="Create a new request to add content"
      />
    </Pagination>
  </div>
);

The React Query configuration is centralized in src/lib/reactQuery.js to ensure consistent behavior across the application.

4. State Management with Zustand

Zustand is used for global state management with persistence. The application uses multiple stores for different concerns.

Here's an actual example from the codebase:

JavaScript
// From authStore.js
const useTokenStore = create(
  persist(
    (set, get) => ({
      token: null,
      type: null,
      refreshToken: null,
      expiredAt: null,
      setToken: (token, type = null) => set({ token, type }),
      setRefreshToken: ({ accessToken, refreshToken, expiredAt }) => {
        const currentAccessToken = get().token;

        set({
          token: accessToken || currentAccessToken,
          refreshToken,
          expiredAt,
        });
      },
      setType: type => set({ type }),
      removeToken: () =>
        set({ token: null, type: null, refreshToken: null, expiredAt: null }),
    }),
    {
      name: 'auth-storage', // name of the item in the storage (must be unique)
    },
  ),
);

// From requisitionItemsStore.js - Store for managing requisition items
const useRequisitionItemsStore = create(set => ({
  items: [],
  setItems: items => set({ items }),
  addItem: item => set(state => ({ items: [...state.items, item] })),
  updateItem: (index, item) =>
    set(state => ({
      items: state.items.map((i, idx) => (idx === index ? item : i)),
    })),
  removeItem: index =>
    set(state => ({
      items: state.items.filter((_, idx) => idx !== index),
    })),
  clearItems: () => set({ items: [] }),
}));

5. Form Handling with React Hook Form

Forms are built using React Hook Form with validation. The application uses custom form components that integrate with React Hook Form.

Here's an example of form handling from the codebase:

JSX
// From a form component in the codebase
const {
  register,
  handleSubmit,
  formState: { errors },
  control,
  setValue,
  watch,
} = useForm({
  defaultValues: {
    requestType: 'GOODS',
    requestDate: new Date(),
    requestedBy: user?.id,
    companyId: user?.company?.id,
    departmentId: user?.department?.id,
    projectId: '',
    purpose: '',
    remarks: '',
  },
});

// Form submission handler
const onSubmit = data => {
  const payload = {
    ...data,
    requestDate: format(new Date(data.requestDate), 'yyyy-MM-dd'),
    items: requisitionItems,
  };

  createRequisition(payload, {
    onSuccess: data => {
      toast.success('Requisition created successfully');
      navigate(`/app/requisition-slip/${data.id}`);
    },
  });
};

// Form rendering
return (
  <form onSubmit={handleSubmit(onSubmit)}>
    <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
      <FormInput
        label="Request Type"
        name="requestType"
        register={register}
        errors={errors}
        type="select"
        options={[
          { value: 'GOODS', label: 'Goods' },
          { value: 'SERVICES', label: 'Services' },
        ]}
        required
      />

      <FormInput
        label="Request Date"
        name="requestDate"
        register={register}
        errors={errors}
        type="date"
        required
      />

      <FormInput
        label="Requested By"
        name="requestedBy"
        register={register}
        errors={errors}
        type="select"
        options={userOptions}
        required
      />

      {/* More form fields... */}
    </div>

    <div className="mt-4 flex justify-end gap-2">
      <Button
        type="button"
        variant="secondary"
        onClick={() => navigate('/app/dashboard')}
      >
        Cancel
      </Button>
      <Button type="submit" variant="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </Button>
    </div>
  </form>
);

Common Development Tasks

1. Creating a New Feature

Based on the actual codebase patterns, here's how to create a new feature:

  1. Create a new folder in src/features
  2. Add API integration files in the api subfolder
  3. Add components in the components subfolder
  4. Create a route component in src/app/routes/app/
  5. Add the feature to the router with permission checks

Example based on the actual codebase patterns:

JSX
// 1. Create API integration
// src/features/supplier/api/get-suppliers.js
import { useQuery } from '@tanstack/react-query';
import { api } from '@lib/apiClient';
import { formatSortParams } from '@utils/query';
import {
  convertObjectToString,
  removeEmptyStringFromObject,
} from '@utils/stringCleanup';

export const getSuppliers = ({ page = 1, limit = 10, sortBy, filterBy, paginate = true }) => {
  return api.get('/v2/suppliers', {
    params: {
      page,
      limit,
      paginate,
      filterBy: convertObjectToString(removeEmptyStringFromObject(filterBy)),
      ...formatSortParams(sortBy),
    },
  });
};

export const useGetSuppliers = (
  { page, limit, sortBy, filterBy, paginate },
  config = {},
) => {
  return useQuery({
    queryKey: ['suppliers', { page, limit, sortBy, filterBy, paginate }],
    queryFn: () => getSuppliers({ page, limit, sortBy, filterBy, paginate }),
    keepPreviousData: true,
    ...config,
  });
};

// 2. Create components
// src/features/supplier/components/SupplierList.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useGetSuppliers } from '../api/get-suppliers';
import { Table, SearchBar, Pagination, Button } from '@components/ui';
import { usePermission } from '@hooks/usePermission';
import { supplierCreate } from '@utils/permissions';

export const SupplierList = () => {
  const navigate = useNavigate();
  const [currentPage, setPage] = useState(1);
  const [currentLimit, setLimit] = useState(10);
  const [sortBy, setSort] = useState({ name: 'ASC' });
  const [searchQuery, setSearchQuery] = useState({});

  const createPermission = usePermission(supplierCreate);

  const { data: suppliers, isLoading } = useGetSuppliers({
    page: currentPage,
    limit: currentLimit,
    sortBy,
    filterBy: searchQuery,
  });

  const headers = [
    { key: 'name', label: 'Name' },
    { key: 'contactPerson', label: 'Contact Person' },
    { key: 'contactNumber', label: 'Contact Number' },
    { key: 'email', label: 'Email' },
    { key: 'status', label: 'Status' },
  ];

  const tdDesign = {
    name: {
      render: ({ name, id }) => (
        <a
          href={`/app/supplier/${id}`}
          className="text-blue-500 underline underline-offset-4"
        >
          {name}
        </a>
      ),
    },
    status: {
      render: ({ status }) => (
        <span className={`badge ${status === 'ACTIVE' ? 'bg-green-500' : 'bg-red-500'}`}>
          {status}
        </span>
      ),
    },
  };

  const searchItems = [
    { key: 'name', label: 'Name', type: 'text' },
    { key: 'contactPerson', label: 'Contact Person', type: 'text' },
    { key: 'email', label: 'Email', type: 'text' },
    { key: 'status', label: 'Status', type: 'select', options: [
      { value: 'ACTIVE', label: 'Active' },
      { value: 'INACTIVE', label: 'Inactive' },
    ]},
  ];

  return (
    <div className="flex flex-col gap-4">
      <SearchBar
        searchItems={searchItems}
        onSearch={val => setSearchQuery(val)}
        setPage={setPage}
      />

      <div className="flex justify-between">
        <h1 className="text-2xl font-bold">Suppliers</h1>

        {createPermission && (
          <Button
            onClick={() => navigate('create')}
            variant="submit"
          >
            Add Supplier
          </Button>
        )}
      </div>

      <Pagination
        total={suppliers?.total}
        setPage={setPage}
        setLimit={setLimit}
        hasLimit={true}
      >
        <Table
          headers={headers}
          data={suppliers?.data}
          tdDesign={tdDesign}
          isLoading={isLoading}
          onSort={setSort}
          currentSort={sortBy}
        />
      </Pagination>
    </div>
  );
};

// 3. Create route component
// src/app/routes/app/supplier.jsx
import { SupplierList } from '@features/supplier/components/SupplierList';

export const SupplierRoute = () => {
  return <SupplierList />;
};

// 4. Add to router with permission check
// src/app/router.jsx
hasPermission(
  {
    path: 'supplier',
    children: [
      {
        path: '',
        lazy: async () => {
          const { SupplierRoute } = await import('./routes/app/supplier');
          return { Component: SupplierRoute };
        },
      },
      {
        path: 'create',
        lazy: async () => {
          const { SupplierCreateRoute } = await import('./routes/app/supplier');
          return { Component: SupplierCreateRoute };
        },
      },
      {
        path: ':id',
        lazy: async () => {
          const { SupplierDetailRoute } = await import('./routes/app/supplier');
          return { Component: SupplierDetailRoute };
        },
      },
    ],
  },
  supplierView, // Permission check
);

2. Adding a New API Integration

Based on the actual codebase patterns, here's how to add a new API integration:

  1. Create a new file in the feature's api folder
  2. Define the API function for the HTTP request
  3. Create a React Query hook (useQuery for GET, useMutation for POST/PUT/DELETE)
  4. Include proper error handling and cache invalidation

Example based on the actual codebase patterns:

JavaScript
// src/features/requisition/api/approve-requisition.js
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@lib/apiClient';
import { toast } from 'react-toastify';

export const approveRequisition = ({ id, remarks }) => {
  return api.post(`/v2/requisitions/${id}/approve`, { remarks });
};

export const useApproveRequisition = (config = {}) => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: approveRequisition,
    onSuccess: (data, variables) => {
      // Invalidate relevant queries to refresh data
      queryClient.invalidateQueries(['requisitions']);
      queryClient.invalidateQueries(['requisition', variables.id]);

      // Show success message
      toast.success('Requisition approved successfully');
    },
    onError: (error) => {
      // Show error message
      toast.error(error.message || 'Failed to approve requisition');
    },
    ...config,
  });
};

// Usage example
const { mutate: approveRequisition, isLoading: isApproving } = useApproveRequisition();

const handleApprove = () => {
  approveRequisition(
    { id: requisitionId, remarks: 'Approved' },
    {
      onSuccess: () => {
        // Additional success handling if needed
        navigate('/app/dashboard');
      },
    }
  );
};

3. Creating a New UI Component

Based on the actual codebase patterns, here's how to create a new UI component:

  1. Create a new folder in src/components/ui for the component
  2. Create an index.jsx file for the main component
  3. Use Tailwind CSS for styling with the cn utility for class merging
  4. Use composition pattern for complex components
  5. Include proper prop validation

Example based on the actual codebase patterns:

JSX
// src/components/ui/Button/index.jsx
import { forwardRef } from 'react';
import PropTypes from 'prop-types';
import { cva } from 'class-variance-authority';
import { cn } from '@utils/cn';

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
  {
    variants: {
      variant: {
        default: 'bg-gray-900 text-white hover:bg-gray-700 focus:ring-gray-900',
        submit: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-600',
        destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-600',
        outline: 'bg-transparent border border-gray-200 hover:bg-gray-100 focus:ring-gray-900',
        secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-900',
        ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-900',
        link: 'bg-transparent underline-offset-4 hover:underline text-blue-600 hover:bg-transparent focus:ring-0',
      },
      size: {
        default: 'h-10 py-2 px-4',
        sm: 'h-8 px-2 rounded-md',
        lg: 'h-12 px-8 rounded-md',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
);

const Button = forwardRef(
  ({ className, variant, size, icon: Icon, children, ...props }, ref) => {
    return (
      <button
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      >
        {Icon && <Icon className="mr-2 h-4 w-4" />}
        {children}
      </button>
    );
  },
);

Button.displayName = 'Button';

Button.propTypes = {
  className: PropTypes.string,
  variant: PropTypes.oneOf([
    'default',
    'submit',
    'destructive',
    'outline',
    'secondary',
    'ghost',
    'link',
  ]),
  size: PropTypes.oneOf(['default', 'sm', 'lg', 'icon']),
  icon: PropTypes.elementType,
  children: PropTypes.node,
};

export { Button, buttonVariants };

// Usage example
import { Button } from '@components/ui/Button';
import { PlusIcon } from '@assets/icons';

<Button variant="submit" size="lg" icon={PlusIcon}>
  Create New
</Button>

For composite components like Table, the pattern is to create subcomponents:

JSX
// src/components/ui/Table/index.jsx
import { forwardRef } from 'react';
import PropTypes from 'prop-types';
import { cn } from '@utils/cn';
import { Spinner } from '@components/ui/Spinner';
import { EmptyState } from '@components/ui/EmptyState';

const Table = forwardRef(
  (
    {
      className,
      headers,
      data,
      tdDesign,
      isLoading,
      onSort,
      currentSort,
      infoDescription,
      ...props
    },
    ref,
  ) => {
    // Sort handling logic
    const handleSort = key => {
      if (!onSort) return;

      const newSort = { ...currentSort };

      if (newSort[key]) {
        if (newSort[key] === 'ASC') {
          newSort[key] = 'DESC';
        } else {
          delete newSort[key];
        }
      } else {
        Object.keys(newSort).forEach(k => delete newSort[k]);
        newSort[key] = 'ASC';
      }

      onSort(newSort);
    };

    // Render loading state
    if (isLoading) {
      return (
        <div className="flex h-60 items-center justify-center">
          <Spinner size="lg" />
        </div>
      );
    }

    // Render empty state
    if (!data || data.length === 0) {
      return (
        <EmptyState
          title="No data available"
          description={infoDescription || 'No data to display'}
        />
      );
    }

    return (
      <div className="w-full overflow-auto">
        <table
          className={cn('w-full border-collapse text-sm', className)}
          ref={ref}
          {...props}
        >
          <thead>
            <tr className="border-b bg-gray-50">
              {headers.map(header => (
                <th
                  key={header.key}
                  className={cn(
                    'px-4 py-3 text-left font-medium text-gray-500',
                    header.sortable !== false && 'cursor-pointer',
                  )}
                  onClick={() => header.sortable !== false && handleSort(header.key)}
                >
                  <div className="flex items-center">
                    {header.label}
                    {currentSort && currentSort[header.key] && (
                      <span className="ml-1">
                        {currentSort[header.key] === 'ASC' ? '↑' : '↓'}
                      </span>
                    )}
                  </div>
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {data.map((row, rowIndex) => (
              <tr
                key={row.id || rowIndex}
                className="border-b hover:bg-gray-50"
              >
                {headers.map(header => {
                  const design = tdDesign?.[header.key];

                  return (
                    <td
                      key={`${row.id || rowIndex}-${header.key}`}
                      className={cn('px-4 py-3', design?.css)}
                    >
                      {design?.render
                        ? design.render(row)
                        : row[header.key] || '-'}
                    </td>
                  );
                })}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  },
);

Table.displayName = 'Table';

Table.propTypes = {
  className: PropTypes.string,
  headers: PropTypes.arrayOf(
    PropTypes.shape({
      key: PropTypes.string.isRequired,
      label: PropTypes.string.isRequired,
      sortable: PropTypes.bool,
    }),
  ).isRequired,
  data: PropTypes.array,
  tdDesign: PropTypes.object,
  isLoading: PropTypes.bool,
  onSort: PropTypes.func,
  currentSort: PropTypes.object,
  infoDescription: PropTypes.string,
};

export { Table };

Debugging

Based on the actual codebase patterns, here are the debugging techniques used:

1. React Developer Tools

Use the React Developer Tools browser extension to inspect component props, state, and the component tree. This is especially useful for debugging complex component hierarchies and state management issues.

2. React Query DevTools

The application includes React Query DevTools in development mode to inspect queries, mutations, and cache state:

JSX
1
2
3
4
5
// From provider.jsx
<QueryClientProvider client={queryClient}>
  {import.meta.env.DEV && <ReactQueryDevtools />}
  {/* Application components */}
</QueryClientProvider>

3. Network Tab

Use the browser's Network tab to inspect API requests and responses. The application uses Axios interceptors that log detailed information about requests and responses.

4. Toast Notifications

The application uses React Toastify for displaying error messages and success notifications:

JSX
1
2
3
4
5
// Error handling example
toast.error(error.message || 'An error occurred');

// Success notification example
toast.success('Operation completed successfully');

5. Console Logging

Use console.log for debugging, but remember to remove them before committing.

JSX
1
2
3
4
5
6
7
useEffect(() => {
  console.log('Component mounted with data:', data);

  return () => {
    console.log('Component unmounted');
  };
}, [data]);

6. Error Boundaries

The application uses Error Boundaries to catch and display errors in a user-friendly way:

JSX
1
2
3
4
// From provider.jsx
<ErrorBoundary FallbackComponent={MainErrorFallback}>
  {/* Application components */}
</ErrorBoundary>

Additional Resources