Skip to main content

Master Web

Frontend component library for master data and settings management in MBC CQRS Serverless applications.

Installation

npm install @mbc-cqrs-serverless/master-web
Start Here

This is the recommended way to integrate master-web with Next.js App Router. Following this pattern will help you avoid common issues like httpClient.get is not a function errors.

When using this library with Next.js App Router (v14+/v15), use the Layout-based Provider Pattern. Set up AppProviders in a layout.tsx file, and use dynamic imports for components in page.tsx files.

Step 1: Create layout.tsx

Create a layout file that sets up the providers. This ensures the context is properly initialized before child components mount.

// app/admin/[tenant]/master/layout.tsx
'use client'

import { useMemo } from 'react'
import dynamic from 'next/dynamic'
import { useParams } from 'next/navigation'
import axios from 'axios'
import { fetchAuthSession } from 'aws-amplify/auth'
import type { IUrlProvider } from '@mbc-cqrs-serverless/master-web/UrlProvider'

// Dynamic import of AppProviders (SSR disabled)
const AppProviders = dynamic(
() =>
import('@mbc-cqrs-serverless/master-web/AppProviders').then(
(mod) => mod.AppProviders
),
{ ssr: false }
)

// Custom URL provider for your application's routing
class MasterUrlProvider implements IUrlProvider {
protected readonly baseUrl: string
public readonly SETTINGS_PAGE_URL: string
public readonly ADD_SETTINGS_PAGE_URL: string
public readonly EDIT_SETTINGS_PAGE_URL: string
public readonly DATA_PAGE_URL: string
public readonly ADD_DATA_PAGE_URL: string
public readonly EDIT_DATA_PAGE_URL: string
public readonly FAQ_CATEGORY_PAGE_URL: string
public readonly TOP_URL: string

constructor(tenantCode: string) {
this.baseUrl = `/admin/${tenantCode}/master`
this.SETTINGS_PAGE_URL = `${this.baseUrl}/master-setting`
this.ADD_SETTINGS_PAGE_URL = `${this.baseUrl}/master-setting/new`
this.EDIT_SETTINGS_PAGE_URL = this.SETTINGS_PAGE_URL
this.DATA_PAGE_URL = `${this.baseUrl}/master-data`
this.ADD_DATA_PAGE_URL = `${this.baseUrl}/master-data/new`
this.EDIT_DATA_PAGE_URL = this.DATA_PAGE_URL
this.FAQ_CATEGORY_PAGE_URL = `${this.baseUrl}/faq-category`
this.TOP_URL = `/admin/${tenantCode}`
}

public getCopySettingPageUrl(id: string): string {
return `${this.baseUrl}/master-setting/${id}/copy/new`
}
public getDetailedCopySettingPageUrl(id: string): string {
return `${this.baseUrl}/master-setting/${id}/copy`
}
}

export default function MasterLayout({ children }: { children: React.ReactNode }) {
const params = useParams<{ tenant: string }>()
const tenantCode = params?.tenant || 'common'

const urlProvider = useMemo(() => new MasterUrlProvider(tenantCode), [tenantCode])

// {{Create httpClient with Axios interceptor for automatic auth token injection}}
const httpClient = useMemo(() => {
const baseEndpoint = process.env.NEXT_PUBLIC_API_ENDPOINT || 'http://localhost:3010'
const instance = axios.create({
baseURL: `${baseEndpoint}/api`,
headers: {
'Content-Type': 'application/json',
'x-tenant-code': tenantCode,
},
})

instance.interceptors.request.use(async (config) => {
try {
const session = await fetchAuthSession()
const token = session.tokens?.idToken?.toString()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
} catch {
// Ignore auth errors
}
return config
})

return instance
}, [tenantCode])

const user = useMemo(() => ({ tenantCode, tenantRole: 'admin' }), [tenantCode])

return (
<AppProviders user={user} urlProvider={urlProvider} httpClient={httpClient}>
<div className="p-6">{children}</div>
</AppProviders>
)
}

Step 2: Create page.tsx

After setting up providers in the layout, each page component is simple:

// app/admin/[tenant]/master/master-setting/page.tsx
'use client'

import dynamic from 'next/dynamic'
import MsLayout from '@mbc-cqrs-serverless/master-web/MsLayout'
import '@mbc-cqrs-serverless/master-web/styles.css'

const MasterSetting = dynamic(
() => import('@mbc-cqrs-serverless/master-web/MasterSetting').then((mod) => mod.default),
{ ssr: false }
)

export default function MasterSettingPage() {
return (
<main>
<MsLayout useLoading>
<MasterSetting />
</MsLayout>
</main>
)
}

Step 3: Configure Environment Variables

# .env.local
NEXT_PUBLIC_API_ENDPOINT=http://localhost:3010
NEXT_PUBLIC_MASTER_APPSYNC_URL=https://xxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
NEXT_PUBLIC_MASTER_APPSYNC_APIKEY=da2-xxxxxxxxxxxxxxxxx
NEXT_PUBLIC_MASTER_APPSYNC_REGION=ap-northeast-1

Why This Pattern?

BenefitDescription
Avoids Context IsolationReact Context in npm packages can become isolated. Layout ensures context is initialized first.
Synchronous InitializationUsing useMemo creates httpClient synchronously, avoiding race conditions.
Automatic Auth TokensAxios interceptors inject the latest auth token on every request.
Simple Page ComponentsPages only need dynamic imports and component rendering.
For More Details

See Next.js App Router Integration for alternative patterns, troubleshooting, and detailed explanations.

Overview

The Master Web package (@mbc-cqrs-serverless/master-web) provides a complete set of React components for managing master data and settings. It integrates seamlessly with the backend Master Service and includes pre-built pages, forms, and data tables.

