Background
I want an interesting number input functionality:
I have multiple goals:
- The $ and % mode have different validation rules on the number value (e.g. maximum 999999.99 vs 100) and should be re-validated when either input or input type updated.
- For same invalid scenarios, $ and % have different error messages (e.g.
Please enter an amount
vsPlease enter a percentage
) and the error message should update accordingly when switching between $ and %. - The
Save changes
button should be enabled/disabled based on whether the value is valid. - The value updates should trigger state update in the parent.
- The $ mode rounds number to 2 fixed decimal places, and % mode rounds to integer, when switching between $ and % the rounding should be updated.
Challenge 1: stale error message
If I implement the input of two modes in one go:
1 | <NumberInput |
The error message will be stale like this when I switch between:
This is interesting, from the position of the icon the NumberInput
has been notified to re-render, but it did not re-render the error message 🤔
The quick solution
If I change to a switch case statement, the whole thing will be forced to re-render, and the problem is solved:
1 | switch (inputType) { |
But why? Why did I get a “Partial” re-render?
Looking into the implementation of NumberInput
, the errorMessage
state’s setter is invoked when:
- The
value
props passed toNumberInput
has changed - The user has changed the input box’s value or blurred the input
- If
showInitialError
is true, when the element first renders
None of the three were triggered, this tells us:
A props update does not cause React to re-render the element.
It just triggers theuseEffect
hook that subscribes to this props change,
and causes React to update the DOM that is reflected in the returned element.
Because:
- there is no effect that is triggered by
errorMessages
and updateserrorMessage
state <ErrorMessage>
part of the returned DOM is not changed becauseerrorMessage
state did not change
React does not think it needs to re-render the error message part, so it stays stale.
No effect that is triggered by errorMessages
and updates errorMessage
state??
Now the part that seems off is that errorMessages
props update does not trigger errorMessage
state update
The dependency chain looks like this:
useCallback
validateInput
depends onerrorMessages
useCallback
setValue
depends on the memorized callbackvalidateInput
- There is a
useEffect
that callsvalidateInput
ifshowInitialError
is true anderrorMessages
update, but when the value isundefined
we wantshowInitialError
to befalse
because we have an empty form.
Challenge 2: the need for boiler plate code to convert between % <-> $
This is the major reason I am very keen to replace the library component with a custom one. Any time user switches between %/$, there has to be a conversion like this * 3 on errors:
1 | const convertPercentageErrorToAmountError = ( |
And a conversion like this * 3 on values:
1 | // if the value is a number, round to 0 decimals for percentage |
My initial plan:
If when the tipping option changes, the number input gets re-rendered, then with
showInitialError={true}
(it will be true because the value will not beundefined
), the values will be automatically validated, so the error will be automatically updated.
However after digging into this plan, it looks that a custom component cannot really simplify the code to achieve our UI/UX need. Here’s why:
As discussed in challenge 1, I need a useEffect that is explicitly triggered by the change. This will lead to an infinite loop because when the values are re-rounded and re-validated, the values are changed, then triggers the use effect again.