React India 2022 ⚛️ 🇮🇳
Twitter, GitHub: | @blenderskool |
Website: | akashhamirwasia.com |
const Form = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<Card>
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
</Card>
);
};
const Form = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<Card>
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
</Card>
);
};
const Form = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = () => {
// Use values of 'email' and 'password' here
};
return (
<Card>
<form onSubmit={$handleLogin$}>
{/* ... */}
</form>
</Card>
);
};
It’s bad!
Gets even worse with more components!
I think people control inputs more than they need to. Use uncontrolled unless you actually need to control the value of the input 🤷♂️
3:21 AM · Sep 24, 2019 · Twitter Web App
const Form = () => {
const handleLogin = (e) => {
const formData = new FormData(e.target);
// Use 'formData' to access values
};
return (
<Card>
<form onSubmit={handleLogin}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
</Card>
);
};
const Form = () => {
const handleLogin = (e) => {
const formData = new FormData(e.target);
// Use 'formData' to access values
};
return (
<Card>
<form onSubmit={handleLogin}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
</Card>
);
};
const Form = () => {
const handleLogin = (e) => {
const formData = new FormData(e.target);
const values = Object.fromEntries(
formData.entries()
);
// Use 'values' to access values
};
return (
<Card>
<form onSubmit={handleLogin}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
</Card>
);
};
Even though it’s not the most React way of doing things, it works very well for most forms.
Sadly, it has various drawbacks for “serious” forms 😕
aka. Cause state updates only when you need, use Uncontrolled components otherwise.
Let's build a simple API around this idea.
aka. Let's overengineer this 😛
const Form = () => {
const fields = useRef({}).current;
return (
<form>
<input
type="email"
name="email"
ref={(el) => $fields['email'] = el$}
/>
<input
type="password"
name="password"
ref={(el) => $fields['password'] = el$}
/>
</form>
);
};
const Form = () => {
const fields = useRef({}).current;
return (
<form>
<input
type="email"
name="email"
ref={(el) => fields['email'] = el}
$onChange$={() => {
/**
* trigger render if we are listening for 'email' input
*/
}}
/>
<input
type="password"
name="password"
ref={(el) => fields['password'] = el}
$onChange$={() => {
/**
* trigger render if we are listening for 'password' input
*/
}}
/>
</form>
);
};
const Form = () => {
const fields = useRef({}).current;
const handleChange = (e) => {
const name = e.target.name;
/**
* trigger render if we are listening for input with name 'name'
*/
}
return (
<form>
<input
type="email"
name="email"
ref={(el) => fields['email'] = el}
onChange={$handleChange$}
/>
<input
type="password"
name="password"
ref={(el) => fields['password'] = el}
onChange={$handleChange$}
/>
</form>
);
};
const $useForm$ = () => {
const fields = useRef({}).current;
const handleChange = (e) => {
const name = e.target.name;
/**
* trigger render if we are listening for input with name 'name'
*/
}
const register = (name) => ({
name,
ref: (el) => fields[name] = el,
onChange: handleChange
});
return { register };
}
...
...
const $Form$ = () => {
const { register } = useForm();
return (
<form>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
</form>
);
}
...
const Form = () => {
const { register } = useForm();
return (
<form>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
<input type="password" {...register("confirmPassword")} />
</form>
);
}
const Form = () => {
const { register } = useForm();
return (
<form>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
<input type="password" {...register("confirmPassword")} />
</form>
);
}
const Form = () => {
const { register, $getValues$ } = useForm();
const handleSignup = () => {
const values = $getValues$();
/* use values as needed */
}
return (
<form onSubmit={$handleSignup$}>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
<input type="password" {...register("confirmPassword")} />
</form>
);
}
const Form = () => {
const { register, getValues, $watch$ } = useForm();
const handleLogin = () => {
const values = getValues();
/* use values as needed */
}
return (
<form onSubmit={handleLogin}>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
<input type="password" {...register("confirmPassword")} />
{watch("password") !== watch("confirmPassword") && "Passwords don't match!"}
</form>
);
}
const Form = () => {
const { register, getValues, $watch$ } = useForm();
const handleLogin = () => {
const values = getValues();
/* use values as needed */
}
return (
<form onSubmit={handleLogin}>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
<input type="password" {...register("confirmPassword")} />
<input type="checkbox" {...register("checkPasswordMatch")} />
{
watch("checkPasswordMatch") ? (
watch("password") !== watch("confirmPassword") && "Passwords don't match!"
) : null
}
</form>
);
}
const useForm = () => {
const fields = useRef({}).current;
const handleChange = (e) => {
const name = e.target.name;
// TODO
}
const getValues = () => {
const values = Object.entries(fields).map(([key, el]) => [key, el.value])
return Object.fromEntries(values);
}
...
return { register, $getValues$ };
}
const useForm = () => {
const fields = useRef({}).current;
const watched = new Set();
const handleChange = (e) => {
const name = e.target.name;
if (watched.has(name)) {
// Trigger re-render
}
}
// Return the current input value, subscribe for future
const watch = (name) => {
watched.add(name);
return fields[name]?.value ?? '';
}
...
return { register, getValues, $watch$ };
}
const useForm = () => {
const fields = useRef({}).current;
const watched = new Set();
const [,forceRender] = useState(0);
const handleChange = (e) => {
const name = e.target.name;
if (watched.has(name)) {
forceRender(x => x + 1);
}
}
const watch = (name) => {
watched.add(name);
return fields[name]?.value ?? '';
}
...
return { register, getValues, $watch$ };
}
const Form = () => {
const { register, getValues, watch } = useForm();
const handleSignup = () => {
const values = getValues();
}
const $isInvalid$ = watch('checkPasswordMatch') &&
watch('password') !== watch('confirmPassword');
return (
<form onSubmit={handleSignup}>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
<input type="password" {...register("confirmPassword")} />
<input type="checkbox" {...register("checkPasswordMatch")} />
{$isInvalid$ && "Passwords don't match!"}
</form>
);
}
Sadly, we are still over-rendering!
Because we render on input change, not on derived data change.
const Form = () => {
const { register, getValues, watch } = useForm({
$derived$: {
isInvalid: (values) => values.password !== values.confirmPassword
}
});
return (
<form onSubmit={handleSignup}>
...
{ watch("isInvalid") && "Passwords don't match!" }
</form>
);
}
const useForm = ({ $derived$ }) => {
...
const $getDerived$ = () => {
return Object.fromEntries(
Object
.entries(derived)
.map(([name, fn]) => [name, fn(getValues())])
);
};
const $memoized$ = useRef(getDerived());
const $isDerivedDifferent$ = (nextDerived) => {
return Object.keys(derived).some(
(name) => watched.has(name) && nextDerived[name] !== memoized.current[name]
);
};
...
return { register, getValues, $getDerived$, watch };
}
const useForm = ({ $derived$ }) => {
...
const getDerived = () => {...};
const memoized = useRef(getDerived());
const isDerivedDifferent = (nextDerived) => {...};
const handleChange = (e) => {
const name = e.target.name;
const nextDerived = getDerived();
if (watched.has(name) || isDerivedDifferent(nextDerived)) {
forceRender((x) => x + 1);
}
memoized.current = nextDerived;
};
const watch = (name) => {
watched.add(name);
return fields[name]?.value ?? memoized.current[name] ?? "";
};
...
}
const Form = () => {
const { register, getValues, watch } = useForm({
$derived$: {
isInvalid: (values) =>
values.password !== values.confirmPassword
}
});
return (
<form onSubmit={handleSignup}>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
<input type="password" {...register("confirmPassword")} />
{watch("isInvalid") && "Passwords don't match!"}
</form>
);
}
Remember: Use this only if you need to!
You don't have to implement useForm(), because it's already available as a library.
npm i react-hook-form
import { useForm } from 'react-hook-form';
const Form = () => {
const { register, getValues, watch } = useForm();
const handleSignup = () => {
const values = getValues();
}
return (
<form onSubmit={handleSignup}>
<input type="email" {...register("email")} />
<input type="password" {...register("password")} />
<input type="password" {...register("confirmPassword")} />
{watch("password") !== watch("confirmPassword") && "Passwords don't match!"}
</form>
);
}
You'll get to learn some awesome stuff!
Twitter, GitHub: | @blenderskool |
LinkedIn: | linkedin.com/in/akash-hamirwasia |
Email: | [email protected] |
Website: | akashhamirwasia.com |