Features

  • Master Settings Management: View, create, edit, and delete master settings
  • Master Data Management: CRUD operations for master data records
  • Rich Text Editor: Built-in rich text editor for content fields
  • JSON Editor: JSON editor for structured data fields
  • Data Tables: Sortable, paginated tables with TanStack Table
  • Real-time Updates: AWS AppSync integration for real-time data sync
  • Copy Functionality: Clone master settings and data

Main Components

Import Options

Components can be imported from the main package or via sub-path imports:

// Main package import
import { MasterSetting } from "@mbc-cqrs-serverless/master-web";

// Sub-path import
import MasterSetting from "@mbc-cqrs-serverless/master-web/MasterSetting";

MasterSetting

Displays a list of master settings with search, filter, and pagination capabilities.

import { MasterSetting } from "@mbc-cqrs-serverless/master-web";
import "@mbc-cqrs-serverless/master-web/styles.css";

export default function MasterSettingsPage() {
return <MasterSetting />;
}

EditMasterSettings

Form component for creating and editing master settings.

import { EditMasterSettings } from "@mbc-cqrs-serverless/master-web";

export default function EditMasterSettingsPage({ params }: { params: { id: string } }) {
return <EditMasterSettings id={params.id} />;
}

CopyMasterSettings

Component for copying master settings to create new settings based on existing ones.

import { CopyMasterSettings } from "@mbc-cqrs-serverless/master-web";

export default function CopyMasterSettingsPage({ params }: { params: { id: string } }) {
return <CopyMasterSettings id={params.id} />;
}

NewCopyMasterSettings

Component for creating a new copy of master settings with a new identifier.

import { NewCopyMasterSettings } from "@mbc-cqrs-serverless/master-web";

export default function NewCopyMasterSettingsPage({ params }: { params: { id: string } }) {
return <NewCopyMasterSettings id={params.id} />;
}

DetailCopy

Component for viewing detailed copy information of master settings.

import { DetailCopy } from "@mbc-cqrs-serverless/master-web";

export default function DetailCopyPage({ params }: { params: { id: string } }) {
return <DetailCopy id={params.id} />;
}

MasterData

Displays master data records in a table format with CRUD operations.

import { MasterData } from "@mbc-cqrs-serverless/master-web";

export default function MasterDataPage() {
return <MasterData />;
}

EditMasterData

Form component for creating and editing master data records. Automatically renders form controls based on the fields definition in the corresponding master setting's attributes.fields.

The component always displays code and name as fixed fields. Any additional fields defined in attributes.fields (other than code and name) are rendered as custom form controls based on their uiComponent type.

import { EditMasterData } from "@mbc-cqrs-serverless/master-web";

export default function EditMasterDataPage({ params }: { params: { id: string } }) {
return <EditMasterData id={params.id} />;
}

AddJsonData

JSON editor component for bulk importing master settings and master data via JSON. This component is used internally by EditMasterSettings for the JSON import tab.

Create-only by Default

AddJsonData uses hardcoded API URLs (/master-setting/bulk for settings, /master-data/bulk for data) which call the framework's createBulk method. This means re-importing JSON data for existing records will fail with a BadRequestException. To support upsert behavior, use an Axios interceptor to rewrite these URLs to custom upsert endpoints. See Master - Upsert Pattern for details.

Provider Setup

Wrap your application with the required providers for authentication and API access.

AppProviders

The AppProviders component requires a user prop of type UserContext containing tenant information.

import { AppProviders } from "@mbc-cqrs-serverless/master-web";
import type { UserContext } from "@mbc-cqrs-serverless/master-web";

export default function RootLayout({ children }: { children: React.ReactNode }) {
// UserContext contains tenant information
const user: UserContext = {
tenantCode: "your-tenant-code",
tenantRole: "admin",
};

return (
<AppProviders user={user}>
{children}
</AppProviders>
);
}

AppProviders Props

PropTypeRequiredDescription
userUserContextYesUser context with tenant information
httpClientAxiosInstanceNoCustom Axios instance for HTTP requests
apolloClientApolloClientNoCustom Apollo Client instance
urlProviderIUrlProviderNoCustom URL provider instance

UserContext Type

The UserContext type defines the shape of the user object. You can use the useUserContext hook's return type or define a compatible type:

type UserContext = {
tenantCode: string; // Tenant identifier
tenantRole: string; // User role within the tenant
};
Type Usage

While UserContext is used internally, you can create a compatible object type for the user prop.

URL Provider

The package provides a URL provider system for managing application URLs.

IUrlProvider Interface

The IUrlProvider interface defines the contract for URL generation:

import type { IUrlProvider } from "@mbc-cqrs-serverless/master-web";

// Interface definition
interface IUrlProvider {
// Static URLs
readonly SETTINGS_PAGE_URL: string;
readonly ADD_SETTINGS_PAGE_URL: string;
readonly EDIT_SETTINGS_PAGE_URL: string;
readonly DATA_PAGE_URL: string;
readonly ADD_DATA_PAGE_URL: string;
readonly EDIT_DATA_PAGE_URL: string;
readonly FAQ_CATEGORY_PAGE_URL: string;
readonly TOP_URL: string;

// Dynamic URL generators
getCopySettingPageUrl(id: string): string;
getDetailedCopySettingPageUrl(id: string): string;
}

BaseUrlProvider Class

The BaseUrlProvider class provides a default implementation that can be extended:

import { BaseUrlProvider, IUrlProvider } from "@mbc-cqrs-serverless/master-web/UrlProvider";

// Create a URL provider with a base segment
const urlProvider = new BaseUrlProvider("my-tenant");

// Access static URLs
console.log(urlProvider.SETTINGS_PAGE_URL); // "/my-tenant/master-setting"
console.log(urlProvider.DATA_PAGE_URL); // "/my-tenant/master-data"

