From 286050a1c8cc879b8ba8cb3f98aab7cde48e7a4c Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Tue, 27 Oct 2026 22:36:28 +0600 Subject: [PATCH 1/9] feat(build): add react compiler support --- package.json | 1 + pnpm-lock.yaml | 12 ++++++++++++ rspack.config.mjs | 3 +++ 3 files changed, 16 insertions(+) diff --git a/package.json b/package.json index f3a2feb6a3..ad512e3800 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "polished": "^4.2.2", "querystring": "^0.2.1", "react": "^18.3.1", + "react-compiler-runtime": "^1.0.0", "react-datepicker": "4.8.0", "react-day-picker": "^9.6.7", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 302ddf7543..12669e64c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: react: specifier: ^18.3.1 version: 18.3.1 + react-compiler-runtime: + specifier: ^1.0.0 + version: 1.0.0(react@18.3.1) react-datepicker: specifier: 4.8.0 version: 4.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -6224,6 +6227,11 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + react-compiler-runtime@1.0.0: + resolution: {integrity: sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental + react-datepicker@4.8.0: resolution: {integrity: sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==} peerDependencies: @@ -14104,6 +14112,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-compiler-runtime@1.0.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-datepicker@4.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@popperjs/core': 2.11.8 diff --git a/rspack.config.mjs b/rspack.config.mjs index e25e533027..56331a6529 100644 --- a/rspack.config.mjs +++ b/rspack.config.mjs @@ -38,6 +38,9 @@ const createSwcLoaderOptions = (isDevelopment) => ({ refresh: false, importSource: '@emotion/react', }, + reactCompiler: { + target: '18', + }, }, experimental: { plugins: [ From 6ddfa93d93e5b7f77fa5c283ae08e15ef5d1c9aa Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Tue, 27 Oct 2026 22:36:39 +0600 Subject: [PATCH 2/9] fix(react-compiler): resolve form state memoization across course builder and coupon details --- .../components/coupon/CouponDiscount.tsx | 16 ++++++------- .../components/coupon/CouponInfo.tsx | 4 ++-- .../components/coupon/CouponLimitation.tsx | 6 ++--- .../components/coupon/CouponPreview.tsx | 24 +++++++++---------- .../components/coupon/CouponValidity.tsx | 8 +++---- .../components/layout/Topbar.tsx | 7 +++--- .../CourseListModal/CategoryListTable.tsx | 4 ++-- .../CourseListModal/CourseListTable.tsx | 4 ++-- .../MembershipPlanListTable.tsx | 5 ++-- .../modals/CourseListModal/SearchField.tsx | 5 ++-- .../components/layouts/Tracker.tsx | 4 ++-- .../components/layouts/header/Header.tsx | 2 +- .../components/modals/AssignmentModal.tsx | 6 ++--- .../components/modals/LessonModal.tsx | 7 +++--- .../components/modals/QuizModal.tsx | 2 +- .../v3/shared/hooks/useFormWithGlobalError.ts | 6 +++-- 16 files changed, 58 insertions(+), 52 deletions(-) diff --git a/assets/src/js/v3/entries/coupon-details/components/coupon/CouponDiscount.tsx b/assets/src/js/v3/entries/coupon-details/components/coupon/CouponDiscount.tsx index f6f724d8a0..c4344549b4 100644 --- a/assets/src/js/v3/entries/coupon-details/components/coupon/CouponDiscount.tsx +++ b/assets/src/js/v3/entries/coupon-details/components/coupon/CouponDiscount.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -62,12 +62,12 @@ function CouponDiscount() { const { tutor_currency } = tutorConfig; const { showModal } = useModal(); - const appliesTo = form.watch('applies_to'); - const discountType = form.watch('discount_type'); - const courses = form.watch('courses') ?? []; - const bundles = form.watch('bundles') ?? []; - const categories = form.watch('categories') ?? []; - const membershipPlans = form.watch('membershipPlans') ?? []; + const appliesTo = useWatch({ control: form.control, name: 'applies_to' }); + const discountType = useWatch({ control: form.control, name: 'discount_type' }); + const courses = useWatch({ control: form.control, name: 'courses' }) ?? []; + const bundles = useWatch({ control: form.control, name: 'bundles' }) ?? []; + const categories = useWatch({ control: form.control, name: 'categories' }) ?? []; + const membershipPlans = useWatch({ control: form.control, name: 'membershipPlans' }) ?? []; const modalType = { specific_courses: 'courses', @@ -210,7 +210,7 @@ function CouponDiscount() { {appliesTo === 'specific_membership_plans' && membershipPlans.length > 0 && (
- {form.watch('membershipPlans')?.map((item) => ( + {membershipPlans?.map((item) => ( (); - const couponType = form.watch('coupon_type'); + const couponType = useWatch({ control: form.control, name: 'coupon_type' }); function handleGenerateCouponCode() { const newCouponCode = generateCouponCode(); diff --git a/assets/src/js/v3/entries/coupon-details/components/coupon/CouponLimitation.tsx b/assets/src/js/v3/entries/coupon-details/components/coupon/CouponLimitation.tsx index 5ec271c994..51429dc0c8 100644 --- a/assets/src/js/v3/entries/coupon-details/components/coupon/CouponLimitation.tsx +++ b/assets/src/js/v3/entries/coupon-details/components/coupon/CouponLimitation.tsx @@ -1,4 +1,4 @@ -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -16,8 +16,8 @@ import type { Coupon } from '@CouponDetails/services/coupon'; function CouponLimitation() { const form = useFormContext(); - const usage_limit_status = form.watch('usage_limit_status'); - const per_user_limit_status = form.watch('per_user_limit_status'); + const usage_limit_status = useWatch({ control: form.control, name: 'usage_limit_status' }); + const per_user_limit_status = useWatch({ control: form.control, name: 'per_user_limit_status' }); return ( diff --git a/assets/src/js/v3/entries/coupon-details/components/coupon/CouponPreview.tsx b/assets/src/js/v3/entries/coupon-details/components/coupon/CouponPreview.tsx index f52a52d6de..11dc409cc6 100644 --- a/assets/src/js/v3/entries/coupon-details/components/coupon/CouponPreview.tsx +++ b/assets/src/js/v3/entries/coupon-details/components/coupon/CouponPreview.tsx @@ -1,4 +1,4 @@ -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; import { format, isToday, isTomorrow, isValid } from 'date-fns'; @@ -48,17 +48,17 @@ const discountTypeText = (appliesTo: CouponAppliesTo, discountText: string) => { function CouponPreview() { const form = useFormContext(); - const couponTitle = form.watch('coupon_title'); - const couponType = form.watch('coupon_type'); - const couponCode = form.watch('coupon_code'); - const discountType = form.watch('discount_type'); - const discountAmount = form.watch('discount_amount'); - const startDate = form.watch('start_date'); - const startTime = form.watch('start_time'); - const endDate = form.watch('end_date'); - const appliesTo = form.watch('applies_to'); - const perUserUsageLimit = form.watch('per_user_usage_limit'); - const couponUsedCount = form.watch('coupon_uses'); + const couponTitle = useWatch({ control: form.control, name: 'coupon_title' }); + const couponType = useWatch({ control: form.control, name: 'coupon_type' }); + const couponCode = useWatch({ control: form.control, name: 'coupon_code' }); + const discountType = useWatch({ control: form.control, name: 'discount_type' }); + const discountAmount = useWatch({ control: form.control, name: 'discount_amount' }); + const startDate = useWatch({ control: form.control, name: 'start_date' }); + const startTime = useWatch({ control: form.control, name: 'start_time' }); + const endDate = useWatch({ control: form.control, name: 'end_date' }); + const appliesTo = useWatch({ control: form.control, name: 'applies_to' }); + const perUserUsageLimit = useWatch({ control: form.control, name: 'per_user_usage_limit' }); + const couponUsedCount = useWatch({ control: form.control, name: 'coupon_uses' }); const startDateTime = startDate && startTime ? `${startDate} ${startTime}` : ''; const activeFromSuffix = isValid(new Date(startDateTime)) diff --git a/assets/src/js/v3/entries/coupon-details/components/coupon/CouponValidity.tsx b/assets/src/js/v3/entries/coupon-details/components/coupon/CouponValidity.tsx index 220d59bcba..4bd49d165c 100644 --- a/assets/src/js/v3/entries/coupon-details/components/coupon/CouponValidity.tsx +++ b/assets/src/js/v3/entries/coupon-details/components/coupon/CouponValidity.tsx @@ -1,4 +1,4 @@ -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -19,9 +19,9 @@ import type { Coupon } from '@CouponDetails/services/coupon'; function CouponValidity() { const form = useFormContext(); - const isEndEnabled = form.watch('is_end_enabled'); - const startDate = form.watch('start_date'); - const startTime = form.watch('start_time'); + const isEndEnabled = useWatch({ control: form.control, name: 'is_end_enabled' }); + const startDate = useWatch({ control: form.control, name: 'start_date' }); + const startTime = useWatch({ control: form.control, name: 'start_time' }); const hasStartDateTime = !!startDate && !!startTime; return ( diff --git a/assets/src/js/v3/entries/coupon-details/components/layout/Topbar.tsx b/assets/src/js/v3/entries/coupon-details/components/layout/Topbar.tsx index ad855b649e..084d46b385 100644 --- a/assets/src/js/v3/entries/coupon-details/components/layout/Topbar.tsx +++ b/assets/src/js/v3/entries/coupon-details/components/layout/Topbar.tsx @@ -1,4 +1,4 @@ -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -35,6 +35,7 @@ function Topbar() { const form = useFormContext(); const coupon = form.getValues(); + const couponStatus = useWatch({ control: form.control, name: 'coupon_status' }) ?? coupon.coupon_status ?? 'active'; const createCouponMutation = useCreateCouponMutation(); const updateCouponMutation = useUpdateCouponMutation(); @@ -66,8 +67,8 @@ function Topbar() {

{courseId ? __('Update Coupon', 'tutor') : __('Create Coupon', 'tutor')}

- - {makeFirstCharacterUpperCase(coupon.coupon_status)} + + {makeFirstCharacterUpperCase(couponStatus)}
{ - const selectedCategories = form.watch('categories') ?? []; + const selectedCategories = useWatch({ control: form.control, name: 'categories' }) ?? []; const { pageInfo, onPageChange, itemsPerPage, offset, onFilterItems } = usePaginatedTable(); const categoryListQuery = useAppliesToQuery({ applies_to: 'specific_category', diff --git a/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/CourseListTable.tsx b/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/CourseListTable.tsx index eff27100f9..4b1029d41c 100644 --- a/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/CourseListTable.tsx +++ b/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/CourseListTable.tsx @@ -1,4 +1,4 @@ -import type { UseFormReturn } from 'react-hook-form'; +import { type UseFormReturn, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -24,7 +24,7 @@ interface CourseListTableProps { } const CourseListTable = ({ type, form }: CourseListTableProps) => { - const selectedCourses = form.watch(type) || []; + const selectedCourses = useWatch({ control: form.control, name: type }) || []; const { pageInfo, onPageChange, itemsPerPage, offset, onFilterItems } = usePaginatedTable(); const courseListQuery = useAppliesToQuery({ applies_to: type === 'courses' ? 'specific_courses' : 'specific_bundles', diff --git a/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/MembershipPlanListTable.tsx b/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/MembershipPlanListTable.tsx index ec49e2ac67..a5f1631a07 100644 --- a/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/MembershipPlanListTable.tsx +++ b/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/MembershipPlanListTable.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo, useState } from 'react'; -import { type UseFormReturn } from 'react-hook-form'; +import { type UseFormReturn, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -27,7 +27,8 @@ interface MembershipPlanListTableProps { } const MembershipPlanListTable = ({ form }: MembershipPlanListTableProps) => { - const selectedPlans = useMemo(() => form.watch('membershipPlans') || [], [form]); + const watchedPlans = useWatch({ control: form.control, name: 'membershipPlans' }); + const selectedPlans = useMemo(() => watchedPlans || [], [watchedPlans]); const getMembershipPlanListQuery = useMembershipPlansQuery(); const [searchTerm, setSearchTerm] = useState(''); diff --git a/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/SearchField.tsx b/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/SearchField.tsx index 0f27eaf19b..8b09db80c0 100644 --- a/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/SearchField.tsx +++ b/assets/src/js/v3/entries/coupon-details/components/modals/CourseListModal/SearchField.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { __ } from '@wordpress/i18n'; import SVGIcon from '@TutorShared/atoms/SVGIcon'; @@ -20,7 +20,8 @@ interface SearchFieldProps { const SearchField = ({ onFilterItems }: SearchFieldProps) => { const actionsForm = useFormWithGlobalError({ defaultValues: { search: '' } }); - const searchValue = useDebounce(actionsForm.watch('search')); + const search = useWatch({ control: actionsForm.control, name: 'search' }) ?? ''; + const searchValue = useDebounce(search); useEffect(() => { onFilterItems({ diff --git a/assets/src/js/v3/entries/course-builder/components/layouts/Tracker.tsx b/assets/src/js/v3/entries/course-builder/components/layouts/Tracker.tsx index 4523319b8c..eeb7de8a01 100644 --- a/assets/src/js/v3/entries/course-builder/components/layouts/Tracker.tsx +++ b/assets/src/js/v3/entries/course-builder/components/layouts/Tracker.tsx @@ -1,4 +1,4 @@ -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { css } from '@emotion/react'; @@ -16,7 +16,7 @@ const Tracker = () => { const navigate = useNavigate(); const form = useFormContext(); - const postTitle = form.watch('post_title'); + const postTitle = useWatch({ control: form.control, name: 'post_title' }); const handleClick = async (step: Step) => { navigate(step.path); diff --git a/assets/src/js/v3/entries/course-builder/components/layouts/header/Header.tsx b/assets/src/js/v3/entries/course-builder/components/layouts/header/Header.tsx index 1fc2419244..34f73b8ddf 100644 --- a/assets/src/js/v3/entries/course-builder/components/layouts/header/Header.tsx +++ b/assets/src/js/v3/entries/course-builder/components/layouts/header/Header.tsx @@ -49,7 +49,7 @@ const Header = () => { const totalEnrolledStudents = (queryClient.getQueryData(['CourseDetails', courseId]) as CourseDetailsResponse) ?.total_enrolled_student; - const isFormDirty = form.formState.dirtyFields && Object.keys(form.formState.dirtyFields).length > 0; + const isFormDirty = form.formState.isDirty; const isTutorPro = !!tutorConfig.tutor_pro_url; const isOpenAiEnabled = tutorConfig.settings?.chatgpt_enable === 'on'; const hasOpenAiAPIKey = tutorConfig.settings?.chatgpt_key_exist; diff --git a/assets/src/js/v3/entries/course-builder/components/modals/AssignmentModal.tsx b/assets/src/js/v3/entries/course-builder/components/modals/AssignmentModal.tsx index 0e9c06998f..c1a3a55e3e 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/AssignmentModal.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/AssignmentModal.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; @@ -138,8 +138,8 @@ const AssignmentModal = ({ mode: 'onChange', }); - const isFormDirty = form.formState.dirtyFields && Object.keys(form.formState.dirtyFields).length > 0; - const isRetryAllowed = form.watch('is_retry_allowed'); + const isFormDirty = form.formState.isDirty; + const isRetryAllowed = useWatch({ control: form.control, name: 'is_retry_allowed' }); useEffect(() => { if (assignmentDetails) { diff --git a/assets/src/js/v3/entries/course-builder/components/modals/LessonModal.tsx b/assets/src/js/v3/entries/course-builder/components/modals/LessonModal.tsx index e1c408f91f..cb2644caf6 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/LessonModal.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/LessonModal.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { useQueryClient } from '@tanstack/react-query'; import { __, sprintf } from '@wordpress/i18n'; @@ -140,7 +140,8 @@ const LessonModal = ({ mode: 'onChange', }); - const isFormDirty = form.formState.dirtyFields && Object.keys(form.formState.dirtyFields).length > 0; + const isFormDirty = form.formState.isDirty; + const isLessonPreviewEnabled = useWatch({ control: form.control, name: 'lesson_preview' }); const hasRightSidebar = Boolean( contentDripType !== '' || @@ -597,7 +598,7 @@ const LessonModal = ({ /> )} /> - +
{ // prettier-ignore diff --git a/assets/src/js/v3/entries/course-builder/components/modals/QuizModal.tsx b/assets/src/js/v3/entries/course-builder/components/modals/QuizModal.tsx index e836d91374..639f0499a7 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/QuizModal.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/QuizModal.tsx @@ -127,7 +127,7 @@ const QuizModal = ({ shouldFocusError: true, }); - const isFormDirty = form.formState.dirtyFields && Object.keys(form.formState.dirtyFields).length > 0; + const isFormDirty = form.formState.isDirty; useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { diff --git a/assets/src/js/v3/shared/hooks/useFormWithGlobalError.ts b/assets/src/js/v3/shared/hooks/useFormWithGlobalError.ts index b0bb6e7141..24397fec46 100644 --- a/assets/src/js/v3/shared/hooks/useFormWithGlobalError.ts +++ b/assets/src/js/v3/shared/hooks/useFormWithGlobalError.ts @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { type FieldValues, useForm, type UseFormProps, type UseFormReturn } from 'react-hook-form'; +import { type FieldValues, useForm, type UseFormProps, type UseFormReturn, useFormState } from 'react-hook-form'; export type FormWithGlobalErrorType = UseFormReturn & { setSubmitError: (error: string | undefined) => void; @@ -11,5 +11,7 @@ export const useFormWithGlobalError = ( ): FormWithGlobalErrorType => { const [submitError, setSubmitError] = useState(); const form = useForm(formOptions); - return { ...form, submitError, setSubmitError }; + const formState = useFormState({ control: form.control }); + + return { ...form, formState, submitError, setSubmitError }; }; From 98f66555aff385de2b4e3728d020bb41b1583fd8 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Thu, 29 Oct 2026 18:21:50 +0600 Subject: [PATCH 3/9] fix(react-compiler): migrate form.watch to useWatch across modals, tables, and search fields --- .../entries/course-builder/components/layouts/Navigator.tsx | 4 ++-- .../modals/ContentBankContentSelectModal/SearchField.tsx | 5 +++-- assets/src/js/v3/entries/course-builder/pages/IconList.tsx | 5 +++-- .../components/modals/CollectionList/CollectionListTable.tsx | 4 +++- .../components/modals/CollectionList/SearchField.tsx | 5 +++-- .../modals/CourseCategorySelectModal/CategoryListTable.tsx | 4 ++-- .../modals/CourseCategorySelectModal/CourseListTable.tsx | 4 ++-- .../modals/CourseCategorySelectModal/SearchField.tsx | 5 +++-- .../components/modals/CourseListModal/CourseListTable.tsx | 4 +++- .../shared/components/modals/CourseListModal/SearchField.tsx | 5 +++-- .../js/v3/shared/components/modals/H5PContentListModal.tsx | 5 +++-- 11 files changed, 30 insertions(+), 20 deletions(-) diff --git a/assets/src/js/v3/entries/course-builder/components/layouts/Navigator.tsx b/assets/src/js/v3/entries/course-builder/components/layouts/Navigator.tsx index 87599078f3..fac031aac5 100644 --- a/assets/src/js/v3/entries/course-builder/components/layouts/Navigator.tsx +++ b/assets/src/js/v3/entries/course-builder/components/layouts/Navigator.tsx @@ -1,4 +1,4 @@ -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { css, type SerializedStyles } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -30,7 +30,7 @@ const Navigator = ({ styleModifier }: NavigatorProps) => { const nextIndex = Math.min(steps.length, currentIndex + 1); const previousStep = steps[previousIndex]; const nextStep = steps[nextIndex]; - const postTitle = form.watch('post_title'); + const postTitle = useWatch({ control: form.control, name: 'post_title' }); const handlePreviousClick = () => { setSteps((previous) => { diff --git a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/SearchField.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/SearchField.tsx index aa718f1f46..48098848d3 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/SearchField.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/SearchField.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -22,7 +22,8 @@ interface SearchFieldProps { const SearchField = ({ onFilterItems }: SearchFieldProps) => { const actionsForm = useFormWithGlobalError({ defaultValues: { search: '' } }); - const searchValue = useDebounce(actionsForm.watch('search')); + const search = useWatch({ control: actionsForm.control, name: 'search' }) ?? ''; + const searchValue = useDebounce(search); useEffect(() => { onFilterItems({ diff --git a/assets/src/js/v3/entries/course-builder/pages/IconList.tsx b/assets/src/js/v3/entries/course-builder/pages/IconList.tsx index 483e89cab7..62e1b645ba 100644 --- a/assets/src/js/v3/entries/course-builder/pages/IconList.tsx +++ b/assets/src/js/v3/entries/course-builder/pages/IconList.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import SVGIcon from '@TutorShared/atoms/SVGIcon'; @@ -16,7 +16,8 @@ import { icons } from '@TutorShared/icons/types'; const IconList = () => { const form = useFormWithGlobalError<{ search: string }>({ defaultValues: { search: '' } }); - const search = useDebounce(form.watch('search')); + const searchValue = useWatch({ control: form.control, name: 'search' }) ?? ''; + const search = useDebounce(searchValue); const filteredIcons = useMemo(() => { if (!search) { return icons; diff --git a/assets/src/js/v3/entries/import-export/components/modals/CollectionList/CollectionListTable.tsx b/assets/src/js/v3/entries/import-export/components/modals/CollectionList/CollectionListTable.tsx index 3e169c7d2f..cc1d8c22ec 100644 --- a/assets/src/js/v3/entries/import-export/components/modals/CollectionList/CollectionListTable.tsx +++ b/assets/src/js/v3/entries/import-export/components/modals/CollectionList/CollectionListTable.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useMemo } from 'react'; +import { useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, _n, sprintf } from '@wordpress/i18n'; @@ -26,7 +27,8 @@ interface CourseListTableProps { const CollectionListTable = ({ form }: CourseListTableProps) => { const { pageInfo, onPageChange, itemsPerPage, onFilterItems } = usePaginatedTable(); - const selectedItems = useMemo(() => form.watch('content_bank') || [], [form]); + const watchedItems = useWatch({ control: form.control, name: 'content_bank' }); + const selectedItems = useMemo(() => watchedItems || [], [watchedItems]); const selectedItemIds = useMemo(() => selectedItems.map((item) => String(item.ID)), [selectedItems]); const getCollectionListQuery = useGetCollectionsPaginatedQuery({ diff --git a/assets/src/js/v3/entries/import-export/components/modals/CollectionList/SearchField.tsx b/assets/src/js/v3/entries/import-export/components/modals/CollectionList/SearchField.tsx index d5a3afd7e0..e8060520f7 100644 --- a/assets/src/js/v3/entries/import-export/components/modals/CollectionList/SearchField.tsx +++ b/assets/src/js/v3/entries/import-export/components/modals/CollectionList/SearchField.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { __ } from '@wordpress/i18n'; import SVGIcon from '@TutorShared/atoms/SVGIcon'; @@ -21,7 +21,8 @@ interface SearchFieldProps { const SearchField = ({ onFilterItems, initialSearchValue }: SearchFieldProps) => { const actionsForm = useFormWithGlobalError({ defaultValues: { search: initialSearchValue || '' } }); - const searchValue = useDebounce(actionsForm.watch('search')); + const search = useWatch({ control: actionsForm.control, name: 'search' }) ?? ''; + const searchValue = useDebounce(search); useEffect(() => { onFilterItems({ diff --git a/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx b/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx index 76225ac4d3..a369efd69e 100644 --- a/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx +++ b/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx @@ -1,4 +1,4 @@ -import type { UseFormReturn } from 'react-hook-form'; +import { type UseFormReturn, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -22,7 +22,7 @@ interface CategoryListTableProps { } const CategoryListTable = ({ form }: CategoryListTableProps) => { - const selectedCategories: Category[] = form.watch('categories') ?? []; + const selectedCategories: Category[] = useWatch({ control: form.control, name: 'categories' }) ?? []; const { pageInfo, onPageChange, itemsPerPage, offset, onFilterItems } = usePaginatedTable(); const categoryListQuery = useCourseCategoryQuery({ applies_to: 'specific_category', diff --git a/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx b/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx index 23c6b789b3..dd4f536a5e 100644 --- a/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx +++ b/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx @@ -1,4 +1,4 @@ -import type { UseFormReturn } from 'react-hook-form'; +import { type UseFormReturn, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -23,7 +23,7 @@ interface CourseListTableProps { } const CourseListTable = ({ type, form }: CourseListTableProps) => { - const selectedCourses: Course[] = form.watch(type) || []; + const selectedCourses: Course[] = useWatch({ control: form.control, name: type }) || []; const { pageInfo, onPageChange, itemsPerPage, offset, onFilterItems } = usePaginatedTable(); const courseListQuery = useCourseCategoryQuery({ applies_to: type === 'courses' ? 'specific_courses' : 'specific_bundles', diff --git a/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/SearchField.tsx b/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/SearchField.tsx index a17c7802e4..1d46d34905 100644 --- a/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/SearchField.tsx +++ b/assets/src/js/v3/shared/components/modals/CourseCategorySelectModal/SearchField.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { __ } from '@wordpress/i18n'; import SVGIcon from '@TutorShared/atoms/SVGIcon'; @@ -20,7 +20,8 @@ interface SearchFieldProps { const SearchField = ({ onFilterItems }: SearchFieldProps) => { const actionsForm = useFormWithGlobalError({ defaultValues: { search: '' } }); - const searchValue = useDebounce(actionsForm.watch('search')); + const search = useWatch({ control: actionsForm.control, name: 'search' }) ?? ''; + const searchValue = useDebounce(search); useEffect(() => { onFilterItems({ diff --git a/assets/src/js/v3/shared/components/modals/CourseListModal/CourseListTable.tsx b/assets/src/js/v3/shared/components/modals/CourseListModal/CourseListTable.tsx index 477b246263..ef0ae7c539 100644 --- a/assets/src/js/v3/shared/components/modals/CourseListModal/CourseListTable.tsx +++ b/assets/src/js/v3/shared/components/modals/CourseListModal/CourseListTable.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useMemo } from 'react'; +import { useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -33,7 +34,8 @@ interface CourseListTableProps { const CourseListTable = ({ form, type = 'courses' }: CourseListTableProps) => { const { pageInfo, onPageChange, itemsPerPage, offset, onFilterItems } = usePaginatedTable(); - const selectedItems = useMemo(() => form.watch(type) || [], [form, type]); + const watchedItems = useWatch({ control: form.control, name: type }); + const selectedItems = useMemo(() => watchedItems || [], [watchedItems]); const selectedItemIds = useMemo(() => selectedItems.map((course) => course.id), [selectedItems]); const queryParams = useMemo( () => ({ diff --git a/assets/src/js/v3/shared/components/modals/CourseListModal/SearchField.tsx b/assets/src/js/v3/shared/components/modals/CourseListModal/SearchField.tsx index a17c7802e4..1d46d34905 100644 --- a/assets/src/js/v3/shared/components/modals/CourseListModal/SearchField.tsx +++ b/assets/src/js/v3/shared/components/modals/CourseListModal/SearchField.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { __ } from '@wordpress/i18n'; import SVGIcon from '@TutorShared/atoms/SVGIcon'; @@ -20,7 +20,8 @@ interface SearchFieldProps { const SearchField = ({ onFilterItems }: SearchFieldProps) => { const actionsForm = useFormWithGlobalError({ defaultValues: { search: '' } }); - const searchValue = useDebounce(actionsForm.watch('search')); + const search = useWatch({ control: actionsForm.control, name: 'search' }) ?? ''; + const searchValue = useDebounce(search); useEffect(() => { onFilterItems({ diff --git a/assets/src/js/v3/shared/components/modals/H5PContentListModal.tsx b/assets/src/js/v3/shared/components/modals/H5PContentListModal.tsx index f0ed2d179a..29baf3c729 100644 --- a/assets/src/js/v3/shared/components/modals/H5PContentListModal.tsx +++ b/assets/src/js/v3/shared/components/modals/H5PContentListModal.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; import { format } from 'date-fns'; @@ -45,7 +45,8 @@ const H5PContentListModal = ({ search: '', }, }); - const search = useDebounce(form.watch('search'), 300); + const searchValue = useWatch({ control: form.control, name: 'search' }) ?? ''; + const search = useDebounce(searchValue, 300); const getH5PQuizzesQuery = useGetH5PQuizContentsQuery(search, contentType); const getH5PContentsQuery = useGetH5PLessonContentsQuery(search, contentType); const [selectedContents, setSelectedContents] = useState([]); From 52579de4e1895722a789cf66e93d788894a4368f Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Thu, 29 Oct 2026 18:31:22 +0600 Subject: [PATCH 4/9] fix(course-builder): make content bank items selectable with useWatch reactivity --- .../ContentListTable.tsx | 13 +++++++------ .../QuestionListTable.tsx | 11 ++++++----- .../modals/ContentBankContentSelectModal/index.tsx | 9 +++++---- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/ContentListTable.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/ContentListTable.tsx index 0bf15b8272..69c586f7c9 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/ContentListTable.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/ContentListTable.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, _n, sprintf } from '@wordpress/i18n'; @@ -30,8 +30,8 @@ type SortDirection = 'asc' | 'desc'; const ContentListTable = () => { const { pageInfo, onPageChange, itemsPerPage, onFilterItems } = usePaginatedTable(); const form = useFormContext(); - const selectedCollection = form.watch('selectedCollection'); - const existingContentIds = form.watch('existingContentIds'); + const selectedCollection = useWatch({ control: form.control, name: 'selectedCollection' }); + const existingContentIds = useWatch({ control: form.control, name: 'existingContentIds' }) ?? []; const [contentType, setContentType] = useState<'lesson' | 'assignment' | ''>(''); const [sortDirection, setSortDirection] = useState('asc'); @@ -50,7 +50,8 @@ const ContentListTable = () => { const totalItems = getContentsQuery.data?.total_record || 0; const totalPages = Number(getContentsQuery.data?.total_page || 0); const collectionName = selectedCollection?.post_title ?? __('All Contents', 'tutor'); - const selectedContents = form.watch('contents') || []; + const watchedContents = useWatch({ control: form.control, name: 'contents' }); + const selectedContents = useMemo(() => watchedContents || [], [watchedContents]); const selectedContentIds = selectedContents.map((content) => String(content.ID)); const fetchedContents = useMemo(() => getContentsQuery.data?.data ?? [], [getContentsQuery.data]); @@ -124,9 +125,9 @@ const ContentListTable = () => {
{ - const selectedContents = form.watch('contents') || []; + const currentSelected = form.getValues('contents') || []; - const filteredContents = selectedContents.filter((content) => String(content.ID) !== String(item.ID)); + const filteredContents = currentSelected.filter((content) => String(content.ID) !== String(item.ID)); const isNewItem = filteredContents.length === selectedContents.length; if (isNewItem) { diff --git a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx index 7fb68f5007..3b60b6bcd7 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/QuestionListTable.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, _n, sprintf } from '@wordpress/i18n'; @@ -33,7 +33,7 @@ const questionTypeOptions = tutorConfig.is_legacy_learning_mode const QuestionListTable = () => { const { pageInfo, onPageChange, itemsPerPage, onFilterItems } = usePaginatedTable(); const form = useFormContext(); - const selectedCollection = form.watch('selectedCollection'); + const selectedCollection = useWatch({ control: form.control, name: 'selectedCollection' }); const [questionTypes, setQuestionTypes] = useState([]); const [sortDirection, setSortDirection] = useState('asc'); const [orderDirection, setOrderDirection] = useState('asc'); @@ -51,7 +51,8 @@ const QuestionListTable = () => { const totalItems = getContentsQuery.data?.total_record || 0; const totalPages = Number(getContentsQuery.data?.total_page || 0); const collectionName = selectedCollection?.post_title ?? __('All Contents', 'tutor'); - const selectedContents = form.watch('contents') || []; + const watchedContents = useWatch({ control: form.control, name: 'contents' }); + const selectedContents = useMemo(() => watchedContents || [], [watchedContents]); const selectedContentIds = selectedContents.map((content) => String(content.ID)); const fetchedContents = useMemo(() => getContentsQuery.data?.data ?? [], [getContentsQuery.data]); @@ -113,8 +114,8 @@ const QuestionListTable = () => { name: 'title', Cell: (item) => { const handleItemToggle = () => { - const selectedContents = form.watch('contents') || []; - const filteredContents = selectedContents.filter((content) => String(content.ID) !== String(item.ID)); + const currentSelected = form.getValues('contents') || []; + const filteredContents = currentSelected.filter((content) => String(content.ID) !== String(item.ID)); const isNewItem = filteredContents.length === selectedContents.length; if (isNewItem) { diff --git a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/index.tsx b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/index.tsx index 76056ddf44..6a42108f30 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/index.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/ContentBankContentSelectModal/index.tsx @@ -1,4 +1,4 @@ -import { FormProvider } from 'react-hook-form'; +import { FormProvider, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -81,7 +81,8 @@ const CollectionListModal = ({ closeModal({ action: 'CONFIRM' }); }; - const selectedCollection = form.watch('selectedCollection'); + const selectedCollection = useWatch({ control: form.control, name: 'selectedCollection' }); + const contentsList = useWatch({ control: form.control, name: 'contents' }) ?? []; return ( @@ -103,7 +104,7 @@ const CollectionListModal = ({ > - +
- + {__('Generate Now', 'tutor')} diff --git a/assets/src/js/v3/entries/course-builder/components/ai-course-modal/ContentGeneration.tsx b/assets/src/js/v3/entries/course-builder/components/ai-course-modal/ContentGeneration.tsx index 7b03c6ec4a..7345df29bb 100644 --- a/assets/src/js/v3/entries/course-builder/components/ai-course-modal/ContentGeneration.tsx +++ b/assets/src/js/v3/entries/course-builder/components/ai-course-modal/ContentGeneration.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -108,10 +108,11 @@ const ContentGeneration = ({ onClose }: { onClose: () => void }) => { const boxRef = useRef(null); const form = useFormWithGlobalError<{ prompt: string }>({ defaultValues: { prompt: '' } }); - const promptValue = form.watch('prompt'); + const promptValue = useWatch({ control: form.control, name: 'prompt' }); const { showToast } = useToast(); useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setLoadingSteps((previous) => { const copy = { ...previous }; const keys = getObjectKeys(copy); diff --git a/assets/src/js/v3/entries/course-builder/components/course-basic/CourseBasicSidebar.tsx b/assets/src/js/v3/entries/course-builder/components/course-basic/CourseBasicSidebar.tsx index 115ab17898..20859bba5a 100644 --- a/assets/src/js/v3/entries/course-builder/components/course-basic/CourseBasicSidebar.tsx +++ b/assets/src/js/v3/entries/course-builder/components/course-basic/CourseBasicSidebar.tsx @@ -58,7 +58,7 @@ const CourseBasicSidebar = () => { ); const isMembershipOnlyMode = isAddonEnabled(Addons.SUBSCRIPTION) && tutorConfig.settings?.membership_only_mode; - const currentAuthor = form.watch('post_author'); + const currentAuthor = useWatch({ control: form.control, name: 'post_author' }); const isInstructorVisible = isTutorPro && isMultiInstructorEnabled && (isAdministrator || (isInstructor && canInstructorMangeCoInstructors)); diff --git a/assets/src/js/v3/entries/course-builder/components/course-basic/CourseSettings.tsx b/assets/src/js/v3/entries/course-builder/components/course-basic/CourseSettings.tsx index 5a04957b7c..c25f897bb6 100644 --- a/assets/src/js/v3/entries/course-builder/components/course-basic/CourseSettings.tsx +++ b/assets/src/js/v3/entries/course-builder/components/course-basic/CourseSettings.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { useIsFetching } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; @@ -42,9 +42,10 @@ const CourseSettings = () => { VisibilityControlKeys.COURSE_BUILDER.BASICS.OPTIONS.ENROLLMENT, ); - const isContentDripActive = form.watch('contentDripType'); - const isBuddyPressEnabled = form.watch('enable_tutor_bp'); - const isPaidCourse = form.watch('course_price_type') === 'paid'; + const isContentDripActive = useWatch({ control: form.control, name: 'contentDripType' }); + const isBuddyPressEnabled = useWatch({ control: form.control, name: 'enable_tutor_bp' }); + const coursePriceType = useWatch({ control: form.control, name: 'course_price_type' }); + const isPaidCourse = coursePriceType === 'paid'; const availableTabs = [ isGeneralSettingsVisible && { diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuizSettings.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuizSettings.tsx index e8fd2e13fb..51da7f7890 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuizSettings.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuizSettings.tsx @@ -1,4 +1,4 @@ -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { useQueryClient } from '@tanstack/react-query'; import { __, sprintf } from '@wordpress/i18n'; @@ -99,16 +99,17 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { const isLegacyLearningMode = tutorConfig.settings?.learning_mode === 'legacy'; const adminPartialEnabled = tutorConfig.settings?.enable_quiz_partial_marking === 'on'; const adminNegativeEnabled = tutorConfig.settings?.enable_quiz_negative_marking === 'on'; - const partialMarkingEnabled = form.watch('quiz_option.enable_partial_marking'); - const negativeMarkType = form.watch('quiz_option.negative_mark_type'); - const negativeMarkingEnabled = form.watch('quiz_option.enable_negative_marking'); + const partialMarkingEnabled = useWatch({ control: form.control, name: 'quiz_option.enable_partial_marking' }); + const negativeMarkType = useWatch({ control: form.control, name: 'quiz_option.negative_mark_type' }); + const negativeMarkingEnabled = useWatch({ control: form.control, name: 'quiz_option.enable_negative_marking' }); - const questions = form.watch('questions'); + const watchedQuestions = useWatch({ control: form.control, name: 'questions' }); + const questions = watchedQuestions || []; const questionsCount = questions.length; const hasOpenEndedQuestions = questions.some((question) => question.question_type === 'open_ended'); const hasShortAnswerQuestions = questions.some((question) => question.question_type === 'short_answer'); const hasCharacterLimitQuestions = hasOpenEndedQuestions || hasShortAnswerQuestions; - const hasAttemptsLimit = form.watch('quiz_option.limit_attempts_allowed'); + const hasAttemptsLimit = useWatch({ control: form.control, name: 'quiz_option.limit_attempts_allowed' }); const showPassRequired = isAddonEnabled(Addons.CONTENT_DRIP) && contentDripType === 'unlock_sequentially' && hasAttemptsLimit; const showContentDripSettings = @@ -116,11 +117,12 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { contentType !== 'tutor_h5p_quiz' && Boolean(contentDripType) && contentDripType !== 'unlock_sequentially'; - const hasQuestionLimit = form.watch('quiz_option.limit_questions_to_answer'); - const hasTimeLimit = form.watch('quiz_option.enable_time_limit'); - const questionsOrder = form.watch('quiz_option.questions_order'); + const hasQuestionLimit = useWatch({ control: form.control, name: 'quiz_option.limit_questions_to_answer' }); + const hasTimeLimit = useWatch({ control: form.control, name: 'quiz_option.enable_time_limit' }); + const questionsOrder = useWatch({ control: form.control, name: 'quiz_option.questions_order' }); + const maxQuestionsForAnswer = useWatch({ control: form.control, name: 'quiz_option.max_questions_for_answer' }); const availableQuestionInPool = hasQuestionLimit - ? Math.min(Number(form.watch('quiz_option.max_questions_for_answer')), questionsCount) || questionsCount + ? Math.min(Number(maxQuestionsForAnswer), questionsCount) || questionsCount : questionsCount; const usedQuestionCountPercentage = (availableQuestionInPool / questionsCount) * 100; const orderedQuestions = (() => { @@ -144,7 +146,8 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { })(); const questionsForStats = hasQuestionLimit ? orderedQuestions.slice(0, availableQuestionInPool) : orderedQuestions; const questionCountForStats = questionsForStats.length; - const passingGrade = Number(form.watch('quiz_option.passing_grade')); + const passingGradeValue = useWatch({ control: form.control, name: 'quiz_option.passing_grade' }); + const passingGrade = Number(passingGradeValue); const totalMarksForStats = questionsForStats.reduce( (sum, question) => sum + Number(question.question_settings.question_mark || 0), 0, @@ -169,8 +172,14 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { return questionCountForStats; })(); - const timeLimitValue = Number(form.watch('quiz_option.time_limit.time_value')); - const timeLimitType = form.watch('quiz_option.time_limit.time_type'); + const timeLimitVal = useWatch({ control: form.control, name: 'quiz_option.time_limit.time_value' }); + const timeLimitValue = Number(timeLimitVal); + const timeLimitType = useWatch({ control: form.control, name: 'quiz_option.time_limit.time_type' }); + const quizAutoStart = useWatch({ control: form.control, name: 'quiz_option.quiz_auto_start' }); + const questionLayoutView = useWatch({ control: form.control, name: 'quiz_option.question_layout_view' }); + const enablePagination = useWatch({ control: form.control, name: 'quiz_option.enable_pagination' }); + const paginationType = useWatch({ control: form.control, name: 'quiz_option.pagination_type' }); + const enableAnswerReveal = useWatch({ control: form.control, name: 'quiz_option.enable_answer_reveal' }); const timePerQuestionInSeconds = questionCountForStats > 0 ? Math.floor(getTimeInSeconds(timeLimitValue, timeLimitType) / questionCountForStats) : 0; @@ -221,7 +230,7 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { )} /> - + { )} /> - +
{
- + { )} /> - +
{__('After', 'tutor')}
{ )} /> - +
@@ -591,13 +600,9 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { )} /> - +
- +
{ )} /> - +
{__('For', 'tutor')}
{
- + {
- {form.watch('quiz_option.time_limit.time_value')} {form.watch('quiz_option.time_limit.time_type')} + {timeLimitVal} {timeLimitType}
{__('Time limit', 'tutor')}
@@ -890,7 +895,7 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => {
- {form.watch('quiz_option.passing_grade')} + {passingGradeValue} {__('%', 'tutor')}
{__('Passing grade', 'tutor')}
diff --git a/assets/src/js/v3/entries/course-builder/components/modals/QuizModal.tsx b/assets/src/js/v3/entries/course-builder/components/modals/QuizModal.tsx index 639f0499a7..d1db145eda 100644 --- a/assets/src/js/v3/entries/course-builder/components/modals/QuizModal.tsx +++ b/assets/src/js/v3/entries/course-builder/components/modals/QuizModal.tsx @@ -352,7 +352,7 @@ const QuizModal = ({ variant="text" type="button" onClick={() => { - if (!form.watch('quiz_title')) { + if (!form.getValues('quiz_title')) { closeModal(); } setIsEdit(false); diff --git a/assets/src/js/v3/entries/course-builder/pages/Additional.tsx b/assets/src/js/v3/entries/course-builder/pages/Additional.tsx index 6b06480c75..9647db0cc3 100644 --- a/assets/src/js/v3/entries/course-builder/pages/Additional.tsx +++ b/assets/src/js/v3/entries/course-builder/pages/Additional.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { css } from '@emotion/react'; import { useIsFetching, useQueryClient } from '@tanstack/react-query'; @@ -56,6 +56,7 @@ const Additional = () => { }, [navigate]); const form = useFormContext(); + const currentCertificateKey = useWatch({ control: form.control, name: 'tutor_course_certificate_template' }); const queryClient = useQueryClient(); const isCourseDetailsFetching = useIsFetching({ queryKey: ['CourseDetails', courseId], @@ -94,7 +95,6 @@ const Additional = () => { return null; } - const currentCertificateKey = form.watch('tutor_course_certificate_template'); const certificateTemplates = courseDetails?.course_certificates_templates ?? []; const isOverViewVisible = @@ -118,7 +118,7 @@ const Additional = () => { }); }; - const SidebarContent = () => ( + const renderSidebarContent = () => (
@@ -341,9 +341,7 @@ const Additional = () => { -
- -
+
{renderSidebarContent()}
@@ -352,9 +350,7 @@ const Additional = () => {
-
- -
+
{renderSidebarContent()}
diff --git a/assets/src/js/v3/entries/course-builder/pages/CourseBasic.tsx b/assets/src/js/v3/entries/course-builder/pages/CourseBasic.tsx index b408b2e2c3..b08840ec10 100644 --- a/assets/src/js/v3/entries/course-builder/pages/CourseBasic.tsx +++ b/assets/src/js/v3/entries/course-builder/pages/CourseBasic.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { useIsFetching, useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; @@ -50,9 +50,8 @@ const CourseBasic = () => { const isTutorPro = !!tutorConfig.tutor_pro_url; const isOpenAiEnabled = tutorConfig.settings?.chatgpt_enable === 'on'; - const postStatus = form.watch('post_status'); - - const editorUsed = form.watch('editor_used'); + const postStatus = useWatch({ control: form.control, name: 'post_status' }); + const editorUsed = useWatch({ control: form.control, name: 'editor_used' }); return (
diff --git a/assets/src/js/v3/shared/components/fields/FormCategoriesInput.tsx b/assets/src/js/v3/shared/components/fields/FormCategoriesInput.tsx index e7728351be..ca35730a79 100644 --- a/assets/src/js/v3/shared/components/fields/FormCategoriesInput.tsx +++ b/assets/src/js/v3/shared/components/fields/FormCategoriesInput.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Controller, type FieldValues } from 'react-hook-form'; +import { Controller, type FieldValues, useWatch } from 'react-hook-form'; import { css, type SerializedStyles } from '@emotion/react'; import { __ } from '@wordpress/i18n'; import { produce } from 'immer'; @@ -56,7 +56,8 @@ const FormMultiLevelInput = ({ }>({ shouldFocusError: true, }); - const searchValue = form.watch('search'); + const search = useWatch({ control: form.control, name: 'search' }) ?? ''; + const searchValue = search; const debouncedSearchValue = useDebounce(searchValue, 300); const categoryListQuery = useCategoryListQuery(debouncedSearchValue); const createCategoryMutation = useCreateCategoryMutation(); @@ -66,6 +67,7 @@ const FormMultiLevelInput = ({ useEffect(() => { if (!categoryListQuery.isLoading && (categoryListQuery.data || []).length > 0) { + // eslint-disable-next-line react-hooks/set-state-in-effect setHasCategories(true); } }, [categoryListQuery.isLoading, categoryListQuery.data]); diff --git a/assets/src/js/v3/shared/components/fields/FormVideoInput.tsx b/assets/src/js/v3/shared/components/fields/FormVideoInput.tsx index 2b9fba370f..d3aa9d5744 100644 --- a/assets/src/js/v3/shared/components/fields/FormVideoInput.tsx +++ b/assets/src/js/v3/shared/components/fields/FormVideoInput.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import type { UseMutationResult } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; @@ -264,7 +264,8 @@ const FormVideoInput = ({ : null, }); - const videoSource = form.watch('videoSource') || ''; + const videoSource = useWatch({ control: form.control, name: 'videoSource' }) || ''; + const videoUrlValue = useWatch({ control: form.control, name: 'videoUrl' }); const fieldValue = field.value; useEffect(() => { @@ -290,6 +291,7 @@ const FormVideoInput = ({ if (!fieldValue.poster_url && thumbnailGeneratorSources.includes(fieldValue.source)) { const source = fieldValue.source as 'vimeo' | 'youtube' | 'html5'; + // eslint-disable-next-line react-hooks/set-state-in-effect setIsThumbnailLoading(true); generateVideoThumbnail(source, fieldValue[`source_${source}` as keyof CourseVideo] || '') .then((url) => { @@ -612,7 +614,7 @@ const FormVideoInput = ({ > {form.watch('videoUrl')}
} + fallback={
{videoUrlValue}
} >
diff --git a/assets/src/js/v3/shared/components/magic-ai-image/ImageGeneration.tsx b/assets/src/js/v3/shared/components/magic-ai-image/ImageGeneration.tsx index 80df005068..fb539df52f 100644 --- a/assets/src/js/v3/shared/components/magic-ai-image/ImageGeneration.tsx +++ b/assets/src/js/v3/shared/components/magic-ai-image/ImageGeneration.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; +import { Controller, useForm, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -117,8 +117,8 @@ export const ImageGeneration = () => { const [showEmptyState, setShowEmptyState] = useState(images.every((image) => image === null)); const [imageLoading, setImageLoading] = useState([false, false, false, false]); - const styleValue = form.watch('style'); - const promptValue = form.watch('prompt'); + const styleValue = useWatch({ control: form.control, name: 'style' }); + const promptValue = useWatch({ control: form.control, name: 'prompt' }); const isDisabledButton = !styleValue || !promptValue; const hasGeneratedImage = images.some(isDefined); diff --git a/assets/src/js/v3/shared/components/magic-ai-image/MagicFill.tsx b/assets/src/js/v3/shared/components/magic-ai-image/MagicFill.tsx index b9ba736220..3ad77ef817 100644 --- a/assets/src/js/v3/shared/components/magic-ai-image/MagicFill.tsx +++ b/assets/src/js/v3/shared/components/magic-ai-image/MagicFill.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css, keyframes } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -39,7 +39,9 @@ const MagicFill = () => { const { onDropdownMenuChange, currentImage, field, onCloseModal } = useMagicImageGeneration(); const storeAIGeneratedImageMutation = useStoreAIGeneratedImageMutation(); - const brushSize = useDebounce(form.watch('brush_size', 40)); + const watchedBrushSize = useWatch({ control: form.control, name: 'brush_size', defaultValue: 40 }); + const promptValue = useWatch({ control: form.control, name: 'prompt' }); + const brushSize = useDebounce(watchedBrushSize); const [trackStack, setTrackStack] = useState([]); const [pointer, setPointer] = useState(1); @@ -94,6 +96,7 @@ const MagicFill = () => { return (
{ const canvas = canvasRef.current; const context = canvas?.getContext('2d'); @@ -261,7 +264,7 @@ const MagicFill = () => { ]} >
- + {__('Generative Erase', __TUTOR_TEXT_DOMAIN__)} diff --git a/assets/src/js/v3/shared/components/modals/AITextModal.tsx b/assets/src/js/v3/shared/components/modals/AITextModal.tsx index d264eefe33..b8f49cc3c1 100644 --- a/assets/src/js/v3/shared/components/modals/AITextModal.tsx +++ b/assets/src/js/v3/shared/components/modals/AITextModal.tsx @@ -5,6 +5,7 @@ import { type ControllerRenderProps, type FieldValues, type Path, + useWatch, } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -107,7 +108,7 @@ const AITextModal = ({ return content[pointer]; }, [content, pointer]); - const prompt = form.watch('prompt'); + const prompt = useWatch({ control: form.control, name: 'prompt' }); function insertText(text: string) { setContent((previous) => [text, ...previous]); diff --git a/assets/src/js/v3/shared/components/subscription/OfferSalePrice.tsx b/assets/src/js/v3/shared/components/subscription/OfferSalePrice.tsx index c0bd91fa0d..db7a274393 100644 --- a/assets/src/js/v3/shared/components/subscription/OfferSalePrice.tsx +++ b/assets/src/js/v3/shared/components/subscription/OfferSalePrice.tsx @@ -1,4 +1,4 @@ -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -20,9 +20,9 @@ const { tutor_currency } = tutorConfig; export function OfferSalePrice() { const form = useFormContext(); - const hasSale = form.watch('offer_sale_price'); - const regularPrice = form.watch('regular_price'); - const hasScheduledSale = !!form.watch('schedule_sale_price'); + const hasSale = useWatch({ control: form.control, name: 'offer_sale_price' }); + const regularPrice = useWatch({ control: form.control, name: 'regular_price' }); + const hasScheduledSale = !!useWatch({ control: form.control, name: 'schedule_sale_price' }); return (
@@ -110,7 +110,7 @@ export function OfferSalePrice() { required: __('Schedule date is required', __TUTOR_TEXT_DOMAIN__), validate: { checkEndDate: (value) => { - const startDate = form.watch('sale_price_from_date'); + const startDate = form.getValues('sale_price_from_date'); const endDate = value; if (startDate && endDate) { return new Date(startDate) > new Date(endDate) @@ -139,9 +139,9 @@ export function OfferSalePrice() { required: __('Schedule time is required', __TUTOR_TEXT_DOMAIN__), validate: { checkEndTime: (value) => { - const startDate = form.watch('sale_price_from_date'); - const startTime = form.watch('sale_price_from_time'); - const endDate = form.watch('sale_price_to_date'); + const startDate = form.getValues('sale_price_from_date'); + const startTime = form.getValues('sale_price_from_time'); + const endDate = form.getValues('sale_price_to_date'); const endTime = value; if (startDate && endDate && startTime && endTime) { return new Date(`${startDate} ${startTime}`) > new Date(`${endDate} ${endTime}`) diff --git a/assets/src/js/v3/shared/components/subscription/SubscriptionItem.tsx b/assets/src/js/v3/shared/components/subscription/SubscriptionItem.tsx index 2469d7c3be..33916d5a06 100644 --- a/assets/src/js/v3/shared/components/subscription/SubscriptionItem.tsx +++ b/assets/src/js/v3/shared/components/subscription/SubscriptionItem.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -35,7 +35,7 @@ export default function SubscriptionItem() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const chargeEnrolmentFee = form.watch('charge_enrollment_fee'); + const chargeEnrolmentFee = useWatch({ control: form.control, name: 'charge_enrollment_fee' }); // @TODO: Will be added after confirmation // const enableTrial = form.watch(`subscriptions.${index}.enable_free_trial` as `subscriptions.0.enable_free_trial`); From 42180ec0da7e33105bde1da1c7ca78677aefac18 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Sat, 31 Oct 2026 13:59:42 +0600 Subject: [PATCH 7/9] fix(react-compiler): complete form.watch migration to useWatch and form.getValues --- .../additional/meeting/GoogleMeetForm.tsx | 14 +++++----- .../curriculum/GenerateQuizWithAi.tsx | 12 +++++---- .../NegativeMarkTypeMismatchNotice.tsx | 8 +++--- .../curriculum/QuestionConditions.tsx | 17 +++++++++--- .../components/curriculum/QuestionForm.tsx | 19 +++++++++----- .../components/curriculum/QuestionList.tsx | 9 ++++--- .../components/curriculum/TopicContent.tsx | 2 +- .../components/curriculum/TopicFooter.tsx | 8 +++--- .../components/curriculum/TopicHeader.tsx | 7 ++--- .../MultipleChoiceAndOrdering.tsx | 5 +++- .../contexts/QuizModalContext.tsx | 6 +++-- .../modals/CollectionList/index.tsx | 5 ++-- .../ExportInitialState.tsx | 5 ++-- .../ImportInitialState.tsx | 13 ++++++---- .../components/modals/CancelOrderModal.tsx | 4 +-- .../components/modals/DiscountModal.tsx | 6 ++--- .../components/modals/RefundModal.tsx | 4 +-- .../components/PaymentMethods.tsx | 8 +++--- .../components/PaymentSettings.tsx | 4 +-- .../components/EuropeanUnionTax.tsx | 11 ++++---- .../tax-settings/components/TaxRates.tsx | 17 +++++++----- .../tax-settings/components/TaxSettings.tsx | 26 ++++++++++--------- .../components/modals/CountrySelectModal.tsx | 10 ++++--- .../components/modals/CountryTaxRateModal.tsx | 15 ++++------- .../fields/quiz/questions/FormCoordinates.tsx | 5 +++- .../fields/quiz/questions/FormDrawImage.tsx | 2 +- .../fields/quiz/questions/FormPuzzle.tsx | 2 +- .../subscription/OfferSalePrice.tsx | 3 ++- 28 files changed, 143 insertions(+), 104 deletions(-) diff --git a/assets/src/js/v3/entries/course-builder/components/additional/meeting/GoogleMeetForm.tsx b/assets/src/js/v3/entries/course-builder/components/additional/meeting/GoogleMeetForm.tsx index fa10fab7b5..fd72da1d63 100644 --- a/assets/src/js/v3/entries/course-builder/components/additional/meeting/GoogleMeetForm.tsx +++ b/assets/src/js/v3/entries/course-builder/components/additional/meeting/GoogleMeetForm.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; import { format } from 'date-fns'; @@ -71,6 +71,8 @@ const GoogleMeetForm = ({ onCancel, data, topicId, meetingId }: GoogleMeetFormPr mode: 'onChange', }); + const meetingStartDate = useWatch({ control: meetingForm.control, name: 'meeting_start_date' }); + const saveGoogleMeetMeeting = useSaveGoogleMeetMutation(); const timezones = tutorConfig.timezones as Record; @@ -217,7 +219,7 @@ const GoogleMeetForm = ({ onCancel, data, topicId, meetingId }: GoogleMeetFormPr validate: { invalidDate: invalidDateRule, checkEndDate: (value) => { - const startDate = meetingForm.watch('meeting_start_date'); + const startDate = meetingForm.getValues('meeting_start_date'); const endDate = value; if (startDate && endDate) { return new Date(startDate) > new Date(endDate) @@ -233,7 +235,7 @@ const GoogleMeetForm = ({ onCancel, data, topicId, meetingId }: GoogleMeetFormPr )} /> @@ -246,9 +248,9 @@ const GoogleMeetForm = ({ onCancel, data, topicId, meetingId }: GoogleMeetFormPr validate: { invalidTime: invalidTimeRule, checkEndTime: (value) => { - const startDate = meetingForm.watch('meeting_start_date'); - const startTime = meetingForm.watch('meeting_start_time'); - const endDate = meetingForm.watch('meeting_end_date'); + const startDate = meetingForm.getValues('meeting_start_date'); + const startTime = meetingForm.getValues('meeting_start_time'); + const endDate = meetingForm.getValues('meeting_end_date'); const endTime = value; if (startDate && endDate && startTime && endTime) { diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/GenerateQuizWithAi.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/GenerateQuizWithAi.tsx index 0de2fe998d..6dae9151a8 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/GenerateQuizWithAi.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/GenerateQuizWithAi.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; +import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; @@ -118,10 +118,12 @@ const GenerateQuizWithAi = () => { ]; }, [topicId, topics]); - const selectedTopicIds = form.watch('topic_ids') ?? emptyTopicIds; - const questionTypes = form.watch('question_types'); - const questionCount = Number(form.watch('question_count')); - const difficulty_level = form.watch('difficulty_level'); + const watchedTopicIds = useWatch({ control: form.control, name: 'topic_ids' }); + const selectedTopicIds = watchedTopicIds ?? emptyTopicIds; + const questionTypes = useWatch({ control: form.control, name: 'question_types' }); + const watchedQuestionCount = useWatch({ control: form.control, name: 'question_count' }); + const questionCount = Number(watchedQuestionCount); + const difficulty_level = useWatch({ control: form.control, name: 'difficulty_level' }); const selectedQuestionTypes = questionTypeOptions .filter((option) => questionTypes?.[option.value]) .map((option) => option.value); diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/NegativeMarkTypeMismatchNotice.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/NegativeMarkTypeMismatchNotice.tsx index 863c7ec6e3..198bf91066 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/NegativeMarkTypeMismatchNotice.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/NegativeMarkTypeMismatchNotice.tsx @@ -1,4 +1,4 @@ -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -56,9 +56,9 @@ const NegativeMarkTypeMismatchNotice = () => { const form = useFormContext(); const { showModal } = useModal(); - const negativeMarkType = form.watch('quiz_option.negative_mark_type'); - const negativeMarkingEnabled = form.watch('quiz_option.enable_negative_marking'); - const negativeMarkValue = form.watch('quiz_option.negative_mark_value'); + const negativeMarkType = useWatch({ control: form.control, name: 'quiz_option.negative_mark_type' }); + const negativeMarkingEnabled = useWatch({ control: form.control, name: 'quiz_option.enable_negative_marking' }); + const negativeMarkValue = useWatch({ control: form.control, name: 'quiz_option.negative_mark_value' }); const adminNegativeMarkEnabled = tutorConfig.settings?.enable_quiz_negative_marking === 'on'; const adminNegativeMarkValue = tutorConfig.settings.quiz_negative_mark_value; diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionConditions.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionConditions.tsx index 2eaa6fc9a6..b9e20b8b7f 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionConditions.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionConditions.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -29,9 +29,18 @@ const QuestionConditions = () => { const form = useFormContext(); const { showModal, closeModal } = useModal(); - const activeQuestion = form.watch(`questions.${activeQuestionIndex}` as 'questions.0'); - const activeQuestionType = form.watch(`questions.${activeQuestionIndex}.question_type`); - const activeDataStatus = form.watch(`questions.${activeQuestionIndex}._data_status`); + const activeQuestion = useWatch({ + control: form.control, + name: `questions.${activeQuestionIndex}` as 'questions.0', + }); + const activeQuestionType = useWatch({ + control: form.control, + name: `questions.${activeQuestionIndex}.question_type` as 'questions.0.question_type', + }); + const activeDataStatus = useWatch({ + control: form.control, + name: `questions.${activeQuestionIndex}._data_status` as 'questions.0._data_status', + }); const handlePreview = useCallback(() => { showModal({ diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionForm.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionForm.tsx index 0ff7b2b880..76470d4d09 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionForm.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionForm.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -45,9 +45,16 @@ const QuestionForm = () => { const alertRef = useRef(null); - const activeQuestionType = form.watch(`questions.${activeQuestionIndex}.question_type`); - const questions = form.watch('questions') || []; - const dataStatus = form.watch(`questions.${activeQuestionIndex}._data_status`); + const activeQuestionType = useWatch({ + control: form.control, + name: `questions.${activeQuestionIndex}.question_type` as 'questions.0.question_type', + }); + const watchedQuestions = useWatch({ control: form.control, name: 'questions' }); + const questions = watchedQuestions || []; + const dataStatus = useWatch({ + control: form.control, + name: `questions.${activeQuestionIndex}._data_status` as 'questions.0._data_status', + }); const questionTypeForm = { true_false: , @@ -129,7 +136,7 @@ const QuestionForm = () => { fallback={
- {`[h5p id: "${form.watch(`questions.${activeQuestionIndex}.question_description`)}"]`} + {`[h5p id: "${form.getValues(`questions.${activeQuestionIndex}.question_description`)}"]`}
} @@ -164,7 +171,7 @@ const QuestionForm = () => {
-
+
{validationError?.message} diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx index d127456fc1..0931426001 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { useFieldArray, useFormContext } from 'react-hook-form'; +import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; import { closestCenter, DndContext, @@ -103,7 +103,8 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => { return questionFields.find((item) => item.question_id === activeSortId); }, [activeSortId, questionFields]); - const questions = form.watch('questions') || []; + const watchedQuestions = useWatch({ control: form.control, name: 'questions' }); + const questions = watchedQuestions || []; const activeQuestion = form.getValues(`questions.${activeQuestionIndex}` as 'questions.0'); const handleAddQuestion = (questionType: QuizQuestionType, content?: H5PContent) => { @@ -319,7 +320,7 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => { }; const handleDuplicateQuestion = (data: QuizQuestion, index: number) => { - const currentQuestion = form.watch(`questions.${index}` as 'questions.0'); + const currentQuestion = form.getValues(`questions.${index}` as 'questions.0'); if (!currentQuestion || validationError) { return; @@ -391,7 +392,7 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => { window.innerHeight - questionListRef.current.getBoundingClientRect().top }px`; } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/refs }, [questionListRef.current, isEditing]); if (!form.getValues('quiz_title')) { diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/TopicContent.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/TopicContent.tsx index 87698a40ad..16d26853b4 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/TopicContent.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/TopicContent.tsx @@ -174,7 +174,7 @@ const TopicContent = ({ type, topic, content, listeners, isDragging = false, onC showModal({ component: modalComponent[contentType], props: { - contentDripType: form.watch('contentDripType'), + contentDripType: form.getValues('contentDripType'), topicId: topicId, lessonId: contentId, assignmentId: contentId, diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/TopicFooter.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/TopicFooter.tsx index 9243398673..cc491ef4b8 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/TopicFooter.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/TopicFooter.tsx @@ -96,7 +96,7 @@ const TopicFooter = ({ topic, nextContentOrder }: TopicFooterProps) => { showModal({ component: LessonModal, props: { - contentDripType: courseDetailsForm.watch('contentDripType'), + contentDripType: courseDetailsForm.getValues('contentDripType'), topicId: topicId, title: __('Lesson', 'tutor'), icon: , @@ -121,7 +121,7 @@ const TopicFooter = ({ topic, nextContentOrder }: TopicFooterProps) => { component: QuizModal, props: { topicId: topicId, - contentDripType: courseDetailsForm.watch('contentDripType'), + contentDripType: courseDetailsForm.getValues('contentDripType'), title: __('Quiz', 'tutor'), icon: , /* translators: %s is the topic title */ @@ -150,7 +150,7 @@ const TopicFooter = ({ topic, nextContentOrder }: TopicFooterProps) => { component: QuizModal, props: { topicId: topicId, - contentDripType: courseDetailsForm.watch('contentDripType'), + contentDripType: courseDetailsForm.getValues('contentDripType'), title: __('Interactive Quiz', 'tutor'), icon: , /* translators: %s is the topic title */ @@ -196,7 +196,7 @@ const TopicFooter = ({ topic, nextContentOrder }: TopicFooterProps) => { component: AssignmentModal, props: { topicId: topicId, - contentDripType: courseDetailsForm.watch('contentDripType'), + contentDripType: courseDetailsForm.getValues('contentDripType'), title: __('Assignment', 'tutor'), icon: , /* translators: %s is the topic title */ diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/TopicHeader.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/TopicHeader.tsx index 7308f21922..bb9b11ab6b 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/TopicHeader.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/TopicHeader.tsx @@ -1,5 +1,5 @@ import { memo, useEffect, useRef, useState } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities'; import { css } from '@emotion/react'; import { animated } from '@react-spring/web'; @@ -127,7 +127,8 @@ const TopicHeader = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [isEdit]); - const summaryValue = form.watch('summary') ?? ''; + const summaryValue = useWatch({ control: form.control, name: 'summary' }) ?? ''; + const titleValue = useWatch({ control: form.control, name: 'title' }); const hasSummary = summaryValue.trim().length > 0; return ( @@ -163,7 +164,7 @@ const TopicHeader = ({ when={isEdit} fallback={
setIsEdit(true)}> - {form.watch('title')} + {titleValue}
} > diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/MultipleChoiceAndOrdering.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/MultipleChoiceAndOrdering.tsx index 629215ca50..8a9b8ea66e 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/MultipleChoiceAndOrdering.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/question-types/MultipleChoiceAndOrdering.tsx @@ -42,7 +42,10 @@ const MultipleChoiceAndOrdering = () => { defaultValue: false, }); - const currentQuestionType = form.watch(`questions.${activeQuestionIndex}.question_type`); + const currentQuestionType = useWatch({ + control: form.control, + name: `questions.${activeQuestionIndex}.question_type` as 'questions.0.question_type', + }); const { fields: optionsFields, diff --git a/assets/src/js/v3/entries/course-builder/contexts/QuizModalContext.tsx b/assets/src/js/v3/entries/course-builder/contexts/QuizModalContext.tsx index 5cab7ec240..3cc208c617 100644 --- a/assets/src/js/v3/entries/course-builder/contexts/QuizModalContext.tsx +++ b/assets/src/js/v3/entries/course-builder/contexts/QuizModalContext.tsx @@ -1,6 +1,7 @@ +/* eslint-disable react-hooks/set-state-in-effect */ import type React from 'react'; import { createContext, useContext, useEffect, useRef, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { type ID, @@ -75,7 +76,8 @@ export const QuizModalContextProvider = ({ }) => { const [activeQuestionId, setActiveQuestionId] = useState(''); const form = useFormContext(); - const questions = form.watch('questions') || []; + const watchedQuestions = useWatch({ control: form.control, name: 'questions' }); + const questions = watchedQuestions || []; const previousQuestions = useRef(questions); const [validationError, setValidationError] = useState<{ message: string; diff --git a/assets/src/js/v3/entries/import-export/components/modals/CollectionList/index.tsx b/assets/src/js/v3/entries/import-export/components/modals/CollectionList/index.tsx index a602675518..d5b3fc224a 100644 --- a/assets/src/js/v3/entries/import-export/components/modals/CollectionList/index.tsx +++ b/assets/src/js/v3/entries/import-export/components/modals/CollectionList/index.tsx @@ -1,4 +1,4 @@ -import { type UseFormReturn } from 'react-hook-form'; +import { type UseFormReturn, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -31,7 +31,8 @@ const CollectionListModal = ({ closeModal, actions, form }: CollectionListModalP }, }); - const selectedItems = (_form.watch('content_bank') as Collection[]) || []; + const watchedItems = useWatch({ control: _form.control, name: 'content_bank' }); + const selectedItems = (watchedItems as Collection[]) || []; const handleAddContent = () => { const selectedItems = _form.getValues('content_bank') || []; diff --git a/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ExportInitialState.tsx b/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ExportInitialState.tsx index fa4fb992a3..ffee5c1b25 100644 --- a/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ExportInitialState.tsx +++ b/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ExportInitialState.tsx @@ -1,5 +1,5 @@ import { type FunctionComponent } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -51,6 +51,7 @@ const ExportInitialState = ({ resetBulkSelection, }: ExportInitialStateProps) => { const { showModal } = useModal(); + const watchedFormValues = useWatch({ control: form.control }) as Record; /** * Returns properly formatted label for form data keys with appropriate count information * @@ -144,7 +145,7 @@ const ExportInitialState = ({ <> {exportableContent.map((contentType) => { const contentKey = contentType.key; - const isChecked = form.watch(contentKey); + const isChecked = watchedFormValues?.[contentKey]; const bulkSelectionCount = bulkSelectionForm.getValues(contentKey as keyof BulkSelectionFormData)?.length || 0; diff --git a/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ImportInitialState.tsx b/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ImportInitialState.tsx index 84271da673..d45dfa3b8e 100644 --- a/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ImportInitialState.tsx +++ b/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ImportInitialState.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -90,9 +90,10 @@ const ImportInitialState = ({ files: propsFiles, currentStep, onClose, onImport collectionId: '', }, }); - const searchTerm = form.watch('collectionSearch'); + const watchedSearchTerm = useWatch({ control: form.control, name: 'collectionSearch' }); + const searchTerm = watchedSearchTerm ?? ''; const search = useDebounce(searchTerm, 300); - const isContentBankSelectionEnabled = form.watch('importIntoContentBank'); + const isContentBankSelectionEnabled = useWatch({ control: form.control, name: 'importIntoContentBank' }); const { intersectionEntry, intersectionElementRef } = useIntersectionObserver({ dependencies: [isContentBankSelectionEnabled], @@ -104,7 +105,8 @@ const ImportInitialState = ({ files: propsFiles, currentStep, onClose, onImport isEnabled: !!isContentBankSelectionEnabled, }); - const files = form.watch('files'); + const watchedFiles = useWatch({ control: form.control, name: 'files' }); + const files = watchedFiles ?? []; useEffect(() => { if (files.length === 0) { @@ -115,6 +117,7 @@ const ImportInitialState = ({ files: propsFiles, currentStep, onClose, onImport return; } + // eslint-disable-next-line react-hooks/set-state-in-effect setIsReadingFile(true); readJsonFile(files[0]) .then((data) => { @@ -322,7 +325,7 @@ const ImportInitialState = ({ files: propsFiles, currentStep, onClose, onImport onClick={async () => onImport({ file: files[0], - collectionId: isContentBankSelectionEnabled ? Number(form.watch('collectionId')) : undefined, + collectionId: isContentBankSelectionEnabled ? Number(form.getValues('collectionId')) : undefined, }) } > diff --git a/assets/src/js/v3/entries/order-details/components/modals/CancelOrderModal.tsx b/assets/src/js/v3/entries/order-details/components/modals/CancelOrderModal.tsx index b1d038f3f1..8474aa998a 100644 --- a/assets/src/js/v3/entries/order-details/components/modals/CancelOrderModal.tsx +++ b/assets/src/js/v3/entries/order-details/components/modals/CancelOrderModal.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -78,7 +78,7 @@ function CancelOrderModal({ title, order_id, closeModal, actions }: CancelOrderM }, }); - const reasonValue = form.watch('reason'); + const reasonValue = useWatch({ control: form.control, name: 'reason' }); const explanation = reasonOptions.find((item) => item.value === reasonValue)?.explanation ?? // prettier-ignore diff --git a/assets/src/js/v3/entries/order-details/components/modals/DiscountModal.tsx b/assets/src/js/v3/entries/order-details/components/modals/DiscountModal.tsx index 720b7f4793..637c10257e 100644 --- a/assets/src/js/v3/entries/order-details/components/modals/DiscountModal.tsx +++ b/assets/src/js/v3/entries/order-details/components/modals/DiscountModal.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -51,8 +51,8 @@ function DiscountModal({ title, closeModal, actions, discount, total_price, orde defaultValues: discount, mode: 'onChange', }); - const type = form.watch('type'); - const value = form.watch('amount'); + const type = useWatch({ control: form.control, name: 'type' }); + const value = useWatch({ control: form.control, name: 'amount' }); const currencySymbol = tutorConfig.tutor_currency.symbol ?? '$'; const content = type === 'flat' ? currencySymbol : '%'; const priceAfterDiscount = useMemo(() => { diff --git a/assets/src/js/v3/entries/order-details/components/modals/RefundModal.tsx b/assets/src/js/v3/entries/order-details/components/modals/RefundModal.tsx index aa93688694..a6f055c13f 100644 --- a/assets/src/js/v3/entries/order-details/components/modals/RefundModal.tsx +++ b/assets/src/js/v3/entries/order-details/components/modals/RefundModal.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller } from 'react-hook-form'; +import { Controller, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -62,7 +62,7 @@ function RefundModal({ }), }, }); - const amount = form.watch('amount', 0); + const amount = useWatch({ control: form.control, name: 'amount', defaultValue: 0 }); useEffect(() => { form.setFocus('amount'); diff --git a/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx b/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx index 44df06d4c9..84f8fa0ab1 100644 --- a/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx +++ b/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; -import { useFormContext } from 'react-hook-form'; +import { useFormContext, useWatch } from 'react-hook-form'; import { closestCenter, DndContext, @@ -26,10 +26,12 @@ import { moveTo } from '@TutorShared/utils/util'; import type { PaymentSettings } from '../services/payment'; import PaymentItem from './PaymentItem'; +const emptyPaymentMethods: PaymentSettings['payment_methods'] = []; + const PaymentMethods = () => { const form = useFormContext(); - // eslint-disable-next-line react-hooks/exhaustive-deps - const paymentMethods = form.watch('payment_methods') ?? []; + const watchedPaymentMethods = useWatch({ control: form.control, name: 'payment_methods' }); + const paymentMethods = watchedPaymentMethods ?? emptyPaymentMethods; const [activeSortId, setActiveSortId] = useState(null); diff --git a/assets/src/js/v3/entries/payment-settings/components/PaymentSettings.tsx b/assets/src/js/v3/entries/payment-settings/components/PaymentSettings.tsx index 0608ac1198..11d0fc8959 100644 --- a/assets/src/js/v3/entries/payment-settings/components/PaymentSettings.tsx +++ b/assets/src/js/v3/entries/payment-settings/components/PaymentSettings.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { FormProvider } from 'react-hook-form'; +import { FormProvider, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -34,7 +34,7 @@ const TaxSettingsPage = () => { mode: 'all', }); const { reset } = form; - const formData = form.watch(); + const formData = useWatch({ control: form.control }); const tutorOptionSaved = () => { reset(form.getValues()); diff --git a/assets/src/js/v3/entries/tax-settings/components/EuropeanUnionTax.tsx b/assets/src/js/v3/entries/tax-settings/components/EuropeanUnionTax.tsx index e4143190c0..50f4a813b9 100644 --- a/assets/src/js/v3/entries/tax-settings/components/EuropeanUnionTax.tsx +++ b/assets/src/js/v3/entries/tax-settings/components/EuropeanUnionTax.tsx @@ -1,4 +1,4 @@ -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -27,18 +27,17 @@ interface ColumnDataType { function EuropeanUnionTax() { const { openCountryTaxRateModal } = useCountryTaxRateModal(); const form = useFormContext(); - const rates = form.watch('rates'); + const watchedRates = useWatch({ control: form.control, name: 'rates' }); + const rates = watchedRates ?? []; const euIndex = rates.findIndex((rate) => String(rate.country) === String(euCountryCode)); const euRate = rates[euIndex]; const isMicroBusiness = euRate?.vat_registration_type === EUTaxRegistrationTypes.microBusiness; const isOneStopBusiness = euRate?.vat_registration_type === EUTaxRegistrationTypes.oneStop; - if (isMicroBusiness && euRate.states.length) { - euRate.states = [euRate.states[0]]; - } + const states = isMicroBusiness && euRate?.states?.length ? [euRate.states[0]] : (euRate?.states ?? []); - const tableData: ColumnDataType[] = euRate.states.map((euState) => { + const tableData: ColumnDataType[] = states.map((euState) => { return { countryId: euState.id, rate: euState.rate, diff --git a/assets/src/js/v3/entries/tax-settings/components/TaxRates.tsx b/assets/src/js/v3/entries/tax-settings/components/TaxRates.tsx index 6d252ec7d3..4477fac74d 100644 --- a/assets/src/js/v3/entries/tax-settings/components/TaxRates.tsx +++ b/assets/src/js/v3/entries/tax-settings/components/TaxRates.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -35,8 +35,10 @@ export default function TaxRates() { const form = useFormContext(); const { openCountrySelectModal } = useCountrySelectModal(); const { openCountryTaxRateModal } = useCountryTaxRateModal(); - const rates = form.watch('rates'); - const activeCountry = form.watch('active_country'); + const watchedRates = useWatch({ control: form.control, name: 'rates' }); + const rates = watchedRates ?? []; + const activeCountry = useWatch({ control: form.control, name: 'active_country' }); + const enableTax = useWatch({ control: form.control, name: 'enable_tax' }); const activeCountryIndex = rates.findIndex((rate) => String(rate.country) === String(activeCountry)); const activeCountrySelectedStates = rates[activeCountryIndex]?.states ?? []; const activeCountryAllStates = getCountryByCode(activeCountry ?? '')?.states ?? []; @@ -318,7 +320,7 @@ export default function TaxRates() {
- +
@@ -335,9 +337,10 @@ export default function TaxRates() { label={__('Apply single tax rate for entire country', 'tutor')} checked={rates[activeCountryIndex]?.is_same_rate ?? false} onChange={(isChecked) => { - const currentCountry = rates[activeCountryIndex]; - currentCountry.is_same_rate = isChecked; - form.setValue('rates', rates); + const updatedRates = rates.map((rate, idx) => + idx === activeCountryIndex ? { ...rate, is_same_rate: isChecked } : rate, + ); + form.setValue('rates', updatedRates); }} /> diff --git a/assets/src/js/v3/entries/tax-settings/components/TaxSettings.tsx b/assets/src/js/v3/entries/tax-settings/components/TaxSettings.tsx index b7236c54d7..40cdebd40d 100644 --- a/assets/src/js/v3/entries/tax-settings/components/TaxSettings.tsx +++ b/assets/src/js/v3/entries/tax-settings/components/TaxSettings.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { FormProvider } from 'react-hook-form'; +import { FormProvider, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; @@ -44,8 +44,8 @@ const TaxSettingsPage = () => { const ratesValue = taxSettingsQuery.data?.rates?.length ? taxSettingsQuery.data.rates : form.getValues('rates'); - const activeCountry = form.watch('active_country'); - const formData = form.watch(); + const activeCountry = useWatch({ control: form.control, name: 'active_country' }); + const formData = useWatch({ control: form.control }); useEffect(() => { if (form.formState.isDirty) { @@ -56,22 +56,24 @@ const TaxSettingsPage = () => { useEffect(() => { if (taxSettingsQuery.data) { const taxData = taxSettingsQuery.data; - - taxData.rates = taxData.rates.map((rate) => { + const formattedRates = taxData.rates.map((rate) => { if (rate.is_same_rate && !rate.states.length) { - rate.states = - getCountryByCode(rate.country)?.states?.map((state) => ({ - id: state.id, - rate: 0, - apply_on_shipping: false, - })) || []; + return { + ...rate, + states: + getCountryByCode(rate.country)?.states?.map((state) => ({ + id: state.id, + rate: 0, + apply_on_shipping: false, + })) || [], + }; } return rate; }); reset({ enable_tax: isDefined(taxData.enable_tax) ? taxData.enable_tax : true, - rates: taxData.rates ?? [], + rates: formattedRates, apply_tax_on: taxData.apply_tax_on ?? 'product', active_country: taxData.active_country || null, show_price_with_tax: isDefined(taxData.show_price_with_tax) ? taxData.show_price_with_tax : false, diff --git a/assets/src/js/v3/entries/tax-settings/components/modals/CountrySelectModal.tsx b/assets/src/js/v3/entries/tax-settings/components/modals/CountrySelectModal.tsx index d637392aff..9e0f8105a7 100644 --- a/assets/src/js/v3/entries/tax-settings/components/modals/CountrySelectModal.tsx +++ b/assets/src/js/v3/entries/tax-settings/components/modals/CountrySelectModal.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Controller, type UseFormReturn } from 'react-hook-form'; +import { Controller, type UseFormReturn, useWatch } from 'react-hook-form'; import { css } from '@emotion/react'; import { __, sprintf } from '@wordpress/i18n'; @@ -44,9 +44,11 @@ const CountrySelectModal = ({ form, closeModal, title }: CountrySelectModalProps defaultValues: { searchValue: '', selectedCountries: {}, activeCountry: '' }, }); - const selectedCountries = regionStateForm.watch('selectedCountries'); - const activeCountry = regionStateForm.watch('activeCountry'); - const searchValue = regionStateForm.watch('searchValue'); + const watchedSelectedCountries = useWatch({ control: regionStateForm.control, name: 'selectedCountries' }); + const selectedCountries = watchedSelectedCountries ?? {}; + const activeCountry = useWatch({ control: regionStateForm.control, name: 'activeCountry' }); + const watchedSearchValue = useWatch({ control: regionStateForm.control, name: 'searchValue' }); + const searchValue = watchedSearchValue ?? ''; let countries = getCountryListAsOptions([]); diff --git a/assets/src/js/v3/entries/tax-settings/components/modals/CountryTaxRateModal.tsx b/assets/src/js/v3/entries/tax-settings/components/modals/CountryTaxRateModal.tsx index 06fcb91a8d..3c40718c7f 100644 --- a/assets/src/js/v3/entries/tax-settings/components/modals/CountryTaxRateModal.tsx +++ b/assets/src/js/v3/entries/tax-settings/components/modals/CountryTaxRateModal.tsx @@ -34,7 +34,7 @@ const CountryTaxRateModal = ({ form, closeModal, title }: CountryTaxRateModalPro taxRate: 0, }, }); - const activeCountryCode = form.watch('active_country'); + const activeCountryCode = form.getValues('active_country'); const activeCountryRate = form.getValues('rates').find((rate) => String(rate.country) === String(activeCountryCode)); const isEU = isEuropeanUnion(activeCountryCode ?? ''); @@ -82,15 +82,10 @@ const CountryTaxRateModal = ({ form, closeModal, title }: CountryTaxRateModalPro rules={requiredRule()} render={(controllerProps) => { const activeCountryStates = activeCountryRate?.states.map((state) => `${state.id}`); - let options = []; - - if (isEU) { - options = europeanUnionData.states.map((state) => ({ label: state.name, value: state.numeric_code })); - } else { - options = getStatesByCountryAsOptions(activeCountryCode ?? ''); - } - - options = options.filter((option) => !activeCountryStates?.includes(option.value)); + const baseOptions = isEU + ? europeanUnionData.states.map((state) => ({ label: state.name, value: state.numeric_code })) + : getStatesByCountryAsOptions(activeCountryCode ?? ''); + const options = baseOptions.filter((option) => !activeCountryStates?.includes(option.value)); return ( 0.01 ? (() => { const hoverCell = hoverTargetRef.current; @@ -928,6 +930,7 @@ const FormCoordinates = ({ field, activeQuestionIndex = 0, axisRangeControllerPr ); })() : null} + {/* eslint-enable react-hooks/refs */}
{axisRangeControllerProps && ( diff --git a/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx b/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx index eb35cae2dd..245e4c3200 100644 --- a/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx +++ b/assets/src/js/v3/shared/components/fields/quiz/questions/FormDrawImage.tsx @@ -56,7 +56,7 @@ const FormDrawImage = ({ field, precisionControllerProps, activeQuestionIndex = ? (`questions.${activeQuestionIndex}._data_status` as const) : ('_data_status' as const); const activeQuestionDataStatus = form - ? ((form.watch(resolvedQuestionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) + ? ((form.getValues(resolvedQuestionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) : QuizDataStatus.NO_CHANGE; const a11yBaseId = useId(); diff --git a/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx b/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx index 4adce21362..4e08133b19 100644 --- a/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx +++ b/assets/src/js/v3/shared/components/fields/quiz/questions/FormPuzzle.tsx @@ -51,7 +51,7 @@ const FormPuzzle = ({ ? (`questions.${activeQuestionIndex}._data_status` as const) : ('_data_status' as const); const activeQuestionDataStatus = form - ? ((form.watch(resolvedQuestionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) + ? ((form.getValues(resolvedQuestionDataStatusPath) as QuizDataStatus | undefined) ?? QuizDataStatus.NO_CHANGE) : QuizDataStatus.NO_CHANGE; const gridSizeOptions = useMemo( () => diff --git a/assets/src/js/v3/shared/components/subscription/OfferSalePrice.tsx b/assets/src/js/v3/shared/components/subscription/OfferSalePrice.tsx index db7a274393..39ee2a9928 100644 --- a/assets/src/js/v3/shared/components/subscription/OfferSalePrice.tsx +++ b/assets/src/js/v3/shared/components/subscription/OfferSalePrice.tsx @@ -23,6 +23,7 @@ export function OfferSalePrice() { const hasSale = useWatch({ control: form.control, name: 'offer_sale_price' }); const regularPrice = useWatch({ control: form.control, name: 'regular_price' }); const hasScheduledSale = !!useWatch({ control: form.control, name: 'schedule_sale_price' }); + const salePriceFromDate = useWatch({ control: form.control, name: 'sale_price_from_date' }); return (
@@ -127,7 +128,7 @@ export function OfferSalePrice() { {...controllerProps} isClearable={false} placeholder="yyyy-mm-dd" - disabledBefore={form.watch('sale_price_from_date') || undefined} + disabledBefore={salePriceFromDate || undefined} /> )} /> From 738d6f7613728e791b0356f9a1c91c1fa5ed9655 Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Sat, 31 Oct 2026 14:11:29 +0600 Subject: [PATCH 8/9] refactor(react-compiler): simplify useWatch calls and eliminate redundant intermediate variables --- .../components/curriculum/GenerateQuizWithAi.tsx | 6 ++---- .../components/curriculum/QuestionForm.tsx | 3 +-- .../components/curriculum/QuestionList.tsx | 3 +-- .../components/curriculum/QuizSettings.tsx | 13 +++++-------- .../course-builder/contexts/QuizModalContext.tsx | 3 +-- .../components/modals/CollectionList/index.tsx | 3 +-- .../import-export-states/ImportInitialState.tsx | 6 ++---- .../payment-settings/components/PaymentMethods.tsx | 5 +++-- .../tax-settings/components/EuropeanUnionTax.tsx | 3 +-- .../v3/entries/tax-settings/components/TaxRates.tsx | 3 +-- .../components/modals/CountrySelectModal.tsx | 10 ++++++---- .../components/fields/FormCategoriesInput.tsx | 3 +-- 12 files changed, 25 insertions(+), 36 deletions(-) diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/GenerateQuizWithAi.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/GenerateQuizWithAi.tsx index 6dae9151a8..6c451a14b3 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/GenerateQuizWithAi.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/GenerateQuizWithAi.tsx @@ -118,11 +118,9 @@ const GenerateQuizWithAi = () => { ]; }, [topicId, topics]); - const watchedTopicIds = useWatch({ control: form.control, name: 'topic_ids' }); - const selectedTopicIds = watchedTopicIds ?? emptyTopicIds; + const selectedTopicIds = useWatch({ control: form.control, name: 'topic_ids', defaultValue: emptyTopicIds }); const questionTypes = useWatch({ control: form.control, name: 'question_types' }); - const watchedQuestionCount = useWatch({ control: form.control, name: 'question_count' }); - const questionCount = Number(watchedQuestionCount); + const questionCount = Number(useWatch({ control: form.control, name: 'question_count' })); const difficulty_level = useWatch({ control: form.control, name: 'difficulty_level' }); const selectedQuestionTypes = questionTypeOptions .filter((option) => questionTypes?.[option.value]) diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionForm.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionForm.tsx index 76470d4d09..c2e7552fe8 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionForm.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionForm.tsx @@ -49,8 +49,7 @@ const QuestionForm = () => { control: form.control, name: `questions.${activeQuestionIndex}.question_type` as 'questions.0.question_type', }); - const watchedQuestions = useWatch({ control: form.control, name: 'questions' }); - const questions = watchedQuestions || []; + const questions = useWatch({ control: form.control, name: 'questions', defaultValue: [] }); const dataStatus = useWatch({ control: form.control, name: `questions.${activeQuestionIndex}._data_status` as 'questions.0._data_status', diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx index 0931426001..003e424b8d 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuestionList.tsx @@ -103,8 +103,7 @@ const QuestionList = ({ isEditing }: { isEditing: boolean }) => { return questionFields.find((item) => item.question_id === activeSortId); }, [activeSortId, questionFields]); - const watchedQuestions = useWatch({ control: form.control, name: 'questions' }); - const questions = watchedQuestions || []; + const questions = useWatch({ control: form.control, name: 'questions', defaultValue: [] }); const activeQuestion = form.getValues(`questions.${activeQuestionIndex}` as 'questions.0'); const handleAddQuestion = (questionType: QuizQuestionType, content?: H5PContent) => { diff --git a/assets/src/js/v3/entries/course-builder/components/curriculum/QuizSettings.tsx b/assets/src/js/v3/entries/course-builder/components/curriculum/QuizSettings.tsx index 51da7f7890..07d9bc2d61 100644 --- a/assets/src/js/v3/entries/course-builder/components/curriculum/QuizSettings.tsx +++ b/assets/src/js/v3/entries/course-builder/components/curriculum/QuizSettings.tsx @@ -103,8 +103,7 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { const negativeMarkType = useWatch({ control: form.control, name: 'quiz_option.negative_mark_type' }); const negativeMarkingEnabled = useWatch({ control: form.control, name: 'quiz_option.enable_negative_marking' }); - const watchedQuestions = useWatch({ control: form.control, name: 'questions' }); - const questions = watchedQuestions || []; + const questions = useWatch({ control: form.control, name: 'questions', defaultValue: [] }); const questionsCount = questions.length; const hasOpenEndedQuestions = questions.some((question) => question.question_type === 'open_ended'); const hasShortAnswerQuestions = questions.some((question) => question.question_type === 'short_answer'); @@ -146,8 +145,7 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { })(); const questionsForStats = hasQuestionLimit ? orderedQuestions.slice(0, availableQuestionInPool) : orderedQuestions; const questionCountForStats = questionsForStats.length; - const passingGradeValue = useWatch({ control: form.control, name: 'quiz_option.passing_grade' }); - const passingGrade = Number(passingGradeValue); + const passingGrade = Number(useWatch({ control: form.control, name: 'quiz_option.passing_grade' })); const totalMarksForStats = questionsForStats.reduce( (sum, question) => sum + Number(question.question_settings.question_mark || 0), 0, @@ -172,8 +170,7 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => { return questionCountForStats; })(); - const timeLimitVal = useWatch({ control: form.control, name: 'quiz_option.time_limit.time_value' }); - const timeLimitValue = Number(timeLimitVal); + const timeLimitValue = Number(useWatch({ control: form.control, name: 'quiz_option.time_limit.time_value' })); const timeLimitType = useWatch({ control: form.control, name: 'quiz_option.time_limit.time_type' }); const quizAutoStart = useWatch({ control: form.control, name: 'quiz_option.quiz_auto_start' }); const questionLayoutView = useWatch({ control: form.control, name: 'quiz_option.question_layout_view' }); @@ -882,7 +879,7 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => {
- {timeLimitVal} {timeLimitType} + {timeLimitValue} {timeLimitType}
{__('Time limit', 'tutor')}
@@ -895,7 +892,7 @@ const QuizSettings = ({ contentDripType }: QuizSettingsProps) => {
- {passingGradeValue} + {passingGrade} {__('%', 'tutor')}
{__('Passing grade', 'tutor')}
diff --git a/assets/src/js/v3/entries/course-builder/contexts/QuizModalContext.tsx b/assets/src/js/v3/entries/course-builder/contexts/QuizModalContext.tsx index 3cc208c617..78a0c66d3b 100644 --- a/assets/src/js/v3/entries/course-builder/contexts/QuizModalContext.tsx +++ b/assets/src/js/v3/entries/course-builder/contexts/QuizModalContext.tsx @@ -76,8 +76,7 @@ export const QuizModalContextProvider = ({ }) => { const [activeQuestionId, setActiveQuestionId] = useState(''); const form = useFormContext(); - const watchedQuestions = useWatch({ control: form.control, name: 'questions' }); - const questions = watchedQuestions || []; + const questions = useWatch({ control: form.control, name: 'questions', defaultValue: [] }); const previousQuestions = useRef(questions); const [validationError, setValidationError] = useState<{ message: string; diff --git a/assets/src/js/v3/entries/import-export/components/modals/CollectionList/index.tsx b/assets/src/js/v3/entries/import-export/components/modals/CollectionList/index.tsx index d5b3fc224a..8c789d3efc 100644 --- a/assets/src/js/v3/entries/import-export/components/modals/CollectionList/index.tsx +++ b/assets/src/js/v3/entries/import-export/components/modals/CollectionList/index.tsx @@ -31,8 +31,7 @@ const CollectionListModal = ({ closeModal, actions, form }: CollectionListModalP }, }); - const watchedItems = useWatch({ control: _form.control, name: 'content_bank' }); - const selectedItems = (watchedItems as Collection[]) || []; + const selectedItems = useWatch({ control: _form.control, name: 'content_bank', defaultValue: [] }) as Collection[]; const handleAddContent = () => { const selectedItems = _form.getValues('content_bank') || []; diff --git a/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ImportInitialState.tsx b/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ImportInitialState.tsx index d45dfa3b8e..3197b50cc0 100644 --- a/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ImportInitialState.tsx +++ b/assets/src/js/v3/entries/import-export/components/modals/import-export-states/ImportInitialState.tsx @@ -90,8 +90,7 @@ const ImportInitialState = ({ files: propsFiles, currentStep, onClose, onImport collectionId: '', }, }); - const watchedSearchTerm = useWatch({ control: form.control, name: 'collectionSearch' }); - const searchTerm = watchedSearchTerm ?? ''; + const searchTerm = useWatch({ control: form.control, name: 'collectionSearch', defaultValue: '' }); const search = useDebounce(searchTerm, 300); const isContentBankSelectionEnabled = useWatch({ control: form.control, name: 'importIntoContentBank' }); @@ -105,8 +104,7 @@ const ImportInitialState = ({ files: propsFiles, currentStep, onClose, onImport isEnabled: !!isContentBankSelectionEnabled, }); - const watchedFiles = useWatch({ control: form.control, name: 'files' }); - const files = watchedFiles ?? []; + const files = useWatch({ control: form.control, name: 'files', defaultValue: [] }); useEffect(() => { if (files.length === 0) { diff --git a/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx b/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx index 84f8fa0ab1..f864aedf2c 100644 --- a/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx +++ b/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx @@ -30,8 +30,9 @@ const emptyPaymentMethods: PaymentSettings['payment_methods'] = []; const PaymentMethods = () => { const form = useFormContext(); - const watchedPaymentMethods = useWatch({ control: form.control, name: 'payment_methods' }); - const paymentMethods = watchedPaymentMethods ?? emptyPaymentMethods; + const paymentMethods = + useWatch({ control: form.control, name: 'payment_methods', defaultValue: emptyPaymentMethods }) ?? + emptyPaymentMethods; const [activeSortId, setActiveSortId] = useState(null); diff --git a/assets/src/js/v3/entries/tax-settings/components/EuropeanUnionTax.tsx b/assets/src/js/v3/entries/tax-settings/components/EuropeanUnionTax.tsx index 50f4a813b9..7cc551c9b3 100644 --- a/assets/src/js/v3/entries/tax-settings/components/EuropeanUnionTax.tsx +++ b/assets/src/js/v3/entries/tax-settings/components/EuropeanUnionTax.tsx @@ -27,8 +27,7 @@ interface ColumnDataType { function EuropeanUnionTax() { const { openCountryTaxRateModal } = useCountryTaxRateModal(); const form = useFormContext(); - const watchedRates = useWatch({ control: form.control, name: 'rates' }); - const rates = watchedRates ?? []; + const rates = useWatch({ control: form.control, name: 'rates', defaultValue: [] }); const euIndex = rates.findIndex((rate) => String(rate.country) === String(euCountryCode)); const euRate = rates[euIndex]; diff --git a/assets/src/js/v3/entries/tax-settings/components/TaxRates.tsx b/assets/src/js/v3/entries/tax-settings/components/TaxRates.tsx index 4477fac74d..cd7b74856b 100644 --- a/assets/src/js/v3/entries/tax-settings/components/TaxRates.tsx +++ b/assets/src/js/v3/entries/tax-settings/components/TaxRates.tsx @@ -35,8 +35,7 @@ export default function TaxRates() { const form = useFormContext(); const { openCountrySelectModal } = useCountrySelectModal(); const { openCountryTaxRateModal } = useCountryTaxRateModal(); - const watchedRates = useWatch({ control: form.control, name: 'rates' }); - const rates = watchedRates ?? []; + const rates = useWatch({ control: form.control, name: 'rates', defaultValue: [] }); const activeCountry = useWatch({ control: form.control, name: 'active_country' }); const enableTax = useWatch({ control: form.control, name: 'enable_tax' }); const activeCountryIndex = rates.findIndex((rate) => String(rate.country) === String(activeCountry)); diff --git a/assets/src/js/v3/entries/tax-settings/components/modals/CountrySelectModal.tsx b/assets/src/js/v3/entries/tax-settings/components/modals/CountrySelectModal.tsx index 9e0f8105a7..828cb6209b 100644 --- a/assets/src/js/v3/entries/tax-settings/components/modals/CountrySelectModal.tsx +++ b/assets/src/js/v3/entries/tax-settings/components/modals/CountrySelectModal.tsx @@ -44,11 +44,13 @@ const CountrySelectModal = ({ form, closeModal, title }: CountrySelectModalProps defaultValues: { searchValue: '', selectedCountries: {}, activeCountry: '' }, }); - const watchedSelectedCountries = useWatch({ control: regionStateForm.control, name: 'selectedCountries' }); - const selectedCountries = watchedSelectedCountries ?? {}; + const selectedCountries = useWatch({ + control: regionStateForm.control, + name: 'selectedCountries', + defaultValue: {}, + }); const activeCountry = useWatch({ control: regionStateForm.control, name: 'activeCountry' }); - const watchedSearchValue = useWatch({ control: regionStateForm.control, name: 'searchValue' }); - const searchValue = watchedSearchValue ?? ''; + const searchValue = useWatch({ control: regionStateForm.control, name: 'searchValue', defaultValue: '' }); let countries = getCountryListAsOptions([]); diff --git a/assets/src/js/v3/shared/components/fields/FormCategoriesInput.tsx b/assets/src/js/v3/shared/components/fields/FormCategoriesInput.tsx index ca35730a79..7cfe2c1ceb 100644 --- a/assets/src/js/v3/shared/components/fields/FormCategoriesInput.tsx +++ b/assets/src/js/v3/shared/components/fields/FormCategoriesInput.tsx @@ -56,8 +56,7 @@ const FormMultiLevelInput = ({ }>({ shouldFocusError: true, }); - const search = useWatch({ control: form.control, name: 'search' }) ?? ''; - const searchValue = search; + const searchValue = useWatch({ control: form.control, name: 'search', defaultValue: '' }); const debouncedSearchValue = useDebounce(searchValue, 300); const categoryListQuery = useCategoryListQuery(debouncedSearchValue); const createCategoryMutation = useCreateCategoryMutation(); From 1a874720069327c290dce75b05a6c5a8aed0e9bc Mon Sep 17 00:00:00 2001 From: b-l-i-n-d Date: Sat, 31 Oct 2026 14:13:49 +0600 Subject: [PATCH 9/9] fix(payment-settings): wrap paymentMethods initialization in useMemo --- .../entries/payment-settings/components/PaymentMethods.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx b/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx index f864aedf2c..7a9576d995 100644 --- a/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx +++ b/assets/src/js/v3/entries/payment-settings/components/PaymentMethods.tsx @@ -26,13 +26,10 @@ import { moveTo } from '@TutorShared/utils/util'; import type { PaymentSettings } from '../services/payment'; import PaymentItem from './PaymentItem'; -const emptyPaymentMethods: PaymentSettings['payment_methods'] = []; - const PaymentMethods = () => { const form = useFormContext(); - const paymentMethods = - useWatch({ control: form.control, name: 'payment_methods', defaultValue: emptyPaymentMethods }) ?? - emptyPaymentMethods; + const watchedPaymentMethods = useWatch({ control: form.control, name: 'payment_methods' }); + const paymentMethods = useMemo(() => watchedPaymentMethods || [], [watchedPaymentMethods]); const [activeSortId, setActiveSortId] = useState(null);