{"slug": "react-hook-form-and-zod-validation-in-bryntum-scheduler-pro", "title": "React Hook Form and Zod validation in Bryntum Scheduler Pro", "summary": "Bryntum, a scheduling component vendor, released a guide and GitHub repository demonstrating how to replace the built-in task editor in Bryntum Scheduler Pro with a custom React Hook Form dialog and Zod validation schema. The tutorial covers setting up a Vite React TypeScript app, installing Scheduler Pro trial and React Hook Form, and integrating a Bryntum date widget while moving validation rules into a Zod schema.", "body_md": "# React Hook Form and Zod validation in Bryntum Scheduler Pro\n\nWe strive to keep posts updated, but code samples may sometimes be outdated. Humans, see the\n\n[Bryntum documentation]; agents,[https://mcp.bryntum.com]for the latest info.\n\nBryntum’s suite of scheduling components is full of forms including task, event, resource, and calendar editors. The [Bryntum Scheduler Pro](https://bryntum.com/products/schedulerpro/) includes a task editor form with validation that can be customized, turned off, or replaced with a custom editor. Bryntum components come with over 100 [widgets](https://bryntum.com/products/schedulerpro/docs/api/widgets) that you can use to modify the forms including DateField, Combo, and ColorPicker.\n\nYou may want to replace the task editor with a custom one if you’re adding the Scheduler Pro to an app that uses UI library form components, such as Material UI, and a form validation library such as [React Hook Form](https://react-hook-form.com/), the popular React form state management and validation library. Replacing the task editor can keep validation and form components consistent with the rest of the application.\n\nIn this guide, we’ll show you how to customize and validate Scheduler Pro’s built-in task editor. We’ll then replace it with a React Hook Form dialog, show how to add a Bryntum date widget, and move the validation rules into a [Zod](https://zod.dev/) schema.\n\nYou can find the code in the [Bryntum Scheduler Pro React Hook Form and Zod GitHub repo](https://github.com/bryntum/bryntum-scheduler-pro-react-hook-form-zod).\n\n## Getting started: Setting up a React Bryntum Scheduler Pro\n\nRun the following commands in your terminal to create a Vite React TypeScript application and install Scheduler Pro, its React wrapper, and React Hook Form:\n\n```\nnpm create vite@latest schedulerpro-react-hook-form -- --template react-ts\ncd schedulerpro-react-hook-form\nnpm install\nnpm install @bryntum/schedulerpro@npm:@bryntum/schedulerpro-trial @bryntum/schedulerpro-react\nnpm install react-hook-form\n```\n\nIf you have a Bryntum license, follow the [npm repository guide](https://bryntum.com/products/schedulerpro/docs/guide/SchedulerPro/npm-repository) to access the private Bryntum repository and install `@bryntum/schedulerpro`\n\ninstead of the trial package.\n\nDelete the Vite starter files that the tutorial does not use:\n\n```\nrm src/App.css && rm -r src/assets\n```\n\nUpdate the `vite.config.ts`\n\nfile so Vite prebundles both Bryntum packages once during development:\n\n``` python\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n    plugins: [react()],\n    optimizeDeps: {\n        include: ['@bryntum/schedulerpro', '@bryntum/schedulerpro-react'],\n    },\n});\n```\n\nThe `optimizeDeps`\n\nentry prevents Vite from processing the Scheduler Pro packages as separate dependency graphs during development.\n\nReplace the contents of `src/index.css`\n\nwith the Bryntum structural CSS, icons, [Svalbard light theme](https://bryntum.com/products/schedulerpro/docs/guide/SchedulerPro/customization/styling#using-a-theme), and the page sizing styles:\n\n```\n@import \"@bryntum/schedulerpro/fontawesome/css/fontawesome.css\";\n@import \"@bryntum/schedulerpro/fontawesome/css/solid.css\";\n@import \"@bryntum/schedulerpro/schedulerpro.css\";\n@import \"@bryntum/schedulerpro/svalbard-light.css\";\n@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap\");\n\nhtml,\nbody,\n#root {\n    height: 100%;\n    margin: 0;\n}\n\nbody {\n    font-family: 'Poppins', 'Segoe UI', Arial, sans-serif;\n}\n\n#root {\n    display: flex;\n    flex-direction: column;\n}\n```\n\nThe first four imports provide Scheduler Pro’s structural styles, icons, and theme. The full-height flex layout makes the scheduler fill the viewport height.\n\n## Loading the Scheduler Pro data\n\nWe’ll load the resources, events, and assignments data from one local file. Create a `data.json`\n\nfile in the `public`\n\nfolder and add the following JSON to it:\n\n```\n{\n    \"success\": true,\n    \"resources\": {\n        \"rows\": [\n            { \"id\": 1, \"name\": \"Dan Stevenson\" },\n            { \"id\": 2, \"name\": \"Talisha Babin\" },\n            { \"id\": 3, \"name\": \"Michael Chen\" },\n            { \"id\": 4, \"name\": \"Sophia Rodriguez\" },\n            { \"id\": 5, \"name\": \"Arjun Mehta\" },\n            { \"id\": 6, \"name\": \"Priya Nair\" },\n            { \"id\": 7, \"name\": \"Liam O'Connor\" },\n            { \"id\": 8, \"name\": \"Grace Kim\" },\n            { \"id\": 9, \"name\": \"Noah Fischer\" },\n            { \"id\": 10, \"name\": \"Isabella Costa\" },\n            { \"id\": 11, \"name\": \"Ethan Walker\" },\n            { \"id\": 12, \"name\": \"Maya Patel\" }\n        ]\n    },\n    \"events\": {\n        \"rows\": [\n            { \"id\": 1, \"name\": \"Project Kickoff\", \"startDate\": \"2026-10-05\", \"duration\": 2, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 2, \"name\": \"Requirement Gathering\", \"startDate\": \"2026-10-07\", \"duration\": 4, \"durationUnit\": \"d\", \"priority\": \"high\" },\n            { \"id\": 3, \"name\": \"UI/UX Design\", \"startDate\": \"2026-10-12\", \"duration\": 5, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 4, \"name\": \"Backend Development\", \"startDate\": \"2026-10-19\", \"duration\": 7, \"durationUnit\": \"d\", \"priority\": \"high\" },\n            { \"id\": 5, \"name\": \"Frontend Development\", \"startDate\": \"2026-10-26\", \"duration\": 6, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 6, \"name\": \"API Integration\", \"startDate\": \"2026-11-02\", \"duration\": 4, \"durationUnit\": \"d\", \"priority\": \"low\" },\n            { \"id\": 7, \"name\": \"Testing & QA\", \"startDate\": \"2026-11-06\", \"duration\": 3, \"durationUnit\": \"d\", \"priority\": \"high\" },\n            { \"id\": 8, \"name\": \"Final Deployment\", \"startDate\": \"2026-11-10\", \"duration\": 2, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 9, \"name\": \"Stakeholder Review\", \"startDate\": \"2026-10-08\", \"duration\": 2, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 10, \"name\": \"Data Migration\", \"startDate\": \"2026-10-14\", \"duration\": 5, \"durationUnit\": \"d\", \"priority\": \"high\" },\n            { \"id\": 11, \"name\": \"Security Audit\", \"startDate\": \"2026-10-21\", \"duration\": 3, \"durationUnit\": \"d\", \"priority\": \"high\" },\n            { \"id\": 12, \"name\": \"Performance Tuning\", \"startDate\": \"2026-10-27\", \"duration\": 4, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 13, \"name\": \"Documentation\", \"startDate\": \"2026-11-03\", \"duration\": 3, \"durationUnit\": \"d\", \"priority\": \"low\" },\n            { \"id\": 14, \"name\": \"User Training\", \"startDate\": \"2026-11-09\", \"duration\": 2, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 15, \"name\": \"Vendor Onboarding\", \"startDate\": \"2026-10-06\", \"duration\": 3, \"durationUnit\": \"d\", \"priority\": \"low\" },\n            { \"id\": 16, \"name\": \"Infrastructure Setup\", \"startDate\": \"2026-10-13\", \"duration\": 6, \"durationUnit\": \"d\", \"priority\": \"high\" },\n            { \"id\": 17, \"name\": \"Load Testing\", \"startDate\": \"2026-10-22\", \"duration\": 3, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 18, \"name\": \"Accessibility Review\", \"startDate\": \"2026-10-29\", \"duration\": 2, \"durationUnit\": \"d\", \"priority\": \"low\" },\n            { \"id\": 19, \"name\": \"Localization\", \"startDate\": \"2026-11-04\", \"duration\": 4, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 20, \"name\": \"Go-Live Support\", \"startDate\": \"2026-11-11\", \"duration\": 3, \"durationUnit\": \"d\", \"priority\": \"high\" },\n            { \"id\": 21, \"name\": \"Design Handoff\", \"startDate\": \"2026-10-06\", \"duration\": 2, \"durationUnit\": \"d\", \"priority\": \"medium\" },\n            { \"id\": 22, \"name\": \"Analytics Setup\", \"startDate\": \"2026-10-15\", \"duration\": 3, \"durationUnit\": \"d\", \"priority\": \"low\" },\n            { \"id\": 23, \"name\": \"Payment Integration\", \"startDate\": \"2026-10-20\", \"duration\": 5, \"durationUnit\": \"d\", \"priority\": \"high\" },\n            { \"id\": 24, \"name\": \"Regression Testing\", \"startDate\": \"2026-11-05\", \"duration\": 3, \"durationUnit\": \"d\", \"priority\": \"medium\" }\n        ]\n    },\n    \"assignments\": {\n        \"rows\": [\n            { \"id\": 1, \"event\": 1, \"resource\": 1 },\n            { \"id\": 2, \"event\": 2, \"resource\": 2 },\n            { \"id\": 3, \"event\": 3, \"resource\": 3 },\n            { \"id\": 4, \"event\": 4, \"resource\": 4 },\n            { \"id\": 5, \"event\": 5, \"resource\": 1 },\n            { \"id\": 6, \"event\": 6, \"resource\": 2 },\n            { \"id\": 7, \"event\": 7, \"resource\": 3 },\n            { \"id\": 8, \"event\": 8, \"resource\": 4 },\n            { \"id\": 9, \"event\": 9, \"resource\": 5 },\n            { \"id\": 10, \"event\": 10, \"resource\": 6 },\n            { \"id\": 11, \"event\": 11, \"resource\": 7 },\n            { \"id\": 12, \"event\": 12, \"resource\": 8 },\n            { \"id\": 13, \"event\": 13, \"resource\": 5 },\n            { \"id\": 14, \"event\": 14, \"resource\": 6 },\n            { \"id\": 15, \"event\": 15, \"resource\": 9 },\n            { \"id\": 16, \"event\": 16, \"resource\": 10 },\n            { \"id\": 17, \"event\": 17, \"resource\": 11 },\n            { \"id\": 18, \"event\": 18, \"resource\": 12 },\n            { \"id\": 19, \"event\": 19, \"resource\": 9 },\n            { \"id\": 20, \"event\": 20, \"resource\": 10 },\n            { \"id\": 21, \"event\": 21, \"resource\": 7 },\n            { \"id\": 22, \"event\": 22, \"resource\": 8 },\n            { \"id\": 23, \"event\": 23, \"resource\": 11 },\n            { \"id\": 24, \"event\": 24, \"resource\": 12 }\n        ]\n    }\n}\n```\n\nThe Bryntum Scheduler Pro’s [project model](https://bryntum.com/products/schedulerpro/docs/api/SchedulerPro/model/ProjectModel) reads these three stores from the same response. Assignments connect each event to a resource using their IDs.\n\n## Creating a custom event model\n\nThe data includes a custom `priority`\n\nvalue. We’ll add it to the Bryntum task model by defining a custom [event model](https://bryntum.com/products/schedulerpro/docs/api/SchedulerPro/model/EventModel). Create a `TaskModel.ts`\n\nfile in `src/lib`\n\n:\n\n```\nmkdir src/lib && touch src/lib/TaskModel.ts\n```\n\nAdd the following lines of code to it:\n\n``` js\nimport { EventModel } from '@bryntum/schedulerpro';\n\nexport const taskPriorities = ['low', 'medium', 'high'] as const;\nexport type TaskPriority = (typeof taskPriorities)[number];\n\nexport default class TaskModel extends EventModel {\n    declare priority: TaskPriority;\n\n    static get fields() {\n        return [\n            { name: 'priority', defaultValue: 'medium' },\n        ];\n    }\n}\n\nexport type TaskRecord = InstanceType<typeof TaskModel>;\n```\n\nThe `TaskModel`\n\nclass extends Scheduler Pro’s `EventModel`\n\n, and adds the custom priority field.\n\n## Configuring the Bryntum Scheduler Pro and task editor\n\nCreate an `AppConfig.ts`\n\nfile in the `src`\n\nfolder and add the following project configuration to it:\n\n``` python\nimport type {\n    BryntumSchedulerProProjectModelProps,\n    BryntumSchedulerProProps,\n} from '@bryntum/schedulerpro-react';\nimport TaskModel, { taskPriorities } from './lib/TaskModel';\n\nexport const projectProps: BryntumSchedulerProProjectModelProps = {\n    eventModelClass: TaskModel,\n    autoLoad: true,\n    transport: {\n        load: {\n            url: 'data.json',\n        },\n    },\n};\n```\n\nThe project config registers the custom task model and loads the data from the local JSON file.\n\nAdd the following Scheduler Pro configuration below `projectProps`\n\n:\n\n``` js\nexport const schedulerProProps: BryntumSchedulerProProps = {\n    startDate: new Date(2026, 9, 5),\n    viewPreset: 'weekAndDay',\n    rowHeight: 50,\n    barMargin: 10,\n    columns: [\n        { type: 'resourceInfo', text: 'Name', field: 'name', width: 220 },\n    ],\n    taskEditFeature: {\n        items: {\n            generalTab: {\n                items: {\n                    percentDoneField: false,\n                    effortField: {\n                        cls: 'b-half-width',\n                    },\n                    nameField: {\n                        required: true,\n                        showRequiredIndicator: true,\n                        minLength: 5,\n                    },\n                    durationField: {\n                        min: '1d',\n                        max: '60d',\n                    },\n                    startDateField: {\n                        required: true,\n                        showRequiredIndicator: true,\n                    },\n                    priorityField: {\n                        type: 'combo',\n                        label: 'Priority',\n                        name: 'priority',\n                        weight: 630,\n                        editable: false,\n                        items: [...taskPriorities],\n                        required: true,\n                        showRequiredIndicator: true,\n                    },\n                },\n            },\n        },\n    },\n};\n```\n\nThe Bryntum Scheduler Pro React wrapper exposes feature configurations as individual props such as `taskEditFeature`\n\n, which configures the [ taskEdit feature](https://bryntum.com/products/schedulerpro/docs/api/SchedulerPro/feature/TaskEdit). You can modify the input items in the editor tabs. Setting the\n\n`percentDoneField`\n\nto `false`\n\nremoves that field, while the `priorityField`\n\nconfig adds a [Combo (dropdown) widget](https://bryntum.com/products/schedulerpro/docs/api/Core/widget/Combo)whose\n\n`name`\n\nmatches the custom model field.The input fields use Bryntum’s built-in validation. The task name is required and must contain at least five characters, the start date is required, and the duration must be between 1 and 60 days. The `DurationField`\n\n`min`\n\nand `max`\n\nconfigs expect duration strings such as `'1d'`\n\nand `'60d'`\n\n.\n\n## Rendering and validating the built-in task editor\n\nReplace the code in `src/App.tsx`\n\nwith the following:\n\n``` js\nimport { useRef, useState } from 'react';\nimport {\n    BryntumSchedulerPro,\n    BryntumSchedulerProProjectModel\n} from '@bryntum/schedulerpro-react';\nimport { projectProps, schedulerProProps } from './AppConfig';\n\nexport default function App() {\n    const projectRef = useRef<BryntumSchedulerProProjectModel>(null);\n    const [project] = useState(projectProps);\n    const [schedulerPro] = useState(schedulerProProps);\n\n    return (\n        <>\n            <BryntumSchedulerProProjectModel ref={projectRef} {...project} />\n            <BryntumSchedulerPro\n                project={projectRef}\n                {...schedulerPro}\n            />\n        </>\n    );\n}\n```\n\nThe project component loads the data, and the Scheduler Pro component receives the project via the React ref. Holding the configuration objects in `useState`\n\nkeeps their identities stable across React renders.\n\n## Running the app and testing the Bryntum Scheduler Pro task editor’s validation\n\nRun the application:\n\n```\nnpm run dev\n```\n\nOpen the app in your browser and open the built-in task editor by double-clicking a task. The task editor’s validation prevents saving when a field’s input is invalid and displays the field’s error in the editor, which you can see by trying to save with an empty task name:\n\nFor this app, the built-in editor is already a complete solution. It owns the edit lifecycle, validates its fields, and updates the project stores. The next section deliberately replaces it for applications that need React Hook Form to own that lifecycle, showing the flexibility of Scheduler Pro.\n\n## Replacing the task editor with a custom task editor that uses React Hook Form\n\nReact Hook Form registers native inputs and collects their values using the [ handleSubmit](https://react-hook-form.com/docs/useform/handlesubmit) function. Create a\n\n`TaskFormDialog.tsx`\n\nfile in the `src`\n\nfolder with the imports and types:\n\n``` js\nimport { Controller, useForm } from 'react-hook-form';\nimport { BryntumDateField } from '@bryntum/schedulerpro-react';\nimport {\n    taskPriorities,\n    type TaskPriority,\n    type TaskRecord,\n} from './lib/TaskModel';\n\nexport interface TaskFormValues {\n    name: string;\n    startDate: Date | null;\n    duration: number;\n    priority: TaskPriority;\n}\n\ninterface TaskFormDialogProps {\n    task: TaskRecord | null;\n    onClose: (task: TaskRecord) => void;\n    onSave: (task: TaskRecord, values: TaskFormValues) => void;\n}\n```\n\nThe `TaskFormValues`\n\ninterface describes the values React Hook Form collects, while `TaskFormDialogProps`\n\ndefines the selected task and the callbacks.\n\nAdd the following dialog component below the types:\n\n```\nexport default function TaskFormDialog({ task, ...props }: TaskFormDialogProps) {\n    if (!task) return null;\n\n    return <TaskForm task={task} {...props} />;\n}\n\nfunction TaskForm({ task, onClose, onSave }: TaskFormDialogProps & {\n    task: TaskRecord;\n}) {\n    const {\n        register,\n        handleSubmit,\n        control,\n        formState: { errors },\n    } = useForm<TaskFormValues>({\n        defaultValues: {\n            name: task.name ?? '',\n            startDate: (task.startDate as Date | null) ?? null,\n            duration: task.duration ?? 1,\n            priority: task.priority ?? 'medium',\n        },\n    });\n\n    return (\n        <div\n            className=\"task-form-overlay\"\n            onKeyDown={({ key }) => key === 'Escape' && onClose(task)}\n            role=\"presentation\"\n        >\n            <div\n                aria-labelledby=\"task-form-title\"\n                aria-modal=\"true\"\n                className=\"task-form-dialog\"\n                role=\"dialog\"\n            >\n                <h2 id=\"task-form-title\">Edit task (React Hook Form)</h2>\n\n                <form noValidate onSubmit={handleSubmit((values) => onSave(task, values))}>\n                    {/* Add the field snippets below here, in order. */}\n                </form>\n            </div>\n        </div>\n    );\n}\n```\n\nThe [ useForm](https://react-hook-form.com/docs/useform) hook is used for form initialization.\n\nThe\n\n`TaskFormDialog`\n\ncomponent renders nothing until a task is selected. Once mounted, `TaskForm`\n\ngives React Hook Form the task’s current values and sends validated submissions to the `onSave`\n\ncallback supplied by `App`\n\n, where this component will be rendered.Add the following native name input in place of the comment inside the `<form>`\n\n:\n\n```\n<label>\n    Name\n    <input\n        aria-describedby={errors.name ? 'name-error' : undefined}\n        aria-invalid={Boolean(errors.name)}\n        autoFocus\n        {...register('name', {\n            required: 'Name is required',\n            minLength: {\n                value: 5,\n                message: 'Name must be at least 5 characters',\n            },\n        })}\n    />\n    {errors.name && (\n        <p className=\"field-error\" id=\"name-error\" role=\"alert\">{errors.name.message}</p>\n    )}\n</label>\n```\n\nThe name input is [registered](https://react-hook-form.com/docs/useform/register) directly with React Hook Form for validation, tracking, and submission. Its rules require a value of at least five characters, and `formState.errors`\n\nsupplies the message shown below it. The `aria-invalid`\n\nand `aria-describedby`\n\nattributes connect the input to its error message for accessibility.\n\nAdd the following start date field below the name input:\n\n```\n<label>\n    Start date\n    <Controller\n        name=\"startDate\"\n        control={control}\n        rules={{ required: 'Start date is required' }}\n        render={({ field }) => (\n            <BryntumDateField\n                ariaDescription={errors.startDate?.message}\n                ariaLabel=\"Start date\"\n                value={field.value ?? undefined}\n                onChange={({ value }) => field.onChange(value)}\n                onFocusOut={field.onBlur}\n            />\n        )}\n    />\n    {errors.startDate && (\n        <p className=\"field-error\" role=\"alert\">{errors.startDate.message}</p>\n    )}\n</label>\n```\n\nThe React Hook Form [ Controller](https://react-hook-form.com/docs/usecontroller/controller) is used to integrate with external controlled UI inputs such as MUI, React-Select, AntD, and Bryntum widgets like the Bryntum\n\n[DateField](https://bryntum.com/products/schedulerpro/docs/api/Core/widget/DateField)that do not expose the native input interface expected by\n\n`register()`\n\n. The controller maps the Bryntum widget’s value, change event, and focus-out event to React Hook Form values.Add the following duration input below the start date field:\n\n```\n<label>\n    Duration (days)\n    <input\n        aria-describedby={errors.duration ? 'duration-error' : undefined}\n        aria-invalid={Boolean(errors.duration)}\n        type=\"number\"\n        {...register('duration', {\n            required: 'Duration is required',\n            valueAsNumber: true,\n            min: { value: 1, message: 'Duration must be at least 1 day' },\n            max: { value: 60, message: 'Duration must be at most 60 days' },\n        })}\n    />\n    {errors.duration && (\n        <p className=\"field-error\" id=\"duration-error\" role=\"alert\">{errors.duration.message}</p>\n    )}\n</label>\n```\n\nThe `valueAsNumber`\n\noption converts the browser’s string input to a number before React Hook Form applies the 1 to 60 day range rules.\n\nFinish the `<form>`\n\nin `src/TaskFormDialog.tsx`\n\nby adding the priority select input and action buttons:\n\n```\n<label>\n    Priority\n    <select {...register('priority', { required: true })}>\n        {taskPriorities.map((priority) => (\n            <option key={priority} value={priority}>{priority}</option>\n        ))}\n    </select>\n</label>\n\n<div className=\"task-form-actions\">\n    <button type=\"submit\">Save</button>\n    <button type=\"button\" onClick={() => onClose(task)}>Cancel</button>\n</div>\n```\n\nThe priority select uses the same values as the `TaskModel`\n\n. The Save button runs `handleSubmit`\n\n, which validates every registered or controlled field before calling `onSave`\n\n; Cancel closes the dialog without submitting.\n\nAdd the dialog styles to `src/index.css`\n\n:\n\n```\n.task-form-overlay {\n    position: fixed;\n    inset: 0;\n    z-index: 1000;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: rgb(0 0 0 / 40%);\n}\n\n.task-form-dialog {\n    box-sizing: border-box;\n    width: 400px;\n    max-width: calc(100vw - 32px);\n    max-height: calc(100vh - 32px);\n    padding: 28px 32px;\n    overflow-y: auto;\n    color: #1f2937;\n    font-family: inherit;\n    background: #fff;\n    border-radius: 12px;\n    box-shadow: 0 16px 40px rgb(0 0 0 / 25%);\n}\n```\n\nThese rules place a modal overlay above Scheduler Pro and constrain the dialog width and height so it remains usable on smaller screens.\n\nAdd the following form field, validation-message, and action-button styles to `src/index.css`\n\n:\n\n```\n.task-form-dialog h2 {\n    margin: 0 0 20px;\n    font-size: 20px;\n}\n\n.task-form-dialog form {\n    display: flex;\n    flex-direction: column;\n    gap: 16px;\n}\n\n.task-form-dialog label {\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n    color: #4b5563;\n    font-size: 13px;\n    font-weight: 600;\n}\n\n/* Direct children only — the BryntumDateField's inner input styles itself */\n.task-form-dialog label > input,\n.task-form-dialog label > select {\n    padding: 8px 10px;\n    color: #1f2937;\n    font-size: 14px;\n    font-weight: 400;\n    background: #fff;\n    border: 1px solid #c9ced6;\n    border-radius: 6px;\n}\n\n.task-form-dialog label > input:focus-visible,\n.task-form-dialog label > select:focus-visible {\n    outline: 2px solid var(--b-color-blue, #1e88e5);\n    outline-offset: -1px;\n}\n\n.field-error {\n    margin: 0;\n    color: #c0392b;\n    font-size: 12px;\n    font-weight: 400;\n}\n\n.task-form-actions {\n    display: flex;\n    gap: 10px;\n    margin-top: 12px;\n}\n\n.task-form-actions button {\n    padding: 9px 20px;\n    font-size: 14px;\n    cursor: pointer;\n    border: 1px solid transparent;\n    border-radius: 8px;\n    transition: background-color 0.15s;\n}\n\n.task-form-actions button[type='submit'] {\n    color: #fff;\n    background: var(--b-color-blue, #1e88e5);\n}\n\n.task-form-actions button[type='submit']:hover {\n    background: #1976d2;\n}\n\n.task-form-actions button[type='button'] {\n    color: #374151;\n    background: #fff;\n    border-color: #c9ced6;\n}\n\n.task-form-actions button[type='button']:hover {\n    background: #f3f4f6;\n}\n```\n\nReplace the code in `src/App.tsx`\n\nwith the following lines of code:\n\n``` js\nimport { useCallback, useRef, useState } from 'react';\nimport {\n    BryntumSchedulerPro,\n    BryntumSchedulerProProjectModel,\n    type BryntumSchedulerProProps\n} from '@bryntum/schedulerpro-react';\nimport { projectProps, schedulerProProps } from './AppConfig';\nimport TaskFormDialog, { type TaskFormValues } from './TaskFormDialog';\nimport type { TaskRecord } from './lib/TaskModel';\n\nexport default function App() {\n    const projectRef = useRef<BryntumSchedulerProProjectModel>(null);\n    const [editingTask, setEditingTask] = useState<TaskRecord | null>(null);\n    const [project] = useState(projectProps);\n    const [schedulerPro] = useState(schedulerProProps);\n\n    const [listeners] = useState<\n        NonNullable<BryntumSchedulerProProps['listeners']>\n    >(() => ({\n        beforeTaskEdit({ taskRecord }) {\n            setEditingTask(taskRecord as TaskRecord);\n            return false;\n        },\n    }));\n    const closeDialog = useCallback((task: TaskRecord) => {\n        if (task.isCreating) {\n            task.eventStore?.remove(task);\n        }\n        setEditingTask(null);\n    }, []);\n\n    const saveTask = useCallback((task: TaskRecord, values: TaskFormValues) => {\n        task.beginBatch();\n        try {\n            task.name = values.name;\n            if (values.startDate) {\n                task.setStartDate(values.startDate, false);\n            }\n            task.duration = values.duration;\n            task.priority = values.priority;\n        }\n        finally {\n            task.endBatch();\n        }\n\n        task.isCreating = false;\n        setEditingTask(null);\n    }, []);\n    return (\n        <>\n            <BryntumSchedulerProProjectModel ref={projectRef} {...project} />\n            <BryntumSchedulerPro\n                project={projectRef}\n                listeners={listeners}\n                {...schedulerPro}\n            />\n            <TaskFormDialog\n                key={editingTask?.id ?? 'none'}\n                task={editingTask}\n                onClose={closeDialog}\n                onSave={saveTask}\n            />\n        </>\n    );\n}\n```\n\nThe [ beforeTaskEdit](https://bryntum.com/products/schedulerpro/docs/api/SchedulerPro/feature/TaskEdit#event-beforeTaskEdit) listener is used to store the selected record in React state and returns\n\n`false`\n\n, preventing Bryntum’s built-in task editor popup from opening.Cancelling a task edit removes a temporary drag-created record, while saving batches the form values into the Bryntum model before clearing its [ isCreating](https://bryntum.com/products/schedulerpro/docs/api/Core/data/Model#property-isCreating) flag.\n\nThe custom task editor now owns the Bryntum Scheduler Pro record lifecycle that the built-in editor previously handled. Cancelling an existing task leaves it unchanged because the React Hook Form values are only written to the record on save.\n\n## Moving the validation rules to Zod\n\nInline React Hook Form rules work well for a small form. A schema becomes useful when the application shares validation between forms or when you need more complex validation.\n\nRun the following command in your terminal to install Zod and the React Hook Form resolvers package:\n\n```\nnpm install zod @hookform/resolvers\n```\n\nZod defines the schema, while `@hookform/resolvers`\n\nlets React Hook Form run that schema during `handleSubmit`\n\nand expose its issues through [ formState](https://react-hook-form.com/docs/useform/formstate) errors.\n\nCreate `taskFormSchema.ts`\n\nin the `src`\n\nfolder and add the following Zod schema to it:\n\n``` js\nimport { z } from 'zod';\nimport { taskPriorities } from './lib/TaskModel';\n\nexport const taskFormSchema = z.object({\n    name: z.string().min(5, 'Name must be at least 5 characters'),\n    startDate: z\n        .date({ message: 'Start date is required' })\n        .nullable()\n        .refine((date) => date !== null, 'Start date is required'),\n    duration: z\n        .number()\n        .min(1, 'Duration must be at least 1 day')\n        .max(60, 'Duration must be at most 60 days'),\n    priority: z.enum(taskPriorities),\n});\n\nexport type TaskFormInput = z.input<typeof taskFormSchema>;\nexport type TaskFormValues = z.output<typeof taskFormSchema>;\n```\n\nThe form input type permits `null`\n\nwhile the start date is empty. After the refinement succeeds, the schema’s output type narrows it to `Date`\n\n.\n\nIn `src/TaskFormDialog.tsx`\n\n, replace the local `TaskFormValues`\n\ninterface with these imports:\n\n``` js\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport {\n    taskFormSchema,\n    type TaskFormInput,\n    type TaskFormValues,\n} from './taskFormSchema';\n```\n\nThese imports replace the form’s handwritten value interface with input and validated output types inferred from the schema.\n\nIn the same file, remove the `TaskPriority`\n\ntype import so that only one type is imported from `./lib/TaskModel`\n\n:\n\n``` js\nimport { taskPriorities, type TaskRecord } from './lib/TaskModel';\n```\n\nIn the `TaskForm`\n\nfunction, replace the existing `useForm`\n\ncall with the following:\n\n```\nconst {\n    register,\n    handleSubmit,\n    control,\n    formState: { errors },\n} = useForm<TaskFormInput, unknown, TaskFormValues>({\n    resolver: zodResolver(taskFormSchema),\n    defaultValues: {\n        name: task.name ?? '',\n        startDate: (task.startDate as Date | null) ?? null,\n        duration: task.duration ?? 1,\n        priority: task.priority ?? 'medium',\n    },\n});\n```\n\nThe `zodResolver`\n\nmakes the schema the form’s validation source.\n\nUpdate the dialog heading:\n\n```\n<h2 id=\"task-form-title\">Edit task (React Hook Form + Zod)</h2>\n```\n\nNext, replace the four labeled fields inside the form with the following versions. The label wrappers, error messages, and the Save and Cancel buttons stay as they are; only the inline validation rules are removed:\n\n```\n<label>\n    Name\n    <input\n        aria-describedby={errors.name ? 'name-error' : undefined}\n        aria-invalid={Boolean(errors.name)}\n        autoFocus\n        {...register('name')}\n    />\n    {errors.name && (\n        <p className=\"field-error\" id=\"name-error\" role=\"alert\">{errors.name.message}</p>\n    )}\n</label>\n\n<label>\n    Start date\n    <Controller\n        name=\"startDate\"\n        control={control}\n        render={({ field }) => (\n            <BryntumDateField\n                ariaDescription={errors.startDate?.message}\n                ariaLabel=\"Start date\"\n                value={field.value ?? undefined}\n                onChange={({ value }) => field.onChange(value)}\n                onFocusOut={field.onBlur}\n            />\n        )}\n    />\n    {errors.startDate && (\n        <p className=\"field-error\" role=\"alert\">{errors.startDate.message}</p>\n    )}\n</label>\n\n<label>\n    Duration (days)\n    <input\n        aria-describedby={errors.duration ? 'duration-error' : undefined}\n        aria-invalid={Boolean(errors.duration)}\n        type=\"number\"\n        {...register('duration', { valueAsNumber: true })}\n    />\n    {errors.duration && (\n        <p className=\"field-error\" id=\"duration-error\" role=\"alert\">{errors.duration.message}</p>\n    )}\n</label>\n\n<label>\n    Priority\n    <select {...register('priority')}>\n        {taskPriorities.map((priority) => (\n            <option key={priority} value={priority}>{priority}</option>\n        ))}\n    </select>\n</label>\n```\n\nOnly `valueAsNumber`\n\nremains because it’s input transformation rather than validation: it converts the duration string before Zod receives it.\n\nFinally, remove the TaskFormValues type import in `src/App.tsx`\n\nand add the schema-derived type import:\n\n``` python\nimport TaskFormDialog from './TaskFormDialog';\nimport type { TaskFormValues } from './taskFormSchema';\n```\n\nBecause the schema’s output type narrows `startDate`\n\nto a `Date`\n\n, the `if (values.startDate)`\n\nguard in `saveTask`\n\nis no longer needed. Replace it with the direct call:\n\n```\ntask.setStartDate(values.startDate, false);\n```\n\nApart from its heading, the form does not change: React Hook Form still registers inputs, controls `BryntumDateField`\n\n, stores errors, and runs `handleSubmit`\n\n. On submission, `zodResolver`\n\npasses the collected values through `taskFormSchema`\n\nand converts any Zod issues into `formState.errors`\n\n; only the location of the validation rules changed:\n\n## Building this demo with the Bryntum MCP server and skills\n\nWe built and verified this demo with Bryntum’s AI tooling. The [Bryntum MCP server](https://bryntum.com/products/schedulerpro/docs/guide/SchedulerPro/ai-features/mcp-server) provides version-specific documentation to coding agents. It helped confirm the Scheduler Pro `taskEdit`\n\nitem refs, the duration string validation, the React wrapper’s `taskEditFeature`\n\nprop, and the supported `beforeTaskEdit`\n\nreplacement pattern.\n\nRun the following command in your terminal to add the MCP to Claude Code:\n\n```\nclaude mcp add --transport http bryntum https://mcp.bryntum.com\n```\n\nThe [Bryntum AI Agent skills](https://bryntum.com/products/schedulerpro/docs/guide/SchedulerPro/ai-features/skills) complement the documentation search with practical knowledge for using Bryntum.\n\n## Next steps\n\nWe learned how to customize the Bryntum Scheduler Pro task editor and replace it with a custom one using a React Hook Form dialog as well as Zod. This lets you integrate Bryntum Scheduler Pro seamlessly into your app.\n\nFrom here, you can add more fields to `TaskModel`\n\nand both editors, connect your Scheduler Pro to a backend, and add more complex validation. For another example of form-driven scheduling in React, take a look at our [React Admin x Bryntum: Creating a scheduler](https://bryntum.com/blog/react-admin-x-bryntum-creating-a-scheduler/) blog post, which uses React Admin to create a custom event form where the form inputs are built using MUI components and React Hook Form for validation.\n\n## Build it with Bryntum Scheduler Pro\n\nStart a free trial, explore live demos, or read the docs.", "url": "https://wpnews.pro/news/react-hook-form-and-zod-validation-in-bryntum-scheduler-pro", "canonical_source": "https://bryntum.com/blog/react-hook-form-and-zod-validation-in-bryntum-scheduler-pro/", "published_at": "2026-08-05 11:14:45+00:00", "updated_at": "2026-08-13 12:08:41.151724+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Bryntum", "Bryntum Scheduler Pro", "React Hook Form", "Zod", "Vite", "React", "TypeScript", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/react-hook-form-and-zod-validation-in-bryntum-scheduler-pro", "markdown": "https://wpnews.pro/news/react-hook-form-and-zod-validation-in-bryntum-scheduler-pro.md", "text": "https://wpnews.pro/news/react-hook-form-and-zod-validation-in-bryntum-scheduler-pro.txt", "jsonld": "https://wpnews.pro/news/react-hook-form-and-zod-validation-in-bryntum-scheduler-pro.jsonld"}}