// Generate dynamic URLs
console.log(urlProvider.getCopySettingPageUrl("123")); // "/my-tenant/master-setting/123/copy/new"
Sub-path Import

The BaseUrlProvider and IUrlProvider are available via the sub-path import @mbc-cqrs-serverless/master-web/UrlProvider. The IUrlProvider type is also exported from the main package.

Custom URL Provider

You can create a custom URL provider by extending BaseUrlProvider or implementing the IUrlProvider interface:

import { AppProviders } from "@mbc-cqrs-serverless/master-web";
import { BaseUrlProvider } from "@mbc-cqrs-serverless/master-web/UrlProvider";

// Extend BaseUrlProvider for custom path structure
class CustomUrlProvider extends BaseUrlProvider {
constructor(tenantCode: string) {
super(`members/${tenantCode}`);
}
}

// Use custom URL provider with AppProviders
const customUrlProvider = new CustomUrlProvider("my-tenant");

<AppProviders user={user} urlProvider={customUrlProvider}>
{children}
</AppProviders>

Custom Hooks

useApolloClient

Access the Apollo Client for GraphQL operations.

import { useApolloClient } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const client = useApolloClient();
// Use client for custom GraphQL queries
}

useHttpClient

Access the HTTP client for REST API calls.

import { useHttpClient } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const httpClient = useHttpClient();
// Use httpClient for custom API requests
}

useUserContext

Access the current user context with tenant information.

import { useUserContext } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const { tenantCode, tenantRole } = useUserContext();
// Access tenant information
console.log(`Tenant: ${tenantCode}, Role: ${tenantRole}`);
}

useLoadingStore

Manage global loading state across components.

import { useLoadingStore } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const { isLoading, setLoading, closeLoading } = useLoadingStore();

// Show loading indicator
setLoading();

// Hide loading indicator
closeLoading();
}

useUrlProvider

Access the URL provider for generating application URLs.

import { useUrlProvider } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const urlProvider = useUrlProvider();

// Use static URLs
const settingsUrl = urlProvider.SETTINGS_PAGE_URL;

// Generate dynamic URLs
const copyUrl = urlProvider.getCopySettingPageUrl("item-123");
}

useAppServices

Access all application services at once. Returns the HTTP client, Apollo client, user context, and URL provider.

import { useAppServices } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const { httpClient, apolloClient, user, urlProvider } = useAppServices();

// Use multiple services in one component
const fetchData = async () => {
const response = await httpClient.get("/api/data");
// ...
};
}

Return Value:

PropertyTypeDescription
httpClientAxiosInstanceHTTP client for REST API calls
apolloClientApolloClientApollo client for GraphQL operations
userUserContextCurrent user context and authentication state
urlProviderIUrlProviderURL provider for generating URLs

useSubscribeCommandStatus

Internal API

This hook is not exported from the main package and is for internal use only. It may change without notice.

Subscribe to AppSync command status updates. Used to track the progress and completion of backend commands.

import { useSubscribeCommandStatus } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const { isListening, message, start } = useSubscribeCommandStatus(
tenantCode,
async (msg) => {
if (msg) {
// Command completed successfully
console.log("Command finished:", msg);
} else {
// Command timed out
console.log("Command timed out");
}
},
true // Show processing toast
);

const handleSubmit = async () => {
const requestId = await submitCommand();
start(requestId, 30000); // Start listening with 30s timeout
};
}

Parameters:

ParameterTypeDescription
xTenantCodestringTenant code for the subscription
doneCallback(msg: DecodedMessage | null) => voidCallback when command completes or times out
isShowProcessbooleanWhether to show processing toast (default: true)

Return Value:

PropertyTypeDescription
isListeningbooleanWhether actively listening for updates
messageDecodedMessage | nullLatest received message
start(reqId: string, timeoutMs?: number) => voidStart listening for a request ID

useSubscribeBulkCommandStatus

Internal API

This hook is not exported from the main package and is for internal use only. It may change without notice.

Subscribe to bulk command status updates. Used when processing multiple items where each receives its own completion message.

import { useSubscribeBulkCommandStatus } from "@mbc-cqrs-serverless/master-web";

function BulkOperationComponent() {
const { isListening, messages, finishedCount, start, stop } =
useSubscribeBulkCommandStatus(
tenantCode,
() => {
// Handle timeout
console.log("Bulk operation timed out");
}
);

const handleBulkSubmit = async (items: Item[]) => {
const requestId = await submitBulkCommand(items);
start(requestId, 60000); // 60s timeout
};

// Check if all items are processed
useEffect(() => {
if (finishedCount === expectedCount) {
stop();
// All items processed
}
}, [finishedCount]);
}

Parameters:

ParameterTypeDescription
xTenantCodestringTenant code for the subscription
onTimeout() => voidOptional callback when operation times out

Return Value:

PropertyTypeDescription
isListeningbooleanWhether actively listening for updates
messagesDecodedMessage[]All received messages
finishedCountnumberNumber of completed items
start(reqId: string, timeoutMs?: number) => voidStart listening for a request ID
stop() => voidManually stop listening

useHealthCheck

Internal API

This hook is not exported from the main package and is for internal use only. It may change without notice.

Performs a health check API call on component mount. Controlled by the NEXT_PUBLIC_ENABLE_HEALTH_CHECK environment variable.

import { useHealthCheck } from "@mbc-cqrs-serverless/master-web";

function App() {
// Automatically calls health check endpoint on mount
useHealthCheck();

return <div>Application content</div>;
}

Environment Variable:

VariableDescription
NEXT_PUBLIC_ENABLE_HEALTH_CHECKSet to "true" to enable health check calls

usePagination

Internal API

This hook is not exported from the main package and is for internal use only. It may change without notice.

