Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions apps/docs/src/remix-hook-form/context-debug-comparison.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Textarea } from '@lambdacurry/forms/remix-hook-form/textarea';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryObj } from '@storybook/react-vite';
import * as React from 'react';
import { useEffect } from 'react';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm, useRemixFormContext } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';

const formSchema = z.object({
message: z.string().min(10, 'Message must be at least 10 characters'),
});

type FormData = z.infer<typeof formSchema>;

// Context monitoring component
const ContextMonitor = ({ label }: { label: string }) => {
const [status, setStatus] = React.useState<string>('Checking...');
const context = useRemixFormContext();

useEffect(() => {
try {
if (context) {
setStatus(`βœ… ${label}: Context OK`);
console.log(`[${label}] Context available:`, context);
} else {
setStatus(`❌ ${label}: Context NULL`);
console.log(`[${label}] Context is null:`, context);
}
} catch (error) {
setStatus(`🚨 ${label}: Error - ${error instanceof Error ? error.message : 'Unknown error'}`);
console.error(`[${label}] Context error:`, error);
}
}, [label, context]);

return (
<div
className="text-xs p-1 border rounded mb-2"
style={{
backgroundColor: status.includes('βœ…') ? '#d4edda' : status.includes('❌') ? '#f8d7da' : '#fff3cd',
}}
>
{status}
</div>
);
};

// Working pattern (like textarea-custom)
const WorkingPatternExample = () => {
const fetcher = useFetcher<{ message: string }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: { message: '' },
fetcher,
submitConfig: { action: '/', method: 'post' },
});

const messageId = React.useId();

return (
<RemixFormProvider {...methods}>
<div className="border p-4 rounded">
<h3 className="font-bold text-green-700 mb-2">βœ… Working Pattern (Direct DOM)</h3>
<ContextMonitor label="Working Pattern" />

<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="space-y-2">
<label htmlFor={`message-${messageId}`} className="text-sm font-medium">
Message (Working)
</label>
<textarea
id={`message-${messageId}`}
{...methods.register('message')}
placeholder="Enter your message here..."
className="w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm min-h-[80px]"
/>
{methods.formState.errors.message && (
<p className="text-sm font-medium text-destructive">{methods.formState.errors.message.message}</p>
)}
</div>
<Button type="submit" className="mt-4">
Submit Working
</Button>
</fetcher.Form>
</div>
</RemixFormProvider>
);
};

// Broken pattern (using component)
const BrokenPatternExample = () => {
const fetcher = useFetcher<{ message: string }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: { message: '' },
fetcher,
submitConfig: { action: '/', method: 'post' },
});

return (
<RemixFormProvider {...methods}>
<div className="border p-4 rounded">
<h3 className="font-bold text-red-700 mb-2">❌ Broken Pattern (Component)</h3>
<ContextMonitor label="Broken Pattern" />

<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="space-y-4">
<Textarea name="message" label="Message (Broken)" placeholder="Enter your message here..." rows={3} />
</div>
<Button type="submit" className="mt-4">
Submit Broken
</Button>
</fetcher.Form>
</div>
</RemixFormProvider>
);
};

// Side-by-side comparison
const ComparisonExample = () => {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-6xl">
<WorkingPatternExample />
<BrokenPatternExample />

<div className="lg:col-span-2 mt-4 p-4 bg-gray-50 rounded">
<h4 className="font-bold mb-2">πŸ” Debug Information</h4>
<p className="text-sm text-gray-600">
Check the browser console for detailed context debugging information. The working pattern uses direct DOM
elements with explicit register(), while the broken pattern uses the Textarea component that calls
useRemixFormContext() internally.
</p>
</div>
</div>
);
};

const handleFormSubmission = async (request: Request) => {
const { data, errors } = await getValidatedFormData<FormData>(request, zodResolver(formSchema));
if (errors) return { errors };
return { message: 'Form submitted successfully' };
};

const meta: Meta = {
title: 'RemixHookForm/Context Debug Comparison',
parameters: { layout: 'centered' },
tags: ['autodocs'],
decorators: [
withReactRouterStubDecorator({
routes: [
{
path: '/',
Component: ComparisonExample,
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
},
],
}),
],
} satisfies Meta;

export default meta;
type Story = StoryObj<typeof meta>;

