I was looking at some tutorial on React Hooks and in the tutorial the author created a useDropdown hook for rendering reusable dropdowns. The code is like this
import React, { useState } from "react";
const useDropdown = (label, defaultState, options) => {
const [state, updateState] = useState(defaultState);
const id = `use-dropdown-${label.replace(" ", "").toLowerCase()}`;
const Dropdown = () => (
<label htmlFor={id}>
{label}
<select
id={id}
value={state}
onChange={e => updateState(e.target.value)}
onBlur={e => updateState(e.target.value)}
disabled={!options.length}
>
<option />
{options.map(item => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</label>
);
return [state, Dropdown, updateState];
};
export default useDropdown;
and he used this in a component like this
import React, { useState, useEffect } from "react";
import useDropdown from "./useDropdown";
const SomeComponent = () => {
const [animal, AnimalDropdown] = useDropdown("Animal", "dog", ANIMALS);
const [breed, BreedDropdown, updateBreed] = useDropdown("Breed", "", breeds);
return (
<div className="search-params">
<form>
<label htmlFor="location">
Location
<input
id="location"
value={location}
placeholder="Location"
onChange={e => updateLocation(e.target.value)}
/>
</label>
<AnimalDropdown />
<BreedDropdown />
<button>Submit</button>
</form>
</div>
);
};
export default SomeComponent;
He said this way we can create reusable dropdown components. I was wondering how is this different from defining a plain old Dropdown component and pass props into it. The only difference I can think of in this case is that now we have the ability to get the state and setState in the parent component(i.e. SomeComponent) and read / set the state of the child(i.e. the component output by useDropdown) directly from there. However is this considered an anti-pattern since we are breaking the one way data flow?
While there is no hard core restriction on how you should define custom hooks and what logic should the contain, its an anti-pattern to write hooks that return JSX
You should evaluate what benefits each approach gives you and then decide on a particular piece of code
There are a few downsides to using hooks to return JSX
useMemo which doesn't give you the flexibility of a custom comparator function like React.memoThe benefit on the other hand is that you have control over the state of the component in the parent. However you can still implement the same logic by using a controlled component approach
import React, { useState } from "react";
const Dropdown = Reat.memo((props) => {
const { label, value, updateState, options } = props;
const id = `use-dropdown-${label.replace(" ", "").toLowerCase()}`;
return (
<label htmlFor={id}>
{label}
<select
id={id}
value={value}
onChange={e => updateState(e.target.value)}
onBlur={e => updateState(e.target.value)}
disabled={!options.length}
>
<option />
{options.map(item => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</label>
);
});
export default Dropdown;
and use it as
import React, { useState, useEffect } from "react";
import useDropdown from "./useDropdown";
const SomeComponent = () => {
const [animal, updateAnimal] = useState("dog");
const [breed, updateBreed] = useState("");
return (
<div className="search-params">
<form>
<label htmlFor="location">
Location
<input
id="location"
value={location}
placeholder="Location"
onChange={e => updateLocation(e.target.value)}
/>
</label>
<Dropdown label="animal" value={animal} updateState={updateAnimal} options={ANIMALS}/>
<Dropdown label="breed" value={breed} updateState={updateBreed} options={breeds}/>
<button>Submit</button>
</form>
</div>
);
};
export default SomeComponent;
Anti-pattern is such a blunt phrase to describe simple of otherwise complex solutions that other developers don't agree with. I agree with Drew's point of view that the hook breaks conventional design, by doing more than it should.
As per React's hook documentation, the purpose of a hook is to allow you to use state and other React features without writing a class. This is typically considered to be setting state, performing computational tasks, doing API or other queries in an asynchronous matter, and responding to user input. Ideally, a functional component should be interchangeable with a class component, but in reality, this is far more difficult to achieve.
The particular solution for creating Dropdown components, while it works, isn't a good solution. Why? It's confusing, it isn't self explanatory and it's difficult to comprehend what's happening. With hooks, they should be simple and perform a single task, eg a button callback handler, calculating and returning a memoized result, or doing some other task you would normally delegate to this.doSomething().
Hooks that return JSX aren't really hooks at all, they're just Functional Components, even if they use the correct prefix naming convention for hooks.
There's also confusion around React and one-way communication for component updates. There's no restriction on which way data can pass, and can be treated in a similar fashion to Angular. There are libraries such as mobx which allows you to subscribe and publish changes to shared class properties, which will update any UI component that listens, and that component can update it too. You can also use RxJS to make asynchronous changes at any time, that can update the UI.
The specific example does steer away from SOLID principles, providing input points for the parent component to control the data of the child component. This is typical of strongly typed languages, such as Java, where it's more difficult to do asynchronous communication (not really a problem these days, but it used to be). There's no reason why a parent component should not be able to update a child component - it's a fundamental part of React. The more abstraction you add, the more complexity your add, and more points of failure.
Adding the use of asynchronous functions, observables (mobx/rxjs), or context can reduce the direct data coupling, but it will create a more complex solution.