Remix.run Logo
moritzwarhier 2 hours ago

The idea is that the component's API is not the DOM. Usually this means your data should flow in a certain way: top-down.

Application code is not supposed to use the DOM as the source of truth for some boolean state that the checkbox is an input for.

You don't usually read a component's state from outside (here: the "checked" property).

Instead you define an API where data only flows top-down.

When your checkbox component follows this paradigm, it is "controlled", and if it contains a standard HTML input, that input's "checked" DOM object property is bound to the data passed into the component ("props"). Clicking it won't check it anymore until you add an "onClick" callback and pass a function into this callback that will make the "checked" prop change.

The checkbox is now "controlled" and it's state was "lifted up" (meaning that it is determined not by the checkbox component itself).

"controlled" means you tell React to always force the "checked" DOM property to be the same as the "checked" prop you pass into the component. You do this by assigning to the reflected "checked" HTML attribute in JSX.

When your components only use this "top-down" data flow, they're "pure" in React lingo. Because they look like pure functions: props => DOM fragment. The machinery behind the scenes means they're not actually that (something has to coordinate the rendering).

But if you don't use internal state (e.g. useState hook) or global stores, these "impure" parts are React internals only, and you can have a mental model that views the component like a pure function.

This makes it easier to connect it with other components in a tree.

For example:

HTMLInputElement.checked can be true without a "checked" attribute being in the markup.

If you want to have some text next to it that says "checked / not checked" you have to wire stuff, and this stuff depends on your markup.

If you have a "controlled" checkbox, you have a tree, not only for the markup, but also for the data: the boolean "checked" state can now be declared one level above both the info text and the checkbox. Then the info text doesn't care at all about events anymore.

And the checkbox component only uses a callback that is also independent from the exact markup structure (e.g. a selector for the HTML input element).

You don't need to read from the checkbox to update the text. You feed both with a boolean and both can be "pure" components. The checkbox gets a "onClick" callback and it's checked state is no longer internal, it's "controlled".

The wiring you have to do instead of the regular DOM events (which would read the input's state) is now to use your "onClick" callback to toggle your boolean.

Internally, in the component, you do whatever you need to read and write to the DOM. But usually that just means "what markup do I return".

Input elements and reflected attributes such as "checked" are already a relatively complex case.

And, you can escape the recommended top-down data flow by many means (refs, context, accessing centralized "stores" from within the component...), but that's often where it gets ugly. But you need to do it often when your app gets bigger (centralized data stores), or when you implement things like UI libraries (refs).