export const SideBySideComparison: Story = {
parameters: {
docs: {
description: {
story: `
### Context Debug Comparison

This story demonstrates the difference between working and broken patterns:

**Working Pattern (Left):**
- Uses direct DOM elements (\`<textarea>\`)
- Explicitly calls \`methods.register('message')\`
- Context is only used for \`handleSubmit\` and form state
- βœ… Works in both dev and static builds

**Broken Pattern (Right):**
- Uses the \`<Textarea>\` component from \`@lambdacurry/forms/remix-hook-form/textarea\`
- Component internally calls \`useRemixFormContext()\` to get \`control\`
- ❌ Fails when context is not available

**Debug Features:**
- Real-time context monitoring with visual indicators
- Console logging for detailed debugging
- Side-by-side comparison to see the difference

Check the browser console for detailed context availability information.
`,
},
},
},
render: () => <ComparisonExample />,
};
109 changes: 84 additions & 25 deletions apps/docs/src/remix-hook-form/text-field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { RemixFormProvider, getValidatedFormData, useRemixForm, useRemixFormContext } from 'remix-hook-form';
import { expect, userEvent } from 'storybook/test';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
import * as React from 'react';

const formSchema = z.object({
username: z.string().min(3, 'Username must be at least 3 characters'),
Expand All @@ -21,6 +22,52 @@ const INITIAL_USERNAME = 'test_user';
const USERNAME_TAKEN = 'test_user';
const USERNAME_TAKEN_ERROR = 'Username is already taken';

// Debug wrapper component to monitor context timing
const DebugWrapper = ({ children }: { children: React.ReactNode }) => {
const [mountTime] = React.useState(Date.now());
const [contextChecks, setContextChecks] = React.useState<string[]>([]);

React.useEffect(() => {
console.log('[DebugWrapper] Component mounted at:', new Date(mountTime).toISOString());

// Check context availability immediately and after delays
const checkContext = (delay: number) => {
setTimeout(() => {
try {
const context = useRemixFormContext();
const timestamp = Date.now() - mountTime;
const status = context ?
`βœ… Context available after ${timestamp}ms` :
`❌ Context null after ${timestamp}ms`;

setContextChecks(prev => [...prev, status]);
console.log(`[DebugWrapper] ${status}`, context);
} catch (error) {
const timestamp = Date.now() - mountTime;
const status = `🚨 Context error after ${timestamp}ms: ${error.message}`;
setContextChecks(prev => [...prev, status]);
console.error(`[DebugWrapper] ${status}`, error);
}
}, delay);
};

// Check at different intervals
[0, 100, 500, 1000, 2000].forEach(checkContext);
}, [mountTime]);

return (
<div>
<div className="fixed top-0 left-0 bg-blue-100 border border-blue-400 p-2 text-xs max-w-sm z-50">
<h4 className="font-bold">Context Timing Debug:</h4>
{contextChecks.map((check, index) => (
<div key={index} className="text-xs">{check}</div>
))}
</div>
{children}
</div>
);
};

const ControlledTextFieldExample = () => {
const fetcher = useFetcher<{ message: string; email: string }>();
const methods = useRemixForm<FormData>({
Expand All @@ -38,32 +85,44 @@ const ControlledTextFieldExample = () => {
},
});

// Debug form provider state
React.useEffect(() => {
console.log('[ControlledTextFieldExample] useRemixForm result:', {
handleSubmit: typeof methods.handleSubmit,
control: typeof methods.control,
formState: methods.formState,
register: typeof methods.register,
});
}, [methods]);

return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="space-y-6">
<TextField name="username" label="Username" description="Enter a unique username" />

<TextField name="price" label="Price" description="Enter the price" prefix="$" />

<TextField name="email" label="Email" description="Enter your email address" suffix="@example.com" />

<TextField
type="number"
name="measurement"
step={0.1}
label="Measurement"
description="Enter a measurement"
prefix="~"
suffix="cm"
/>

<Button type="submit" className="mt-4">
Submit
</Button>
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
</div>
</fetcher.Form>
<DebugWrapper>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="space-y-6">
<TextField name="username" label="Username" description="Enter a unique username" />

<TextField name="price" label="Price" description="Enter the price" prefix="$" />

<TextField name="email" label="Email" description="Enter your email address" suffix="@example.com" />

<TextField
type="number"
name="measurement"
step={0.1}
label="Measurement"
description="Enter a measurement"
prefix="~"
suffix="cm"
/>

<Button type="submit" className="mt-4">
Submit
</Button>
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
</div>
</fetcher.Form>
</DebugWrapper>
</RemixFormProvider>
);
};
Expand Down
Loading