confusion about this React custom hook usage
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.
I agree with Drew that using a custom hook to just return jsx based on function parameters breaks the conventional component abstraction. To extend on this, I can think of four different ways to work with jsx in React.
Static JSX
If jsx doesn't rely on state/props, you can just define it as a const
even outside of your components. This is especially useful if you have an array of content.
Example:
const myPs =
[
<p key="who">My name is...</p>,
<p key="what">I am currently working as a...</p>,
<p key="where">I moved to ...</p>,
];
const Component = () => (
<>
{ myPs.map(p => p) }
</>
);
Component
For both stateful and stateless parts of your jsx. Components are the React way to break down your UI into maintainable and reusable pieces.
Context
Context providers return jsx (since they are also "just" components). Usually, you would just wrap the child components inside the context you want to provide like so:
return (
<UserContext.Provider value={context}>
{children}
</UserContext.Provider>
);
But context can also be used to develop global components. Imagine a dialog context that maintains a global modal dialog. The goal is to never open more than one modal dialog at once. You use the context to manage the state of the dialog but also render the global dialog jsx through the context provider component:
function DialogProvider({ children }) {
const [showDialog, setShowDialog] = useState(false);
const [dialog, setDialog] = useState(null);
const openDialog = useCallback((newDialog) => {
setDialog(newDialog);
setShowDialog(true);
}, []);
const closeDialog = useCallback(() => {
setShowDialog(false);
setDialog(null);
}, []);
const context = {
isOpen: showDialog,
openDialog,
closeDialog,
};
return (
<DialogContext.Provider value={context}>
{ showDialog && <Dialog>{dialog}</Dialog> }
{children}
</DialogContext.Provider>
);
}
Updating the context will also update the global dialog within the UI for the user. Setting a new dialog will remove the old one.
Custom Hook
Generally, hooks are a great way to encapsulate logic that you want to share between components. I have seen them being used as an abstraction layer of complicated context. Imagine a very complicated UserContext
and most of your components just care about if the user is logged in, you can abstract that away through a custom useIsLoggedIn
hook.
const useIsLoggedIn = () => {
const { user } = useContext(UserContext);
const [isLoggedIn, setIsLoggedIn] = useState(!!user);
useEffect(() => {
setIsLoggedIn(!!user);
}, [user]);
return isLoggedIn;
};
Another great example is a hook that combines state that you actually want to reuse (not share) in different components / containers:
const useStatus = () => {
const [status, setStatus] = useState(LOADING_STATUS.IS_IDLE);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(status === LOADING_STATUS.IS_LOADING);
}, [status]);
return { status, setStatus, isLoading };
};
This hook creates API call related state that you can reuse in any component that deals with API calls.
I got one example where I actually use a custom hook to render jsx instead of using a component:
const useGatsbyImage = (src, alt) => {
const { data } = useContext(ImagesContext);
const fluid = useMemo(() => (
data.allFile.nodes.find(({ relativePath }) => src === relativePath).childImageSharp.fluid
), [data, src]);
return (
<Img
fluid={fluid}
alt={alt}
/>
);
};
Could I have created a component for that? Sure, but am also just abstracting away a context which for me was a pattern to use a hook. React is not opinionated. You can define your own conventions.
Again, I think Drew already gave you a pretty good answer. I hope my examples just help you to get a better idea of the usage of the different tools React provides you.
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
- When you write a hook that returns JSX component, you are essentially defining the component within the functional component, so on each and every re-render you will be creating a new instance of the component. This will lead to the component being unmounted and mounted again. Which is bad for performance and also buggy if you have stateful login within the component as the state will get reset with every re-render of parent
- By defining a JSX component within the hook, you are taking away the option of lazy loading your component if the need be.
- Any performance optimization to the component will require you to make use of
useMemo
which doesn't give you the flexibility of a custom comparator function like React.memo
The 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 = React.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;