Skip to content

Commit 6f6a864

Browse files
Fix dynamic select storybook test failure
- Refactored `AutoCalculationExample` story to separate form definition and consumption to stabilize `onChange` and avoid re-subscription loops. - Modified `useOnFormValueChange` hook to correctly track `prevValue` using `useRef` and optimize dependencies. - Updated `AutoCalculation` story to use `waitFor` instead of `setTimeout` to prevent CI race conditions. - Updated `selectRadixOption` test helper to blur the trigger after selection (click on body) to simulate "clicking off" and ensure onBlur events fire.
1 parent ca4f96a commit 6f6a864

File tree

2 files changed

+76
-72
lines changed

2 files changed

+76
-72
lines changed

apps/docs/src/remix-hook-form/use-on-form-value-change.stories.tsx

Lines changed: 71 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { expect, userEvent, within, waitFor } from '@storybook/test';
88
import { useState, useMemo, useCallback } from 'react';
99
import { useFetcher } from 'react-router';
1010
import { useRemixForm, RemixFormProvider, getValidatedFormData } from 'remix-hook-form';
11+
import { useFormContext } from 'react-hook-form';
1112
import { z } from 'zod';
1213
import type { ActionFunctionArgs } from 'react-router';
1314
import { selectRadixOption } from '../lib/storybook/test-utils';
@@ -219,105 +220,107 @@ const orderSchema = z.object({
219220

220221
type OrderFormData = z.infer<typeof orderSchema>;
221222

222-
const AutoCalculationExample = () => {
223-
const fetcher = useFetcher<{ message: string }>();
224-
225-
const rawMethods = useRemixForm<OrderFormData>({
226-
resolver: zodResolver(orderSchema),
227-
defaultValues: {
228-
quantity: '1',
229-
pricePerUnit: '100',
230-
discount: '0',
231-
total: '100.00',
232-
},
233-
fetcher,
234-
submitConfig: {
235-
action: '/',
236-
method: 'post',
237-
},
238-
});
239-
240-
// Memoize methods to prevent unnecessary re-renders of the story tree
241-
// which can disrupt interaction tests using Portals
242-
const methods = useMemo(() => rawMethods, [rawMethods]);
223+
// biome-ignore lint/suspicious/noExplicitAny: simple story example
224+
const AutoCalculationForm = ({ fetcher }: { fetcher: any }) => {
225+
const { setValue, getValues, handleSubmit } = useFormContext<OrderFormData>();
243226

244227
const calculateTotal = useCallback(() => {
245-
const quantity = Number.parseFloat(methods.getValues('quantity') || '0');
246-
const pricePerUnit = Number.parseFloat(methods.getValues('pricePerUnit') || '0');
247-
const discount = Number.parseFloat(methods.getValues('discount') || '0');
228+
const quantity = Number.parseFloat(getValues('quantity') || '0');
229+
const pricePerUnit = Number.parseFloat(getValues('pricePerUnit') || '0');
230+
const discount = Number.parseFloat(getValues('discount') || '0');
248231

249232
const subtotal = quantity * pricePerUnit;
250233
const total = subtotal - subtotal * (discount / 100);
251-
methods.setValue('total', total.toFixed(2));
252-
}, [methods]);
234+
setValue('total', total.toFixed(2));
235+
}, [setValue, getValues]);
253236

254237
// Recalculate when quantity changes
255238
useOnFormValueChange({
256239
name: 'quantity',
257-
methods,
258240
onChange: calculateTotal,
259241
});
260242

261243
// Recalculate when price changes
262244
useOnFormValueChange({
263245
name: 'pricePerUnit',
264-
methods,
265246
onChange: calculateTotal,
266247
});
267248

268249
// Recalculate when discount changes
269250
useOnFormValueChange({
270251
name: 'discount',
271-
methods,
272252
onChange: calculateTotal,
273253
});
274254

255+
return (
256+
<form onSubmit={handleSubmit} className="w-96">
257+
<div className="space-y-6">
258+
<TextField type="number" name="quantity" label="Quantity" description="Number of items" min={1} />
259+
260+
<TextField
261+
type="number"
262+
name="pricePerUnit"
263+
label="Price per Unit"
264+
description="Price for each item"
265+
prefix="$"
266+
min={0}
267+
step="0.01"
268+
/>
269+
270+
<TextField
271+
type="number"
272+
name="discount"
273+
label="Discount"
274+
description="Discount percentage (0-100)"
275+
suffix="%"
276+
min={0}
277+
max={100}
278+
/>
279+
280+
<TextField
281+
name="total"
282+
label="Total"
283+
description="Automatically calculated total"
284+
prefix="$"
285+
disabled
286+
className="font-semibold"
287+
/>
288+
289+
<Button type="submit" className="w-full">
290+
Submit Order
291+
</Button>
292+
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
293+
</div>
294+
</form>
295+
);
296+
};
297+
298+
const AutoCalculationExample = () => {
299+
const fetcher = useFetcher<{ message: string }>();
300+
301+
const methods = useRemixForm<OrderFormData>({
302+
resolver: zodResolver(orderSchema),
303+
defaultValues: {
304+
quantity: '1',
305+
pricePerUnit: '100',
306+
discount: '0',
307+
total: '100.00',
308+
},
309+
fetcher,
310+
submitConfig: {
311+
action: '/',
312+
method: 'post',
313+
},
314+
});
315+
275316
// Don't render if methods is not ready
276317
if (!methods || !methods.handleSubmit) {
277318
return <div>Loading...</div>;
278319
}
279320

280321
return (
281322
<RemixFormProvider {...methods}>
282-
<form onSubmit={methods.handleSubmit} className="w-96">
283-
<div className="space-y-6">
284-
<TextField type="number" name="quantity" label="Quantity" description="Number of items" min={1} />
285-
286-
<TextField
287-
type="number"
288-
name="pricePerUnit"
289-
label="Price per Unit"
290-
description="Price for each item"
291-
prefix="$"
292-
min={0}
293-
step="0.01"
294-
/>
295-
296-
<TextField
297-
type="number"
298-
name="discount"
299-
label="Discount"
300-
description="Discount percentage (0-100)"
301-
suffix="%"
302-
min={0}
303-
max={100}
304-
/>
305-
306-
<TextField
307-
name="total"
308-
label="Total"
309-
description="Automatically calculated total"
310-
prefix="$"
311-
disabled
312-
className="font-semibold"
313-
/>
314-
315-
<Button type="submit" className="w-full">
316-
Submit Order
317-
</Button>
318-
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
319-
</div>
320-
</form>
323+
<AutoCalculationForm fetcher={fetcher} />
321324
</RemixFormProvider>
322325
);
323326
};

packages/components/src/remix-hook-form/hooks/use-on-form-value-change.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,12 @@ export const useOnFormValueChange = <
8989
formMethods?.getValues ? formMethods.getValues(name) : undefined,
9090
);
9191

92+
const watch = formMethods?.watch;
93+
const getValues = formMethods?.getValues;
94+
9295
useEffect(() => {
9396
// Early return if no form methods are available or hook is disabled
94-
if (!enabled || !formMethods || !formMethods.watch || !formMethods.getValues) return;
95-
96-
const { watch } = formMethods;
97+
if (!enabled || !watch || !getValues) return;
9798

9899
// Subscribe to the field value changes
99100
const subscription = watch(((value, { name: changedFieldName }) => {
@@ -113,5 +114,5 @@ export const useOnFormValueChange = <
113114
// Cleanup subscription on unmount
114115

115116
return () => subscription.unsubscribe();
116-
}, [name, onChange, enabled, formMethods]);
117+
}, [name, onChange, enabled, watch, getValues]);
117118
};

0 commit comments

Comments
 (0)