When you interview 100s of "senior" and the basics of further state:
"A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature."
The theory behind HOCs comes from ...
Function Composition
In mathematics, , which also explains the difference between useCallback and useMemo.
A programming language has first-class functions if it allows you to assign functions to variables.
You can abstract the composition to combine any two functions:
const compose2 = (f, g) => x => f(g(x));
const doubleThenInc2 = compose2(inc, double);
You omit the argument x in the definition of doubleThenInc2. This means doubleThenInc2 is defined point-free, which is when you define a function without mentioning its arguments.
const doubleThenInc = x => inc(double(x)); // mentions X 👉 pointed
const doubleThenInc2 = compose2(inc, double); // point-free
If you want to compose an arbitrary amount of functions, you need to is essentially a wrapper for the function keyword and handles prototypal inheritance. In other words, classes compile to constructor functions.
Therefore, since all components are functions in React and JavaScript has higher-order functions, you get HOCs for free. That is what the docs mean when they say HOCs "are a pattern that emerges from React’s compositional nature."
Now you should understand the basic definition of HOCs:
A Higher-Order component is a function that takes a component and returns a new component.
Any function whose input and output is a React component is a HOC.
HOCs by Example
You probably want to see what a higher-order component looks like. Follow the rest of this tutorial to write your own using TDD. You're going to use to write the tests.
You can deduce two requirements from the definition of a higher-order component:
- HOCs are functions.
- HOCs take a component and return a component.
You can capture these requirements in a unit test.
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import myHOC from './my-hoc';
function MyComponent({ title = 'Hello' }) {
return <p>{title}</p>;
}
describe('myHOC', () => {
test('given a component: returns the component with a default title', () => {
const WrappedComponent = myHOC(MyComponent);
render(<WrappedComponent />);
expect(screen.getByText('Hello')).toHaveTextContent('Hello');
});
});
The test checks both requirements because when this test passes, you can logically deduce that your HOC is a function and that it returns a component without spelling out those requirements explicitly. If the HOC were not a function and you tried to call it, it would throw, and your unit test would fail with a clear stack trace. Likewise, the test renders the return value of the HOC, which ensures it is a React component.
Notice how you did NOT test for typeof function here. Unit tests which only test types are an anti-pattern. It's redundant with simply calling the function and checking its output value. In general, type checks are redundant with well-written unit tests. This is why unit tests can catch most type errors, without the need for additional measures like type annotations (though annotations and type inference can still be useful to enable IDE tooling).
You can get the test to pass by making your HOC the . In a Remix app, you won't need a layout HOC because you can export a layout component from your root.tsx file.
Now, make your test pass by using the Layout component in your HOC.
import { Layout } from './layout';
export default Component => () => (
<Layout>
<Component />
</Layout>
);
Your tests should both pass now.
✓ app/with-layout.test.jsx (2)
✓ withLayout (2)
✓ given a component: returns the component with a default title
✓ given a component: renders the layout around the component
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 16:05:39
Duration 128ms
PASS Waiting for file changes...
press h to show help, press q to quit
Notice how the withLayout HOC now takes in a component and then returns a function because before this change it actually was NOT a higher-order component.
This also shows the most common misconception about HOCs. Many developers answer the question of "What is a higher-order component" with "it's a component that takes in a React component and returns it".
They probably think of something like this.
// Wrong! ❌
function NotAHigherOrderComponent({ Component }) {
return (
<div>
<h1>Header added by NotAHigherOrderComponent</h1>
<Component />
</div>
);
}
function MyComponent() {
return <p>Hello, I am a regular component.</p>;
}
function App() {
return (
<div>
<NotAHigherOrderComponent Component={MyComponent} />
</div>
);
}
What you see above is a React component that takes in another React component as a prop.
But that's is NOT a higher-order component because HOCs are functions and NOT components. You can NOT render a HOC.
Looking back at the your withLayout HOC, it contains a bug. Can you spot it?
If not, that is okay. You can write the following test to expose the error.
describe('withLayout', () => {
// ... your other tests
test('given props for the wrapped component: passes on the props to the wrapped component', () => {
const WrappedComponent = withLayout(MyComponent);
const customTitle = 'Custom Title';
render(<WrappedComponent title={customTitle} />);
expect(screen.getByText(customTitle)).toHaveTextContent(customTitle);
});
});
The new test fails.
❯ app/with-layout.test.jsx (3)
❯ withLayout (3)
✓ given a component: returns the component with a default title
✓ given a component: renders the layout around the component
× given props for the wrapped component: passes on the props to the wrapped component
The test exposes the problem: You fail to pass props to the wrapped component. You can make the test pass by passing on the props the HOC receives.
import { Layout } from './layout';
export default Component => props => (
<Layout>
<Component {...props} />
</Layout>
);
Now your tests pass because your HOC correctly passes on the props to the wrapped component.
✓ app/with-layout.test.jsx (3)
✓ withLayout (3)
✓ given a component: returns the component with a default title
✓ given a component: renders the layout around the component
✓ given props for the wrapped component: passes on the props to the wrapped component
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 16:55:45
Duration 139ms
PASS Waiting for file changes...
press h to show help, press q to quit
However, the abstraction capabilities of HOCs wouldn't be as useful if they didn't have another key feature. with mapStateToProps. (In fact, it accepts two more arguments: mapDispatchToProps and mergeProps.)
Assume that some pages should render without the header, so you modify your layout component to take in a prop that let's you show and hide the header.
export function Layout({ children, showHeader = true }) {
return (
<div>
{showHeader && (
<header>
<h1>Some Title</h1>
</header>
)}
<main>{children}</main>
<footer>
<p>Some footer</p>
</footer>
</div>
);
}
Now write a test that allows you to modify your HOC. You'll also need to modify your existing tests to accommodate the fact that your HOC now takes in a configuration object.
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import withLayout from './with-layout';
function MyComponent({ title = 'Hello' }) {
return <p>{title}</p>;
}
describe('withLayout', () => {
test('given a component: returns the component with a default title', () => {
const WrappedComponent = withLayout()(MyComponent);
render(<WrappedComponent />);
expect(screen.getByText('Hello')).toHaveTextContent('Hello');
});
test('given a component: renders the layout around the component', () => {
const WrappedComponent = withLayout()(MyComponent);
render(<WrappedComponent />);
expect(screen.getByRole('heading')).toHaveTextContent(/some title/i);
expect(screen.getByRole('main')).toContainElement(
screen.getByText('Hello'),
);
expect(screen.getByRole('contentinfo')).toHaveTextContent(/some footer/i);
});
test('given props for the wrapped component: passes on the props to the wrapped component', () => {
const WrappedComponent = withLayout()(MyComponent);
const customTitle = 'Custom Title';
render(<WrappedComponent title={customTitle} />);
expect(screen.getByText(customTitle)).toHaveTextContent(customTitle);
});
test('given used in composition with other HOCs: passes on the props of the other HOCs', () => {
const compose =
(...fns) =>
x =>
fns.reduceRight((y, f) => f(y), x);
const withTitle = Component => props => (
<Component title="foo" {...props} />
);
const ComposedComponent = compose(withLayout(), withTitle)(MyComponent);
render(<ComposedComponent />);
expect(screen.getByText('foo')).toHaveTextContent('foo');
});
test('given a component and NOT rendering the header: does NOT render the header', () => {
const WrappedComponent = withLayout({ showHeader: false })(MyComponent);
render(<WrappedComponent />);
expect(screen.queryByRole('heading')).toBeNull();
});
});
Watch all your tests fail because your component still lacks the configuration object. Add it to make them pass.
import { Layout } from './layout';
export default ({ showHeader = true } = {}) =>
Component =>
props => (
<Layout showHeader={showHeader}>
<Component {...props} />
</Layout>
);
To answer the question of when to use composition for HOCs, remember what I told you learned earlier. HOCs are excellent if you want to abstract away common logic between many components. You chose to give your function a layout functionality because that is one area that most screens of your application will share. Using compose you can define a HOC that you can use to wrap all your pages with.
Real-World Example
Here is a real-world example of a SignInForm container component. See if you understand it, then read the explanation to check if you were correct.
import { withFormik } from 'formik';
import compose from 'ramda/src/compose.js';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import SignInComponent from './sign-in-form-component.js';
import { isAuthenticating, signIn } from './user-authentication-reducer.js';
import { signInValidationSchema } from './validation-schema.js';
const initialFormValues = { email: '', password: '' };
const mapStateToProps = state => ({ loading: isAuthenticating(state) });
const formikConfig = {
handleSubmit: ({ email, password }, { props: { signIn } }) => {
signIn({ email, password });
},
mapPropsToValues: () => initialFormValues,
validationSchema: signInValidationSchema,
};
export default compose(
withRouter,
connect(
mapStateToProps,
{ signIn }
),
withFormik(formikConfig),
)(SignInComponent);
In the example above, you composed 3 different HOCs.
withRouteris a HOC from React Router DOM. It injects thehistoryobject, which you can use to navigate to the password reset screen, when the user clicks the "Forgot Password" button.
connectis a HOC from React Redux. You use it to connect your component to your Redux store. You inject theloadingprop and thesignInaction creator.
withFormikis a HOC from Formik. Formik let's you control local form state and handles form validation for you.
Sometimes you need to from the inner component to the resulting component. Here is a Higher-Order HOC (a function that returns a HOC), which does this for you.
import hoistNonReactStatics from 'hoist-non-react-statics';
const hoistStatics = higherOrderComponent => Component => {
const WrappedComponent = higherOrderComponent(Component);
hoistNonReactStatics(WrappedComponent, Component);
return WrappedComponent;
};
BTW: !
SOCIAL SHARE CARD GENERATOR