Comprehensive hook for handling pagination with search, sorting, and table views. Integrates with URL query parameters for persistent state.

import { usePagination } from "@mbc-cqrs-serverless/master-web";
import { parseAsString } from "next-usequerystate";

interface SearchProps extends SearchPropsBase {
name?: string;
status?: string;
}

function DataListPage() {
const {
searchProps,
paginate,
onSubmitSearch,
executeSearch,
handlePaginationChange,
handleSortChange,
} = usePagination<DataRecord, SearchProps, Paginate<DataRecord>>({
searchPropDefinitions: {
name: parseAsString,
status: parseAsString,
},
getData: async (queries) => {
return await fetchData(queries);
},
rootPath: "/data-list",
isSearchInit: true,
});

useEffect(() => {
executeSearch();
}, []);

return (
<DataTable
data={paginate?.results ?? []}
onPaginationChange={handlePaginationChange}
onSortChange={handleSortChange}
/>
);
}

Parameters:

ParameterTypeDescription
searchPropDefinitionsUseQueryStatesKeysMapQuery parameter definitions using next-usequerystate
getData(queries) => Promise<Paginate>Function to fetch paginated data from server
getDataClient(queries) => Promise<Array>Function for client-side data filtering
isSearchInitbooleanWhether to search on initial load (default: true)
rootPathstringRoot path for the page (used for path validation)
tableViewsTableView[]Optional predefined table view configurations
getStorage() => SearchPropsFunction to retrieve saved search conditions
setStorage(props) => voidFunction to save search conditions
resetUseFormResetForm reset function from react-hook-form
setValueUseFormSetValueForm setValue function from react-hook-form
convertSearchProps(props) => SearchPropsOptional function to convert search props before executing search
convertChangeQueries(props) => SearchPropsOptional function to convert query parameters before URL change

Return Value:

PropertyTypeDescription
searchPropsSearchPropsCurrent search parameters
queriesSearchPropsURL query parameters
setQueries(props) => Promise<void>Update URL query parameters directly
paginatePaginate<T>Paginated results with count and data
setPaginate(paginate) => voidManually set paginate state
setPaginateClient(items, page?) => voidSet paginate for client-side data
getPaginateClient(items) => PaginateConvert array to paginate object
isCalledSearchbooleanWhether search has been triggered
onSubmitSearch(props) => Promise<void>Submit search with new parameters
executeSearch() => Promise<object>Execute search with current parameters
searchUsingTableView(props, tableView) => Promise<void>Search using a predefined table view
getSearchQuery() => SearchProps | nullGet current search query from URL or storage
handlePaginationChangeOnChangeFn<PaginationState>Handler for page/size changes
handleSortChangeOnChangeFn<SortingState>Handler for sort changes
onResetSearchForm() => Promise<void>Reset search form to empty values

usePaginationRange

Internal API

This hook and the DOTS constant are not exported from the main package and are for internal use only. They may change without notice.

Calculates the page number range for pagination UI, including ellipsis for large page counts.

import { usePaginationRange, DOTS } from "@mbc-cqrs-serverless/master-web";

function PaginationUI({ totalPages, currentPage }: Props) {
const range = usePaginationRange({
totalPageCount: totalPages,
currentPage: currentPage,
siblingCount: 1,
});

return (
<nav>
{range.map((item, index) => (
item === DOTS ? (
<span key={index}>...</span>
) : (
<button key={index} onClick={() => goToPage(item as number)}>
{item}
</button>
)
))}
</nav>
);
}

Parameters:

ParameterTypeDescription
totalPageCountnumberTotal number of pages
currentPagenumberCurrent active page number
siblingCountnumberNumber of page buttons to show on each side (default: 1)

Return Value:

(number | string)[] - Array of page numbers and DOTS ("...") for ellipsis

useLoadingForm

Internal API

This hook is not exported from the main package and is for internal use only. It may change without notice.

Combines react-hook-form with global loading state. Provides form utilities along with loading state management.

import { useLoadingForm } from "@mbc-cqrs-serverless/master-web";

interface FormData {
name: string;
email: string;
}

function MyForm() {
const {
form,
control,
handleSubmit,
loading,
loadingStore,
errors,
} = useLoadingForm<FormData>({
defaultValues: {
name: "",
email: "",
},
});

const onSubmit = async (data: FormData) => {
loadingStore.setLoading();
try {
await saveData(data);
} finally {
loadingStore.closeLoading();
}
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Form fields */}
</form>
);
}

Parameters:

ParameterTypeDescription
propsUseFormProps<T>react-hook-form useForm options

Return Value:

PropertyTypeDescription
formUseFormReturn<T>Full react-hook-form instance
controlControl<T>Form control for controlled components
handleSubmitUseFormHandleSubmit<T>Form submit handler
watchUseFormWatch<T>Watch form values
getValuesUseFormGetValues<T>Get form values
setValueUseFormSetValue<T>Set form values
resetUseFormReset<T>Reset form
triggerUseFormTrigger<T>Trigger validation
errorsFieldErrors<T>Form validation errors
setErrorUseFormSetError<T>Set form error manually
loadingbooleanCurrent loading state
loadingStoreLoadingStateLoading store with setLoading/closeLoading
isValidbooleanWhether form is valid

useAsyncAction

Internal API

This hook is not exported from the main package and is for internal use only. It may change without notice.

Execute async functions with automatic loading overlay. Shows global loading indicator during async operations.

import { useAsyncAction } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const { performAction, isLoading } = useAsyncAction();

const handleClick = async () => {
const result = await performAction(async () => {
// This runs with loading overlay
return await fetchData();
});
console.log(result);
};

return (
<button onClick={handleClick} disabled={isLoading}>
Load Data
</button>
);
}

Return Value:

