1. Custom Hooks and Component Composition
Implement reusable custom hooks and component composition patterns for better code organization and maintainability. This approach demonstrates modern React patterns including hook composition, render props, and TypeScript integration.
react-hooks-composition.mdc
import { useState, useEffect, useCallback, ReactNode } from 'react';
// Generic async state hook with TypeScript
interface AsyncState {
data: T | null;
loading: boolean;
error: Error | null;
}
function useAsync(asyncFn: () => Promise, deps: any[] = []): AsyncState {
const [state, setState] = useState>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let mounted = true;
const fetchData = async () => {
setState(prev => ({ ...prev, loading: true }));
try {
const result = await asyncFn();
if (mounted) {
setState({ data: result, loading: false, error: null });
}
} catch (error) {
if (mounted) {
setState({ data: null, loading: false, error });
}
}
};
fetchData();
return () => { mounted = false; };
}, deps);
return state;
}
// Composable form hook with validation
interface FormField {
value: T;
error?: string;
touched: boolean;
}
type FormState = {
[K in keyof T]: FormField;
};
interface UseFormOptions {
initialValues: T;
validate?: (values: T) => Partial>;
onSubmit: (values: T) => Promise;
}
function useForm>({
initialValues,
validate,
onSubmit,
}: UseFormOptions) {
const [formState, setFormState] = useState>(() =>
Object.keys(initialValues).reduce((acc, key) => ({
...acc,
[key]: {
value: initialValues[key],
touched: false,
},
}), {} as FormState)
);
const handleChange = useCallback((name: keyof T, value: T[keyof T]) => {
setFormState(prev => ({
...prev,
[name]: {
...prev[name],
value,
touched: true,
},
}));
}, []);
const handleSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault();
if (validate) {
const values = Object.keys(formState).reduce((acc, key) => ({
...acc,
[key]: formState[key].value,
}), {} as T);
const errors = validate(values);
if (Object.keys(errors).length > 0) {
setFormState(prev =>
Object.keys(prev).reduce((acc, key) => ({
...acc,
[key]: {
...prev[key],
error: errors[key],
touched: true,
},
}), {} as FormState)
);
return;
}
}
await onSubmit(
Object.keys(formState).reduce((acc, key) => ({
...acc,
[key]: formState[key].value,
}), {} as T)
);
}, [formState, validate, onSubmit]);
return { formState, handleChange, handleSubmit };
}
// Example usage with TypeScript and composition
interface User {
name: string;
email: string;
age: number;
}
interface DataProviderProps {
children: (state: AsyncState) => ReactNode;
fetchData: () => Promise;
}
function DataProvider({ children, fetchData }: DataProviderProps) {
const state = useAsync(fetchData);
return <>{children(state)}>;
}
function UserForm() {
const { formState, handleChange, handleSubmit } = useForm({
initialValues: {
name: '',
email: '',
age: 0,
},
validate: (values) => {
const errors: Partial> = {};
if (!values.name) errors.name = 'Name is required';
if (!values.email) errors.email = 'Email is required';
if (values.age < 18) errors.age = 'Must be 18 or older';
return errors;
},
onSubmit: async (values) => {
// Submit logic here
console.log(values);
},
});
return (
);
}
Why This Rule Matters
- Promotes code reusability through custom hooks
- Ensures type safety with TypeScript integration
- Improves component organization and maintainability
- Reduces code duplication
- Makes testing easier with isolated logic
Using with Cursor
Cursor's AI capabilities can help you:
- Generate custom hooks based on usage patterns
- Suggest component composition strategies
- Identify opportunities for hook extraction
- Debug React hooks dependencies
- Convert class components to functional components with hooks