[Tips] React Form Tips
Conditional Form Fields
- React reconciliation: how it works and why should we care @ Developer Way.
- Reconciliation @ React Docs
Conditional form fields are commonly used in forms, but if they are not handled properly, they can cause unexpected bugs.
Take this form as an example,
// https://github.com/pjchender/react-form-best-practice/commit/773337d60c856b5b7c7768554e39308c7a7b9de3
{
isBusinessUser ? (
<input
id="company-name"
placeholder="Apple. Inc"
// ...
/>
) : (
<input
id="personal-name"
placeholder="John Doe"
// ...
/>
);
}
Problem: why does the state remain unchanged even after rerendering the component?
7cb56ca @ GitHub
The term "rerendering" here refers only to the invoking of the render
method, not to "mount" and "remount".
The question here is why the state is preserved even after the component has been changed.
It's all about the "reconciliation" algorithm of React. In reconciliation, React compares the types of DOM elements such as img
, div
, span
. If the root elements differ in their types, React will create a completely new one from scratch instead of changing the existing one.
However, if the element types are the same, React analyzes the attributes of each. It maintains the same foundational DOM node and only modifies the attributes that have changed.
In the example above, since the element types are the same (i.e., input
), the instance remains unchanged when a component rerenders. This ensures that the state is preserved across different renders.
That's the reason why the state remains the same even after the React component has been rerendered.
Solve the problem
In order to ensure that the state is not retained by React, we have to make sure the component is unmounted and then remounted.
Solution 1: Keep the DOM but will return null
32f134 @ GitHub
{
isBusinessUser && <input id="company-name" placeholder="Apple. Inc" />;
}
{
!isBusinessUser && <input id="personal-name" placeholder="John Doe" />;
}
Solution 2: Use the "key" attribute to let React differentiate them
62d78c @ GitHub
{
watchIsBusiness ? (
<input
key="companyName"
id="company-name"
placeholder="Apple. Inc"
// ...
/>
) : (
<input
key="personalName"
id="personal-name"
placeholder="John Doe"
// ...
/>
);
}
Solution 3: Use "key" attributes to force React to render
62d78c @ GitHub
<div
key={watchIsBusiness ? 'Company Name' : 'Personal Name'}
>
{watchIsBusiness ? (
<input
id="company-name"
placeholder="Apple. Inc"
// ...
/>
) : (
<input
id="personal-name"
placeholder="John Doe"
// ...
/>
)}
</div>
If we want to learn more about this, check out the article React reconciliation: how it works and why should we care for detailed. The article also provides a Youtube video that explains this concept.
React Hook Form makes things weirder when not properly handling reconciliation
The behavior will become much weirder if you use React Hook Form but don't handle reconciliation properly.
It makes form behavior weird if you don't handle reconciliation properly
00ef0e @ GitHub
As you can see in the image, if we did not use the key
to handle reconciliation, the Personal Name "Aaron" is not preserved when we toggle the Personal Name field for the first time, which is not expected. However, once we toggle the checkbox, the value is maintained in the state as expected.
Key points when using react hook form
- Consider
useForm
as equivalent touseState
. This means all form values/state will be preserved within that component. - When using conditional form fields, ensure you handle reconciliation properly. This allows React to distinguish between different components.
- If you want the form behavior to resemble that of native browsers (uncontrolled input) closely, set
shouldUnregister
to true. In this case, react-hook-form will remove its field name and value when the component isn't displayed on the webpage.
Controlled and Uncontrolled components
Problem: A component is changing an uncontrolled input to be controlled
之所以會看到這個錯誤,一般是因為在 value 中使用了 undefined
,<input value={undefined} />
。
React 會把 value={undefined}
的 input 視為 uncontrolled 的;如果 value 有值(不是 undefined
)的話,表示這是 controlled input。
因此,如果一開始 render 時在 value
給的是 undefined
,後來卻又給值,這個 input 就會從 uncontrolled 變成 controlled。
舉例來說:
- 在
useForm
中一開始給的value
是undefined
- 當使用者輸入內容時,這個
value
會有值
這時候就會出現上述的錯誤
import { Controller, useForm } from 'react-hook-form';
type FormData = {
name: string;
};
export default function Form() {
const { handleSubmit, control } = useForm<FormData>({
defaultValues: {
name: undefined,
},
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="name"
control={control}
render={({ field }) => <input type="string" {...field} />}
/>
<input type="submit" />
</form>
);
}
那預設值可以是 null
嗎?答案也是不行的。
input
的 value
不能是 null
,如果是 Controlled Input 的話,則 value
也不能是 undefined
。一旦使用了 undefined
,該 input 就會被視為 Uncontrolled Input。
根據建議,如果一開始表單是沒有資料的話,預設值可以使用空字串(empty string)。因此,我們可以把上面的範例改成:
// ...
export default function Form() {
const { handleSubmit, control } = useForm<FormData>({
defaultValues: {
name: '',
},
});
return (/* ... */);
}
處理 Input Number 的方式
範例程式碼 @ CodeSandbox
input[type="number"]
是一個很常用到,但是卻有點麻煩的東西。