PropertyTypeDescription
performAction<T>(fn: () => Promise<T>) => Promise<T>Execute async function with loading overlay
isLoadingbooleanCurrent loading state

useNavigation

Internal API

This hook is not exported from the main package and is for internal use only. It may change without notice.

Navigate between pages with automatic loading indicator. Wraps Next.js router with loading state management.

import { useNavigation } from "@mbc-cqrs-serverless/master-web";

function MyComponent() {
const { navigate, reload, hardNavigate } = useNavigation();

return (
<div>
<button onClick={() => navigate("/dashboard")}>
Go to Dashboard
</button>
<button onClick={() => reload()}>
Refresh Page
</button>
<button onClick={() => hardNavigate("/external-page")}>
Full Page Navigation
</button>
</div>
);
}

Return Value:

PropertyTypeDescription
navigate(url: string) => voidNavigate using Next.js router with loading indicator
reload() => voidRefresh current page with loading indicator
hardNavigate(url: string) => voidFull browser navigation (window.location)

UI Components

The package includes several reusable UI components:

JsonEditor

A JSON editor component for editing structured data. Uses the jsoneditor library with tree mode.

import { JsonEditor } from "@mbc-cqrs-serverless/master-web";

function MyForm() {
const [jsonData, setJsonData] = useState({ key: "value" });

return (
<JsonEditor
json={jsonData}
onChange={setJsonData}
/>
);
}

JsonEditor Props

PropTypeRequiredDescription
jsonobjectYesThe JSON data to display and edit
onChange(json: object) => voidNoCallback when JSON content changes

RichTextEditor

A rich text editor for content fields. Built on React Quill with customizable toolbar.

import { RichTextEditor } from "@mbc-cqrs-serverless/master-web";

function MyForm() {
const [content, setContent] = useState("");

return (
<RichTextEditor
value={content}
onChange={setContent}
placeholder="Enter your content here..."
/>
);
}

RichTextEditor Props

PropTypeRequiredDescription
valuestringNoThe HTML content for the editor (defaults to empty string)
onChange(value: string) => voidYesCallback when content changes
placeholderstringNoPlaceholder text when editor is empty (defaults to empty string)

MsLayout

Layout component for master management pages. Provides loading overlay and toast notifications.

import { MsLayout } from "@mbc-cqrs-serverless/master-web";

function MasterPage() {
return (
<MsLayout useLoading={true}>
<MasterSetting />
</MsLayout>
);
}

MsLayout Props

PropTypeRequiredDescription
useLoadingbooleanYesEnable or disable the loading overlay
childrenReact.ReactNodeYesChild components to render

ConfirmButton

Internal API

This component is not exported from the main package and is for internal use only. It may change without notice.

Button component that displays a confirmation dialog before executing an action. Useful for destructive operations like delete.

ConfirmButton Props

PropTypeDefaultDescription
size'default' | 'sm' | 'lg' | 'icon''default'Button size
triggerBtnTextstring-Text displayed on the trigger button
titlestring-Title of the confirmation dialog
cancelTextstring-Text for the cancel button
confirmTextstring-Text for the confirm button
loadingbooleanfalseShows loading state on the button
onConfirm() => void-Callback function when confirm is clicked
classNamestring-Additional CSS classes
disabledbooleanfalseDisables the button
variantstring-Button variant style
// Note: Internal import path - may change without notice
import ConfirmButton from "@mbc-cqrs-serverless/master-web/dist/components/buttons/ConfirmButton";

function DeleteAction() {
const handleDelete = () => {
// Perform delete operation
};

return (
<ConfirmButton
triggerBtnText="Delete"
title="Are you sure you want to delete this item?"
cancelText="Cancel"
confirmText="Delete"
variant="destructive"
onConfirm={handleDelete}
/>
);
}

BackButton

Internal API

This component is not exported from the main package and is for internal use only. It may change without notice.

Navigation button component for returning to the previous page or a specified location.

BackButton Props

PropTypeDefaultDescription
onClickPrev() => void-Callback function when button is clicked
classNamestring-Additional CSS classes
// Note: Internal import path - may change without notice
import { BackButton } from "@mbc-cqrs-serverless/master-web/dist/components/buttons/back-button";
import { useRouter } from "next/navigation";

function DetailPage() {
const router = useRouter();

return (
<div>
{/* Page content */}
<BackButton onClickPrev={() => router.back()} />
</div>
);
}

DatePicker

Internal API

This component is not exported from the main package and is for internal use only. It may change without notice.

Date selection component with calendar popup. Uses date-fns for formatting and Japanese locale support.

DatePicker Props

PropTypeDefaultDescription
valueDate | string-Current selected date value
onChange(date?: string) => void-Callback when date is selected (returns ISO string with timezone)
disabledbooleanfalseDisables the date picker
// Note: Internal import path - may change without notice
import DatePicker from "@mbc-cqrs-serverless/master-web/dist/components/form/DatePicker";
import { useState } from "react";

function DateForm() {
const [date, setDate] = useState<string>();

return (
<DatePicker
value={date}
onChange={(newDate) => setDate(newDate)}
/>
);
}

FormSubmitButton

Internal API

This component is not exported from the main package and is for internal use only. It may change without notice.

Submit button component designed to work with react-hook-form. Automatically handles form state, validation errors, and loading states.

FormSubmitButton Props

PropTypeDefaultDescription
childrenReact.ReactNode-Button content
disabledbooleanfalseManually disable the button
loadingbooleanfalseShows loading state
classNamestring-Additional CSS classes
disableDirtybooleanfalseIf true, button is enabled even when form is not dirty

The button is automatically disabled when:

  • There are validation errors in the form
  • The form has not been modified (unless disableDirty is true)
// Note: Internal import path - may change without notice
import FormSubmitButton from "@mbc-cqrs-serverless/master-web/dist/components/form/FormSubmitButton";
import { FormProvider, useForm } from "react-hook-form";
import { useState } from "react";

