HOC - Functional Component

I have already created a HOC in my react app following this, and its working fine. However i was wondering if there is a way to create a HOC as functional component(With or without state)??? since the given example is a class based component.

Tried to find the same over web but couldn't get anything. Not sure if thats even possible?? Or right thing to do ever??

Any leads will be appreciated :)

82203 次浏览

Definitely you can create a functional stateless component that accepts component as an input and return some other component as an output, for example;

  1. You can create a PrivateRoute component that accepts a Component as a prop value and returns some other Component depending on if user is authenticated or not.
  2. If user is not authenticated(read it from context store) then you redirect user to login page with <Redirect to='/login'/>else you return the component passed as a prop and send other props to that component <Component {...props} />

App.js

const App = () => {
return (
<Switch>
<PrivateRoute exact path='/' component={Home} />
<Route exact path='/about' component={About} />
<Route exact path='/login' component={Login} />
<Route exact path='/register' component={Register} />
</Switch>
);
}


export default App;

PrivateRoute.jsx

import React, { useContext , useEffect} from 'react';
import { Route, Redirect } from 'react-router-dom'
import AuthContext from '../../context/auth/authContext'


const PrivateRoute = ({ component: Component, ...rest }) => {
const authContext = useContext(AuthContext)
const { loadUser, isAuthenticated } = authContext
useEffect(() => {
loadUser()
// eslint-disable-next-line
}, [])
if(isAuthenticated === null){
return <></>
}
return (
<Route {...rest} render={props =>
!isAuthenticated ? (
<Redirect to='/login'/>
) : (
<Component {...props} />
)
}
/>
);
};
export default PrivateRoute;

Higher Order Components does not have to be class components, their purpose is to take a Component as an input and return a component as an output according to some logic.

I agree with siraj, strictly speaking the example in the accepted answer is not a true HOC. The distinguishing feature of a HOC is that it returns a component, whereas the PrivateRoute component in the accepted answer is a component itself. So while it accomplishes what it set out to do just fine, I don't think it is a great example of a HOC.

In the functional component world, the most basic HOC would look like this:

const withNothing = Component => ({ ...props }) => (
<Component {...props} />
);

Calling withNothing returns another component (not an instance, that's the main difference), which can then be used just like a regular component:

const ComponentWithNothing = withNothing(Component);
const instance = <ComponentWithNothing someProp="test" />;

One way to use this is if you want to use ad-hoc (no pun intended lol) context providers.

Let's say my application has multiple points where a user can login. I don't want to copy the login logic (API calls and success/error messages) across all these points, so I'd like a reusable <Login /> component. However, in my case all these points of login differ significantly visually, so a reusable component is not an option. What I need is a reusable <WithLogin /> component, which would provide its children with all the necessary functionality - the API call and success/error messages. Here's one way to do this:

// This context will only hold the `login` method.
// Calling this method will invoke all the required logic.
const LoginContext = React.createContext();
LoginContext.displayName = "Login";


// This "HOC" (not a true HOC yet) should take care of
// all the reusable logic - API calls and messages.
// This will allow me to pass different layouts as children.
const WithLogin = ({ children }) => {
const [popup, setPopup] = useState(null);


const doLogin = useCallback(
(email, password) =>
callLoginAPI(email, password).then(
() => {
setPopup({
message: "Success"
});
},
() => {
setPopup({
error: true,
message: "Failure"
});
}
),
[setPopup]
);


return (
<LoginContext.Provider value={doLogin}>
{children}


{popup ? (
<Modal
error={popup.error}
message={popup.message}
onClose={() => setPopup(null)}
/>
) : null}
</LoginContext.Provider>
);
};


// This is my main component. It is very neat and simple
// because all the technical bits are inside WithLogin.
const MyComponent = () => {
const login = useContext(LoginContext);


const doLogin = useCallback(() => {
login("a@b.c", "password");
}, [login]);


return (
<WithLogin>
<button type="button" onClick={doLogin}>
Login!
</button>
</WithLogin>
);
};

Unfortunately, this does not work because LoginContext.Provider is instantiated inside MyComponent, and so useContext(LoginContext) returns nothing.

HOC to the rescue! What if I added a tiny middleman:

const withLogin = Component => ({ ...props }) => (
<WithLogin>
<Component {...props} />
</WithLogin>
);

And then:

const MyComponent = () => {
const login = useContext(LoginContext);


const doLogin = useCallback(() => {
login("a@b.c", "password");
}, [login]);


return (
<button type="button" onClick={doLogin}>
Login!
</button>
);
};


const MyComponentWithLogin = withLogin(MyComponent);

Bam! MyComponentWithLogin will now work as expected.

This may well not be the best way to approach this particular situation, but I kinda like it.

And yes, it really is just an extra function call, nothing more! According to the official guide:

HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.

The following is an over simplified example of using HOC with functional components.

The functional component to be "wrapped":

import React from 'react'
import withClasses from '../withClasses'


const ToBeWrappedByHOC = () => {
return (
<div>
<p>I'm wrapped by a higher order component</p>
</div>
)
}


export default withClasses(ToBeWrappedByHOC, "myClassName");

The Higher Order Component:

import React from 'react'




const withClasses = (WrappedComponent, classes) => {
return (props) => (
<div className={classes}>
<WrappedComponent {...props} />
</div>
);
};


export default withClasses;

The component can be used in a different component like so.

<ToBeWrappedByHOC/>

I might be late to the party but here is my two-cent regarding the HOC

  • Creating HOC in a true react functional component way is kind of impossible because it is suggested not to call hook inside a nested function.

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in-depth below.)

