DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Max Frolov
Max Frolov

Posted on

Reusable logic with React HOC

In the previous post, I've covered Reusable logic with React Render Props .

This time I will tell you how:
a) to use HOC correctly and which cases it should be used in;
b) to take out the logic that can be reused in other components.

Thanks to the HOC (higher-order component) pattern we can easily create "modules" that can both accept options for internal use and passing additional functionality. This functionality will help to implement many things without describing the same logic within each component where it is used.

So HOC is a higher-order component aka a higher-order function which takes a component as an argument and returns another component. The returned component render will contain the passed component but with more advanced functionality.

It is important to pass the received props inside the EnhancedComponent further to the ComposedComponent. Otherwise, all props that we will pass externally will remain closed at the EnhancedComponent level.

The main tasks of HOC are:

a) to expand the functionality of the wrapped component;
b) to store logic that can be reused.

A brief description of the HOC can be written as follows:
(component) => (props) => { ...logic return extendedComponent }.

How to understand the moment to use HOC has come?

Let's imagine we have two pages for a registered user.

On every page we need:
a) to understand whether the user is authorized in the system;
b) to obtain user profile data if authorized.

As a comparison let's firstly write logic without using HOC.

First page:

const PageFirst = () => {
    // state
    const [isUserLoading, setUserLoadingState] = React.useState(false)
    const [userProfile, setUserProfile] = React.useState({ isAuthorized: false, data: {} })

    React.useEffect(() => {
      handleGetUser()
    }, [])

    const handleGetUser = async () => {
      try {
        setUserLoadingState(true)

        const response = await getUser()

        setUserProfile({ isAuthorized: true, data: response.data })
      } catch (error) {
        console.log('Error while User preload:', error)
      } finally {
        setUserLoadingState(false)
      }
    }

    if (!userProfile.isAuthorized && !isUserLoading) {
      return <div>U're not authorized</div>
    }

    return (
      <div>
        {isUserLoading ? (
          <div>Loading...</div>
        ) : (
          <>
            <div>Your First Name: {userProfile.data.firstName}</div>
            <div>Your Last Name: {userProfile.data.lastName}</div>
          </>
        )}
      </div>
    )
  }

Second page:

const PageSecond = () => {
    // state
    const [isUserLoading, setUserLoadingState] = React.useState(false)
    const [userProfile, setUserProfile] = React.useState({ isAuthorized: false, data: {} })

    React.useEffect(() => {
      handleGetUser()
    }, [])

    const handleGetUser = async () => {
      try {
        setUserLoadingState(true)

        const response = await getUser()

        setUserProfile({ isAuthorized: true, data: response.data })
      } catch (error) {
        console.log('Error while User preload:', error)
      } finally {
        setUserLoadingState(false)
      }
    }

    if (!userProfile.isAuthorized && !isUserLoading) {
      return <div>U're not authorized</div>
    }

    return (
      <div>
        {isUserLoading ? (
          <div>Loading...</div>
        ) : (
          <div>
            Your Full Name: {userProfile.data.firstName} {userProfile.data.lastName}
          </div>
        )}
      </div>
    )
  }

As we see in the example above we can take out:

a) a function to obtain a user;
b) a state of userProfile and isUserLoading;
c) repeating condition for rendering a message the user is not authorized and a user loading message. (message about user loading)

Let's try to move these elements to the HOC (withAuth). Usually, the prefix β€œwith” is used in the name of the HOC.

HOC withAuth:

const withAuth = ComposedComponent => {
    const EnhancedComponent = (props) => {
      // state
      const [isUserLoading, setUserLoadingState] = React.useState(false)
      const [userProfile, setUserProfile] = React.useState({ isAuthorized: false, data: {} })

      React.useEffect(() => {
        handleGetUser()
      }, [])

      const handleGetUser = async () => {
        try {
          setUserLoadingState(true)

          const response = await getUser()

          setUserProfile({ isAuthorized: true, data: response.data })
        } catch (error) {
          console.log('Error while User preload:', error)
        } finally {
          setUserLoadingState(false)
        }
      }

      if (!userProfile.isAuthorized && !isUserLoading) {
        return <div>U're not authorized</div>
      }

      return <>{isUserLoading ? <div>Loading...</div> : <ComposedComponent {...props} userProfile={userProfile} />}</>
    }

    return EnhancedComponent
  }

  const PageFirst = withAuth(({ userProfile }) => (
    <>
      <div>Your First Name: {userProfile.data.firstName}</div>
      <div>Your Last Name: {userProfile.data.lastName}</div>
      <div>Is Authorized: {userProfile.isAuthorized ? 'Yes' : 'No'}</div>
    </>
  ))

  const PageSecond = withAuth(({ userProfile }) => (
    <div>
      Your Full Name: {userProfile.data.firstName} {userProfile.data.lastName}
    </div>
  ))