function MyForm() {
const methods = useForm();
const [isSubmitting, setIsSubmitting] = useState(false);

return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{/* Form fields */}
<FormSubmitButton loading={isSubmitting}>
Save
</FormSubmitButton>
</form>
</FormProvider>
);
}

DataTable

Internal API

This component is not exported from the main package and is for internal use only. It may change without notice.

Full-featured data table component built on TanStack Table. Supports server-side pagination, sorting, row selection, and custom column definitions.

DataTable Props

PropTypeDefaultDescription
columnsColumnDef<TData, TValue>[]-Column definitions using TanStack Table format
dataTData[]-Array of data to display
pageCountnumber-Total number of pages
rowCountnumber-Total number of rows
paginationPaginationState-Current pagination state (pageIndex, pageSize)
onPaginationChange(pagination: PaginationState) => void-Callback when pagination changes
sortingSortingState-Current sorting state
onSortingChange(sorting: SortingState) => void-Callback when sorting changes
onClickRow(row: TData) => void-Callback when a row is clicked
rowKeykeyof TData | ((row: TData) => string)-Key extractor for row identification
rowSelectionRowSelectionState-Current row selection state
onRowSelectionChange(state: RowSelectionState) => void-Callback when row selection changes
State Types

PaginationState, SortingState, and RowSelectionState are TanStack Table types. PaginationState contains pageIndex and pageSize properties.

DataTable Features

  • Server-side pagination with page size options (10, 20, 50, 100)
  • Jump to specific page functionality
  • Column sorting
  • Row selection
  • Click handler for row navigation
  • Empty state display
  • Custom column widths via column meta
