HoC with React Hooks
Asked Answered
A

2

11

I'm trying to port from class component to react hooks with Context API, and I can't figure out what is the specific reason of getting the error.

First, my Codes:

// contexts/sample.jsx
import React, { createContext, useState, useContext } from 'react'
const SampleCtx = createContext()

const SampleProvider = (props) => {
  const [ value, setValue ] = useState('Default Value')
  const sampleContext = { value, setValue }
  return (
    <SampleCtx.Provider value={sampleContext}>
      {props.children}
    </SampleCtx.Provider>
  )
}

const useSample = (WrappedComponent) => {
  const sampleCtx = useContext(SampleCtx)
  return (
    <SampleProvider>
      <WrappedComponent
        value={sampleCtx.value}
        setValue={sampleCtx.setValue} />
    </SampleProvider>
  )
}

export {
  useSample
}

// Sends.jsx
import React, { Component, useState, useEffect } from 'react'
import { useSample } from '../contexts/sample.jsx'

const Sends = (props) => {
  const [input, setInput ] = useState('')

  const handleChange = (e) => {
    setInput(e.target.value)
  }
  const handleSubmit = (e) => {
    e.preventDefault()

    props.setValue(input)
  }

  useEffect(() => {
    setInput(props.value)
  }, props.value)

  return (
    <form onSubmit={handleSubmit}>
      <input value={input} onChange={handleChange} />
      <button type="submit">Submit</button>
    </form>
  )
}

Error I got:

Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.

Explanation for my code:

I used Context API to manage the states, and previously I used class components to make the views. I hope the structure is straightforward that it doesn't need any more details.

I thought it should work as well, the <Sends /> component gets passed into useSample HoC function, and it gets wrapped with <SampleProvider> component of sample.jsx, so that <Sends /> can use the props provided by the SampleCtx context. But the result is failure.

Is it not valid to use the HoC pattern with React hooks? Or is it invalid to hand the mutation function(i.e. setValue made by useState()) to other components through props? Or, is it not valid to put 2 or more function components using hooks in a single file? Please correct me what is the specific reason.

Aerodyne answered 4/9, 2019 at 5:40 Comment(2)
@JosephD. I fixed the typo, and that is a trivial one. I didn't use <Consumer /> because I wanted to use useContext instead. Do I have to use <Consumer /> even if I use the useContext hook?Aerodyne
Nope. useContext() and <Consumer /> are equivalent.Soldierly
S
16

So HOCs and Context are different React concepts. Thus, let's break this into two.

Provider

Main responsibility of the provider is to provide the context values. The context values are consumed via useContext()

const SampleCtx = createContext({});

export const SampleProvider = props => {
  const [value, setValue] = useState("Default Value");
  const sampleContext = { value, setValue };

  useEffect(() => console.log("Context Value: ", value)); // only log when value changes

  return (
    <SampleCtx.Provider value={sampleContext}>
      {props.children}
    </SampleCtx.Provider>
  );
};

HOC

The consumer. Uses useContext() hook and adds additional props. Returns a new component.

const withSample = WrappedComponent => props => { // curry
  const sampleCtx = useContext(SampleCtx);
  return (
    <WrappedComponent
      {...props}
      value={sampleCtx.value}
      setValue={sampleCtx.setValue}
    />
  );
};

Then using the HOC:

export default withSample(Send)

Composing the provider and the consumers (HOC), we have:

import { SampleProvider } from "./provider";
import SampleHOCWithHooks from "./send";

import "./styles.css";

function App() {
  return (
    <div className="App">
      <SampleProvider>
        <SampleHOCWithHooks />
      </SampleProvider>
    </div>
  );
}

See Code Sandbox for full code.

Soldierly answered 4/9, 2019 at 7:36 Comment(2)
eslint will produce a warning for the HOC because hooks cannot be called inside callback methods. React Hook "useContext" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.eslint(react-hooks/rules-of-hooks)Commentate
2022: this error happens when HoC's have start with an upper case characterBanshee
G
6

Higher order Components are functions that takes a Component and returns another Component, and the returning Components can be class component, a Functional Component with hooks or it can have no statefull logic. In your example you're returning jsx from useSample.

const useSample = (WrappedComponent) => {
  const sampleCtx = useContext(SampleCtx)
  return ( // <-- here
    <SampleProvider>
      <WrappedComponent
        value={sampleCtx.value}
        setValue={sampleCtx.setValue} />
    </SampleProvider>
  )
}

if you want to make a HOC what you can do is something like this

const withSample = (WrappedComponent) => {
  return props => {
        const sampleCtx = useContext(SampleCtx)
        <WrappedComponent
            value={sampleCtx.value}
            setValue={sampleCtx.setValue} {...props} />
    }
}

Godfry answered 4/9, 2019 at 7:5 Comment(3)
I tried it, but in that case, the context doesn't get generated properly. sampleCtx is undefined, which means the useContext does not generate a context properly.Aerodyne
Plus, don't we have to still use <SampleProvider />?Aerodyne
provider should wrap the whole section of the app where you want to access the context state, you don't need to wrap each component in a provider. for example if you want this data to be available in your whole app then wrap your root component with the provider.Godfry

© 2022 - 2024 — McMap. All rights reserved.