Now we have HOC withAuth which took upon all the logic of getting userProfile. In order to get profile data inside the component, it is enough to wrap our component in withAuth. Optimization helped us to reduce the code by almost half: from 80 rows to 47.

In order to pass additional parameters to the HOC you need to use a higher-order function.

Short description:
(...arguments) => (component) => (props) => { ...logic return extendedComponent }.

Example of parameter passing to HOC:

// higher order functions
  const withAuth = (options = { isAdmin: false }) => ComposedComponent => {
    const EnhancedComponent = (props) => {
      // state
      const [isUserLoading, setUserLoadingState] = React.useState(false)
      const [userProfile, setUserProfile] = React.useState({ isAuthorized: false, data: {} })

      React.useEffect(() => {
        handleGetUser()
      }, [])

      const handleGetUser = async () => {
        try {
          setUserLoadingState(true)

          const response = await getUser(options.isAdmin)

          setUserProfile({ isAuthorized: true, data: response.data })
        } catch (error) {
          console.log('Error while User preload:', error)
        } finally {
          setUserLoadingState(false)
        }
      }

      if (!userProfile.isAuthorized && !isUserLoading) {
        return <div>U're not authorized</div>
      }

      return <>{isUserLoading ? <div>Loading...</div> : <ComposedComponent {...props} userProfile={userProfile} />}</>
    }

    return EnhancedComponent
  }

  // passing options
  const PageFirst = withAuth({ isAdmin: true })(({ userProfile }) => (
    <>
      <div>Your First Name: {userProfile.data.firstName}</div>
      <div>Your Last Name: {userProfile.data.lastName}</div>
      <div>Is Authorized: {userProfile.isAuthorized ? 'Yes' : 'No'}</div>
    </>
  ))

Certainly, it is possible not to create another function and pass options by a second argument on the first call together with the component. But this will not be quite correct from the composition point of view.

In the case of HOC it is better not to mix the component transfer along with the options but to separate them by passing them to each function separately. This is a more flexible option since we can close certain options and use HOC by passing the necessary parameters to it in advance.

Example of parameter closure in HOC:

const withAuthAdmin = withAuth({ isAdmin: true })
  const withAuthDefault = withAuth({})

  const PageFirst = withAuthAdmin(({ userProfile }) => (
    <>
      <div>Your First Name: {userProfile.data.firstName}</div>
      <div>Your Last Name: {userProfile.data.lastName}</div>
      <div>Is Authorized: {userProfile.isAuthorized ? 'Yes' : 'No'}</div>
    </>
  ))

  const PageSecond = withAuthDefault(({ userProfile }) => (
    <div>
      Your Full Name: {userProfile.data.firstName} {userProfile.data.lastName}
    </div>
  ))

The HOC can also return a component wrapped in another HOC.

When we turn the EnhancedComponent to the HOC we will access all the functionality of the HOC inside the EnhancedComponent, through props. Then we can decide whether to pass it to the ComposedComponent or not.

Example of using HOC inside HOC:

const withLoadingState = ComposedComponent => props => {
    // state
    const [isUserLoading, setUserLoadingState] = React.useState(false)

    const handleSetUserLoading = value => {
      setUserLoadingState(value)
    }

    return <ComposedComponent {...props} isUserLoading={isUserLoading} handleSetUserLoading={handleSetUserLoading} />
  }

  const withAuth = ComposedComponent => {
    const EnhancedComponent = ({ isUserLoading, handleSetUserLoading, ...rest }) => {
      // state
      const [userProfile, setUserProfile] = React.useState({ isAuthorized: false, data: {} })

      React.useEffect(() => {
        handleGetUser()
      }, [])

      const handleGetUser = async () => {
        try {
          handleSetUserLoading(true)

          const response = await getUser()

          setUserProfile({ isAuthorized: true, data: response.data })
        } catch (error) {
          console.log('Error while User preload:', error)
        } finally {
          handleSetUserLoading(false)
        }
      }

      if (!userProfile.isAuthorized && !isUserLoading) {
        return <div>U're not authorized</div>
      }

      return <>{isUserLoading ? <div>Loading...</div> : <ComposedComponent {...rest} userProfile={userProfile} />}</>
    }

    // here we wrap EnhancedComponent with HOC
    return withLoadingState(EnhancedComponent)
  }

I believe after reading this post you will think where you could use HOC in your current or future project.

More tips and best practices on my Twitter.
More tutorials here.

Feedback is appreciated. Cheers!

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.