// Note: Internal import path - may change without notice
import { DataTable } from "@mbc-cqrs-serverless/master-web/dist/components/table/data-table";
import { ColumnDef, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table";
import { useState } from "react";
import { useRouter } from "next/navigation";

type User = {
id: string;
name: string;
email: string;
};

const columns: ColumnDef<User>[] = [
{
accessorKey: "name",
header: "Name",
meta: { size: "200px" },
},
{
accessorKey: "email",
header: "Email",
},
];

function UserList() {
const router = useRouter();
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [sorting, setSorting] = useState<SortingState>([]);

// Fetch data based on pagination and sorting
const { data, pageCount, rowCount } = useUsers(pagination, sorting);

return (
<DataTable
columns={columns}
data={data}
pageCount={pageCount}
rowCount={rowCount}
pagination={pagination}
onPaginationChange={setPagination}
sorting={sorting}
onSortingChange={setSorting}
rowKey="id"
onClickRow={(row) => router.push(`/users/${row.id}`)}
/>
);
}

LoadingOverlay

Internal API

This component is not exported from the main package and is for internal use only. It may change without notice.

Full-screen loading overlay component with spinner animation. Useful for indicating loading state during async operations.

LoadingOverlay Props

PropTypeDefaultDescription
isLoadingboolean-Controls visibility of the overlay
// Note: Internal import path - may change without notice
import LoadingOverlay from "@mbc-cqrs-serverless/master-web/dist/components/LoadingOverlay";
import { useState } from "react";

function MyPage() {
const [isLoading, setIsLoading] = useState(false);

const handleSubmit = async () => {
setIsLoading(true);
try {
await saveData();
} finally {
setIsLoading(false);
}
};

return (
<div>
<LoadingOverlay isLoading={isLoading} />
{/* Page content */}
</div>
);
}

Environment Variables

Configure the following environment variables for the Master Web package:

VariableDescription
NEXT_PUBLIC_MASTER_API_BASEBase URL for REST API endpoints
NEXT_PUBLIC_MASTER_APPSYNC_URLAWS AppSync GraphQL endpoint URL
NEXT_PUBLIC_MASTER_APPSYNC_APIKEYAWS AppSync API key for authentication
NEXT_PUBLIC_MASTER_APPSYNC_REGIONAWS region for AppSync

Example .env.local

NEXT_PUBLIC_MASTER_API_BASE=https://api.example.com
NEXT_PUBLIC_MASTER_APPSYNC_URL=https://xxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
NEXT_PUBLIC_MASTER_APPSYNC_APIKEY=da2-xxxxxxxxxxxxxxxxx
NEXT_PUBLIC_MASTER_APPSYNC_REGION=ap-northeast-1

Styling

Import the package styles in your application:

import "@mbc-cqrs-serverless/master-web/styles.css";

The components use Tailwind CSS for styling. Ensure your project has Tailwind CSS configured.

Next.js App Router Integration

Version Note

In v0.0.42, React/Next.js were externalized as peer dependencies, resolving the Context isolation issue that caused httpClient.get is not a function errors. If you are using v0.0.41 or earlier, upgrade to v0.0.42 or later for the fix.

When using master-web components with Next.js App Router (v14+/v15), there are important considerations for handling Server-Side Rendering (SSR) and client-side state.

SSR Compatibility Issue

The JsonEditor component uses the jsoneditor library internally, which requires browser APIs (self, window) that are not available during SSR. This causes errors like:

ReferenceError: self is not defined

Solution: Dynamic Import with SSR Disabled

Use Next.js dynamic imports with ssr: false to load master-web components only on the client side:

'use client'

import dynamic from 'next/dynamic'
import { useMemo } from 'react'

// Create wrapper component that handles dynamic import
function MasterSettingWrapper({ httpClient, urlProvider, user }) {
// Dynamic import inside component for proper context handling
const MasterSetting = useMemo(
() =>
dynamic(() => import('@mbc-cqrs-serverless/master-web/MasterSetting'), {
ssr: false,
loading: () => <div>Loading...</div>,
}),
[]
)

return (
<AppProviders user={user} httpClient={httpClient} urlProvider={urlProvider}>
<MasterSetting />
</AppProviders>
)
}
Important

Define the dynamic import inside a wrapper component (using useMemo) rather than at the module level. This ensures the AppProviders context is properly available when the component mounts.

The most recommended implementation pattern is to use Next.js App Router's layout.tsx to set up AppProviders. This pattern reliably avoids the httpClient.get is not a function error.

  1. Solves React Context Isolation Issues: React Context bundled in npm packages can become isolated from the application's context. By setting up providers in the Layout, the Context is guaranteed to be initialized before child components mount.

  2. Synchronous httpClient Initialization: By using useMemo, the httpClient is created synchronously without async state management (useState + useEffect).

  3. Automatic Auth Token Injection: Using Axios interceptors, the latest auth token is automatically retrieved for each request.

Implementation Example: layout.tsx

// app/admin/[tenant]/master/layout.tsx
'use client'

import { useMemo } from 'react'
import dynamic from 'next/dynamic'
import { useParams } from 'next/navigation'
import axios from 'axios'
import { fetchAuthSession } from 'aws-amplify/auth'
import type { IUrlProvider } from '@mbc-cqrs-serverless/master-web/UrlProvider'
import '@/modules/common/components/ConfigureAmplifyClientSide'

// Dynamic import of AppProviders (SSR disabled)
const AppProviders = dynamic(
() =>
import('@mbc-cqrs-serverless/master-web/AppProviders').then(
(mod) => mod.AppProviders
),
{ ssr: false }
)

// Multi-tenant URL provider
class MasterUrlProvider implements IUrlProvider {
protected readonly baseUrl: string

public readonly SETTINGS_PAGE_URL: string
public readonly ADD_SETTINGS_PAGE_URL: string
public readonly EDIT_SETTINGS_PAGE_URL: string
public readonly DATA_PAGE_URL: string
public readonly ADD_DATA_PAGE_URL: string
public readonly EDIT_DATA_PAGE_URL: string
public readonly FAQ_CATEGORY_PAGE_URL: string
public readonly TOP_URL: string

constructor(tenantCode: string) {
this.baseUrl = `/admin/${tenantCode}/master`

this.SETTINGS_PAGE_URL = `${this.baseUrl}/master-setting`
this.ADD_SETTINGS_PAGE_URL = `${this.baseUrl}/master-setting/new`
this.EDIT_SETTINGS_PAGE_URL = this.SETTINGS_PAGE_URL
this.DATA_PAGE_URL = `${this.baseUrl}/master-data`
this.ADD_DATA_PAGE_URL = `${this.baseUrl}/master-data/new`
this.EDIT_DATA_PAGE_URL = this.DATA_PAGE_URL
this.FAQ_CATEGORY_PAGE_URL = `${this.baseUrl}/faq-category`
this.TOP_URL = `/admin/${tenantCode}`
}

public getCopySettingPageUrl(id: string): string {
return `${this.baseUrl}/master-setting/${id}/copy/new`
}

public getDetailedCopySettingPageUrl(id: string): string {
return `${this.baseUrl}/master-setting/${id}/copy`
}
}

export default function MasterLayout({
children,
}: {
children: React.ReactNode
}) {
const params = useParams<{ tenant: string }>()
const tenantCode = params?.tenant || 'common'

// Create URL provider synchronously with useMemo
const urlProvider = useMemo(() => new MasterUrlProvider(tenantCode), [tenantCode])

// Create httpClient synchronously with useMemo (interceptor auto-injects auth token)
const httpClient = useMemo(() => {
const baseEndpoint = process.env.NEXT_PUBLIC_API_ENDPOINT || 'http://localhost:3010'
const instance = axios.create({
baseURL: `${baseEndpoint}/api`,
headers: {
'Content-Type': 'application/json',
'x-tenant-code': tenantCode,
},
})

// Interceptor to get auth token
instance.interceptors.request.use(async (config) => {
try {
const session = await fetchAuthSession()
const token = session.tokens?.idToken?.toString()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
} catch {
// Ignore auth errors
}
return config
})

return instance
}, [tenantCode])

const user = useMemo(
() => ({
tenantCode,
tenantRole: 'admin',
}),
[tenantCode]
)

return (
<AppProviders user={user} urlProvider={urlProvider} httpClient={httpClient}>
<div className="p-6">{children}</div>
</AppProviders>
)
}

Page Component Implementation

After setting up providers in the Layout, each page component becomes simple:

// app/admin/[tenant]/master/master-setting/page.tsx
'use client'

import dynamic from 'next/dynamic'
import MsLayout from '@mbc-cqrs-serverless/master-web/MsLayout'

const MasterSetting = dynamic(
() => import('@mbc-cqrs-serverless/master-web/MasterSetting').then((mod) => mod.default),
{ ssr: false }
)

export default function MasterSettingPage() {
return (
<main>
<MsLayout useLoading>
<MasterSetting />
</MsLayout>
</main>
)
}
// app/admin/[tenant]/master/master-setting/new/page.tsx
'use client'

import dynamic from 'next/dynamic'
import MsLayout from '@mbc-cqrs-serverless/master-web/MsLayout'

const EditMasterSettings = dynamic(
() => import('@mbc-cqrs-serverless/master-web/EditMasterSettings').then((mod) => mod.default),
{ ssr: false }
)

export default function NewMasterSettingPage() {
return (
<main>
<MsLayout useLoading>
<EditMasterSettings />
</MsLayout>
</main>
)
}
Key Points
  • layout.tsx: Set up AppProviders, httpClient, and urlProvider
  • page.tsx: Simply render components with dynamic imports
  • MsLayout: Provides loading overlay and toast notifications

AWS Amplify v6 Integration

The default httpClient in master-web uses AWS Amplify v5 APIs (Auth.currentSession()). If your project uses AWS Amplify v6, you must provide a custom httpClient.

The Layout-based Provider Pattern example above uses Amplify v6's fetchAuthSession with Axios interceptors. This is the recommended approach.

Alternative Pattern: Configuration in Page Component

Alternative implementation if you don't use the Layout pattern:

'use client'

import { useState, useEffect, useCallback, useMemo } from 'react'
import axios, { AxiosInstance } from 'axios'
import * as Auth from 'aws-amplify/auth' // Amplify v6 import
import { AppProviders } from '@mbc-cqrs-serverless/master-web/AppProviders'
import { BaseUrlProvider } from '@mbc-cqrs-serverless/master-web/UrlProvider'
import dynamic from 'next/dynamic'

interface MasterTemplateProps {
tenantCode: string
}

// Custom URL provider for multi-tenant routing
class MasterUrlProvider extends BaseUrlProvider {
constructor(tenantCode: string) {
// BaseUrlProvider adds leading slash, so omit it here
super(`admin/${tenantCode}/master`)
}
}

// Wrapper component for proper context handling
function MasterSettingWrapper({
httpClient,
urlProvider,
user,
}: {
httpClient: AxiosInstance
urlProvider: MasterUrlProvider
user: { tenantCode: string; tenantRole: string }
}) {
const MasterSetting = useMemo(
() =>
dynamic(() => import('@mbc-cqrs-serverless/master-web/MasterSetting'), {
ssr: false,
loading: () => <div>Loading component...</div>,
}),
[]
)

return (
<AppProviders user={user} httpClient={httpClient} urlProvider={urlProvider}>
<MasterSetting />
</AppProviders>
)
}

export function MasterTemplate({ tenantCode }: MasterTemplateProps) {
const [httpClient, setHttpClient] = useState<AxiosInstance | null>(null)
const [isReady, setIsReady] = useState(false)

// Setup httpClient with Amplify v6 authentication
const setupHttpClient = useCallback(async () => {
let authToken = ''
try {
// Amplify v6 API for fetching auth session
const session = await Auth.fetchAuthSession()
authToken = session.tokens?.idToken?.toString() || ''
} catch {
// Handle unauthenticated state
}

const client = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_ENDPOINT || 'http://localhost:3010',
headers: {
'Content-Type': 'application/json',
'x-tenant-code': tenantCode,
...(authToken && { Authorization: `Bearer ${authToken}` }),
},
})

setHttpClient(client)
setIsReady(true)
}, [tenantCode])

