TLDR: If you have a React form where the same set of components shows up but with different content based on "tabs" or some other selector, consider keeping a 1:1 mapping between the component and its state and hiding the inactive tabs visually, rather than depending on rendering.
React-based design systems always have a weird decision to make about controlled components (where a component's value is constantly being updated by the parent as the component notifies the parent of key press and other events) vs uncontrolled (where the component handles its own affairs, but still has to notify the parent on each change)
My opinion of React slipped a little when I realized that the functioning of a normal controlled input box or textarea utterly depended on a rendering optimization - in the simple declarative UI model, when a user types a character into an input, React rerenders the input with the new value. But of course, if (and sadly, when) React actually rerenders the input, transient state like "cursor position" is reset, since that's not part of the state the parent is managing. So normal UI features like "entering text anywhere but the very end of the current content" depends on React being smart enough to know it doesn't REALLY need to rerender that input box.
But uncontrolled components have always been weird as well... mostly because of the legacy of html5 syntax. If you just put in the value prop with no handler, the control would be locked to that value, so design systems like Material UI lean on "defaultValue=" for uncontrolled components.
(There's a third path that is sadly underused, IMO: you don't need any handlers whatsoever for basic form components if you wrap them in a <form> tag
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const username = formData.get('username');
// or onvert all data into a standard JavaScript object
const allData = Object.fromEntries(formData.entries());
};
That gets a little messier in a TypeScript world, but I think the fixes for that still beat all those handlers... let html5 and the browser do what they were design to do! And of course there are other solutions to this problem like formik)
Anyway!
At work we have a component wrapping Kendo React Richtext Editor, and the company's legacy wrapper for it is in some need of refactoring. The editor is uncontrolled, its keeping a copy of its own state, and sometimes there's drift with what the parent thinks is being displayed and what's actually being shown to the user.
That kind of problem became especially acute on a few pages that used a tab component to display the same form, but with a different set of values based on the current tab selection. If you entered data in a field, swapped tabs, then went back, often that original data would be not show properly (like maybe the parent would be aware of the updated values, but on rerender, the KendoEdit would revert back to the original values it was set with.)
These problems would go away if our tab implementation kept a separate set of form elements in the DOM, and just hid all of those for the inactive tabs, and I'm going to pitch that as a future refactoring... In this world of uncontrolled components, not having a 1:1 mapping to something in memory is a whole armory's worth of foot guns.
(It's probably temping to reuse components with something like
<input value={mainData[currentTab].content}>
since it feels more "efficient" somehow, like costly to have components in the DOM but not shown... but the more I think about it, visual hiding and 1:1 mapping of component and state seems like the safer bet)
No comments:
Post a Comment