As far as I can tell the usual/recommended way in React is to have state and pass it with the setter to a component, e.g.
const [name, setName] = useState("");
return <>{/* stuff */} <MyStringInput value={name} setValue={setName} </>;
It feels weird to me that we have this setName
function when we don’t actually want to manually use it in this scope, also, MyStringInput
assumes setValue
is the setter for value
but this is not enforced. I would think something like this would be nicer:
const [name, nameInput] = myStringInput("");
return <>{/* stuff */} {nameInput} </>;
But reading materials on react it seems you’re supposed to either have a hook returning no JSX or a component returning only JSX. What are the flaws if any in the second approach?
3
Answers
You can have something similar to what you want by creating custom hooks.
/hooks/use-input.js
your-file.js
But at this point, this just a regular custom input component you could create separately. So I would recommend you do so.
In React, it’s possible to combine a component and a hook, and this is a common pattern known as a "custom hook." Custom hooks are functions that can encapsulate reusable logic, and they often include React hooks to manage state or side effects. While it’s not common to combine a component and a hook directly, custom hooks are used to abstract and reuse logic across multiple components, effectively separating concerns between the rendering logic (in components) and the state or side effect logic (in hooks). Here’s why combining them in this way is useful:
Reusability: By separating the logic into a custom hook, you can reuse that logic across multiple components. This promotes code reuse and maintains a cleaner separation of concerns in your application.
Readability and Maintainability: Components become simpler and more focused on rendering when you move the logic to a custom hook. This makes components easier to read, understand, and maintain.
Testability: Custom hooks can be tested independently from components. You can write unit tests specifically for your custom hooks to ensure they behave as expected. This makes your codebase more testable and robust.
Composition: You can compose multiple custom hooks within a component, allowing you to mix and match various pieces of logic easily. This promotes a more modular and flexible approach to building components.
Here’s an example of a custom hook in React that combines a state management hook (
useState
) with some custom logic:You can then use this custom hook in multiple components:
In this example, the
useCounter
custom hook encapsulates the counter logic, and it can be easily reused in different components. This separation of concerns helps keep your React codebase organized and maintainable.Hooks are a way of composing behaviour and state whereas JSX/components are essentially a higher-level macro view of the application.
It can help to understand the progression of React itself to know why the pattern you describe came about in the first place. On first glance, you actually might think the older class components were more succinct in describing what you want to do, but they were also inherently less flexible.
Back before hooks existed, you had class components. In class components, the component itself is the abstraction you use for state, effects (aka lifecycle), and presentation. All of these tended to be coupled in one unit. The problem with inherently coupling the lifecycle/state with a component was that the lifecycle and state are intrinsically coupled to the presentational layer so logic becomes harder to reuse elsewhere in the app. You very often might find whilst the scope of the state is only useful to you now for a limited bit of JSX, that later on, you’ll find as your app grows something else needs access to that and you need to move state (and its associated lifecycle behaviours around).
Many patterns emerged to work around this on class components like HOCs and render props (worth googling these), but those had unlikeable APIs and some had fundamental weaknesses that made composing behaviours more clumsy than it is now with hooks. Not to say they’re bad, actually they often still have a place to this day in specific circumstances, especially render props.
Another aspect of class components was that the API used to access the lifecycle of class components tended to mean that in the end, you had multiple functional requirements split around your component, whereas they really should be grouped by purpose/functionality. Hooks enable the consumer to pick and choose what part of the library functionality they want to use in a neat unit of reuse.
In addition, having the hooks separate means it’s easier for consumers to well…hook…the logic of some lib into a presentational style/jsx tree of their choosing, without being inherently coupled to someone else’s. Form libraries are a great example of this (see react-hook-form).
This gave rise to hooks. In short, hooks means:
Using your example, whilst you may think that now nothing needs access to the scope of the hook, if this assumption was baked in (as it was with class components), it’s really annoying to refactor and pull apart later when you decided that something new actually does need that state. Hooks ensures that state and behaviour remain portable and composable. However, if this code is at the application level, this is where it can actually start to be useful to make those assumptions and create wrappers that couple them. If you are not authoring something highly reusable in many unknown contexts, that assumption is usually safe.
If you’re sure that consumers will simply never care about the scope of the hook in isolation, the real answer is to simply make a component that wraps the hooks and the inner components and that component is the API. In libraries though, this can sometimes be a big mistake since if something else in the user’s userland that is potentially "higher-up" needs access to that state, the library user is basically trapped unless they do dirty duplication tricks which inherently increase the surface area of bugs in an application.
That said, many libraries will expose easy-to-use component wrappers that keep the state with the inner components, and also expose the underlying hooks and inner components separately as a way to enable ease of use whilst also enabling power users who may need complex relationships with that state that spread across multiple areas of their codebase of their choosing.
As I commented elsewhere on this question, returning components from a hook is a bad idea. With that approach, you have basically created a component (which inherently couples hooks and other inner components) inside of a hook, instead of just using a component to do that. Such a hook’s behaviour is inherently inseparable from its presentational inner components and the problems of class components start to reappear. This typically leads to scoping and performance nightmares as well as being very hard to reason about.