useEffect(() => {
setupHttpClient()
}, [setupHttpClient])

const urlProvider = useMemo(() => new MasterUrlProvider(tenantCode), [tenantCode])

const user = useMemo(
() => ({
tenantCode,
tenantRole: 'admin',
}),
[tenantCode]
)

// Wait for httpClient to be ready before rendering
if (!isReady || !httpClient) {
return <div>Loading...</div>
}

return (
<MasterSettingWrapper
httpClient={httpClient}
urlProvider={urlProvider}
user={user}
/>
)
}

Multi-Tenant Routing Setup

For multi-tenant applications, set up dynamic routes with tenant code in the URL:

Directory Structure

app/
└── admin/
└── [tenant]/
└── master/
├── layout.tsx # AppProviders setup (recommended)
├── page.tsx # Master top page
├── master-setting/
│ ├── page.tsx # Settings list
│ ├── new/
│ │ └── page.tsx # Create new setting
│ └── [pk]/
│ └── [sk]/
│ └── page.tsx # Edit setting
└── master-data/
├── page.tsx # Data list
├── new/
│ └── page.tsx # Create new data
└── [pk]/
└── [sk]/
└── page.tsx # Edit data

Page Component Example

// app/admin/[tenant]/master/master-setting/page.tsx
import { MasterTemplate } from '@/modules/master/templates/MasterTemplate'

export default async function MasterSettingPage({
params,
}: {
params: Promise<{ tenant: string }>
}) {
const { tenant } = await params

return <MasterTemplate tenantCode={tenant} />
}

Common Issues and Solutions

httpClient.get is not a function Error

This error occurs when the httpClient is not properly initialized before the component tries to use it.

Causes:

  • React Context in npm packages becomes isolated from the application's context
  • httpClient is initialized asynchronously and not ready when the component mounts

Solutions:

  1. Use Layout-based Provider Pattern (Recommended): See the recommended pattern above
  2. Use a wrapper component pattern: Ensure httpClient is ready
  3. Add explicit isReady state check: Check before rendering AppProviders
  4. Define dynamic imports inside the wrapper component: Not at module level

URL Routing Issues

If URLs are generated incorrectly (e.g., //admin/... instead of /admin/...), check the BaseUrlProvider configuration:

// ❌ Wrong - double slash issue
class MasterUrlProvider extends BaseUrlProvider {
constructor(tenantCode: string) {
super(`/admin/${tenantCode}/master`) // Leading slash causes issue
}
}

// ✅ Correct - no leading slash
class MasterUrlProvider extends BaseUrlProvider {
constructor(tenantCode: string) {
super(`admin/${tenantCode}/master`) // BaseUrlProvider adds the slash
}
}

Implementing IUrlProvider Directly

When implementing the IUrlProvider interface directly without extending BaseUrlProvider, you need to explicitly define all URL properties. See the Layout-based Provider Pattern example above for reference.

Dependencies

Key dependencies used by this package:

  • React 18.x
  • Next.js 14.x / 15.x
  • TanStack React Table 8.x
  • Apollo Client
  • Radix UI components
  • Tailwind CSS 3.x
  • react-hook-form
  • Zod for validation

Changelog

Version History

See Web Packages Changelog for all version history and release notes.