In the JSX world, an HOC is basically this: a component that takes another component as an argument and returns a new component that is the result of the composition of the two. HOC’s are great for shared behavior across components that we’d rather not repeat.
Instead of repeating this loading, error, data pattern for each component that talks to a foreign data source asynchronously, we can use a higher-order component factory to deal with these states for us. Let’s consider a withAsync
higher-order component factory that remedies this:
const TodoList = withAsync(BasicTodoList);
const withAsync = (Component) => (props) => {
if (props.loading) {
return "Loading...";
}
if (props.error) {
return error.message;
}
return (
<Component
// Pass through whatever other props we give `Component`.
{...props}
/>
);
};
So now, when any Component
is passed into withAsync
, we get a new component that renders appropriate pieces of information based on its props. This changes our initial component into something more workable:
const TodoList = withAsync(BasicTodoList);
const App = () => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState([]);
const [error, setError] = useState([]);
useEffect(() => {
fetch("https://mytodolist.com/items")
.then((res) => res.json())
.then((data) => {
setIsLoading(false);
setData(data);
})
.catch(setError);
}, []);
return <TodoList loading={isLoading} error={error} data={data} />;
};
Composing HOCs
Composing multiple Higher Order Components (HOCs) together is a common pattern in React, which allows developers to mix and match functionalities and behaviors across components. Here’s an example of how you might compose multiple HOCs:
// compose.js
const compose =
(...hocs) =>
(WrappedComponent) =>
hocs.reduceRight((acc, hoc) => hoc(acc), WrappedComponent);
// Usage:
const EnhancedComponent = compose(withLogging, withUser)(MyComponent);
In this compose
function, reduceRight
is used to apply each HOC from right to left to the WrappedComponent
. This way, you can list your HOCs in a flat list, which is easier to read and maintain. The compose
function is a common utility in functional programming, and libraries like Redux provide their own compose
utility function for this purpose.
const EnhancedComponent = compose(
withErrorHandler,
withLoadingSpinner,
withAuthentication,
withAuthorization,
withPagination,
withDataFetching,
withLogging,
withUser,
withTheme,
withIntl,
withRouting
)(MyComponent);
Can we think of any React HOCs that we use fairly frequently? Yes, we can! React.memo
is one that we just covered in this chapter and is indeed a higher-order component! Let’s look at another one: React.forwardRef
. This is a higher-order component that forwards a ref to a child component. Let’s look at an example:
const FancyInput = React.forwardRef((props, ref) => (
<input type="text" ref={ref} {...props} />
));
const App = () => {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<FancyInput ref={inputRef} />
</div>
);
};
Render Props
Since we’ve talked about JSX expressions above, a common pattern is to have props that are functions that receive component-scoped state as arguments to facilitate code reuse. Here’s a simple example:
<WindowSize
render={({ width, height }) => (
<div>
Your window is {width}x{height}px
</div>
)}
/>
Notice how there’s a prop called render
that receives a function as a value? This prop even outputs some JSX markup that’s actually rendered. But why? Turns out WindowSize
does some magic internally to compute the size of a user’s window, and then calls props.render
to return the structure we declare, making use of enclosing state to render the window size.
const WindowSize = (props) => {
const [size, setSize] = useState({ width: -1, height: -1 });
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return props.render(size);
};
Children as a function
Since children
is a prop, some have preferred to drop the render
prop name altogether and instead just use children
. This would change the use of WindowSize
to look like this:
<WindowSize>
{({ width, height }) => (
<div>
Your window is {width}x{height}px
</div>
)}
</WindowSize>
Control Props
Controlled Components are components that do not maintain their own internal state. Instead, they receive their current value as a prop from a parent component, which is the single source of truth for their state. When the state should change, controlled components notify the parent using callback functions, typically onChange
. The parent is thus responsible for managing the state and updating the value of the controlled component.
function Toggle({ on, onToggle }) {
const [isOn, setIsOn] = React.useState(false);
const handleToggle = () => {
const nextState = on === undefined ? !isOn : on;
if (on === undefined) {
setIsOn(nextState);
}
if (onToggle) {
onToggle(nextState);
}
};
return (
<button onClick={handleToggle}>
{on !== undefined ? on : isOn ? "On" : "Off"}
</button>
);
}