Rules of Hooks

Here is what I have tried and failed

import React, { useState } from "react";


import "./styles.css";


function Component(props) {
console.log(props);
return (
<div>
<h2> Component Count {props.count}</h2>
<button onClick={props.handleClick}>Click</button>
</div>
);
}


function Component1(props) {
console.log(props);
return (
<div>
<h2> Component1 Count {props.count}</h2>
<button onClick={props.handleClick}>Click</button>
</div>
);
}


function HOC(WrapperFunction) {
return function (props) {
const handleClick = () => {
setCount(count + 1);
};


const [count, setCount] = useState(0);
return (
<WrapperFunction handleClick={handleClick} count={count} {...props} />
);
}
}


const Comp1 = HOC((props) => {
return <Component {...props} />;
});
const Comp2 = HOC((props) => {
return <Component1 {...props} />;
});


export default function App() {
return (
<div className="App">
<Comp1 name="hel" />
<Comp2 />
</div>
);
}


CodeSandBox

Even though the code works in codesandbox but it won't run in your local machine because of the above rule, you should get the following error if you try to run this code

React Hook "useState" cannot be called inside a callback

So to go around this I have done the following

import "./styles.css";
import * as React from "react";
//macbook
function Company(props) {
return (
<>
<h1>Company</h1>
<p>{props.count}</p>
<button onClick={() => props.increment()}>increment</button>
</>
);
}


function Developer(props) {
return (
<>
<h1>Developer</h1>
<p>{props.count}</p>
<button onClick={() => props.increment()}>increment</button>
</>
);
}


//decorator
function HOC(Component) {
// return function () {
//   const [data, setData] = React.useState();
//   return <Component />;
// };
class Wrapper extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<Component count={this.state.count} increment={this.handleClick} />
);
}
}
return Wrapper;
}


const NewCompany = HOC(Company);
const NewDeveloper = HOC(Developer);


export default function App() {
return (
<div className="App">
<NewCompany name={"Google"} />
<br />
<NewDeveloper />
</div>
);
}


CodeSandbox

I think for functional component this works fine

import {useEffect, useState} from 'react';


// Target Component
function Clock({ time }) {
return <h1>{time}</h1>
}


// HOC
function app(C) {
return (props) => {
const [time, setTime] = useState(new Date().toUTCString());
useEffect(() => {
setTimeout(() => setTime(new Date().toUTCString()), 1000);
})
return <C {...props} time={time}/>
}
}


export default app(Clock);

You can test it here: https://codesandbox.io/s/hoc-s6kmnv

Yes it is possible

import React, { useState } from 'react';


const WrapperCounter = OldComponent =>{
function WrapperCounter(props){
const[count,SetCount] = useState(0)
const incrementCounter = ()=>{
SetCount(count+1)
}
return(<OldComponent {...props} count={count} incrementCounter={incrementCounter}></OldComponent>)
}
return WrapperCounter
}


export default WrapperCounter

import React from 'react';
import WrapperCounter from './WrapperCounter';
function CounterFn({count,incrementCounter}){
return(
<button onClick={incrementCounter}>Counter inside functiona component {count}</button>
)
}


export default WrapperCounter(CounterFn)