React - Can't use hooks in custom hooks

Viewed 7555

I have weird issue. I'm trying to create new hooks in reactjs. But i cannot use useEffect, useState and other React Hooks in my custom hook. I'm researching it but i couldn't find any solution. Here is my custom hook:

export function useStore(initialState) {
  const [state, setState] = useState(initialState);  
  const store = state;

  return store;
}

When im trying to use: useStore('foo'); i get this error:

Error: 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 error link to react (I can't share link...) for tips about how to debug and fix this problem.

I also tried to delete useState hook in my code. And it worked that's why in my point of view that problem is hooks. Can someone help me?

Thank you :)

3 Answers

useCounter, useSlider, usePicker

You can write hooks using other hooks. Consider these examples -

app preview

A custom useCounter hook shows us how we can write small child components which send their state upwards to the parent component -

const useCounter = (init = 0) => {
  const [value, setValue] = useState(init)
  return [
    value, 
    <div>
      <button onClick={_ => setValue(value - 1)}>-</button>
      {value}
      <button onClick={_ => setValue(value + 1)}>+</button>
    </div>
  ]
}

In MyApp, when useCounter is called, we get the stateful value and the stateful counter component -

const initialOrder =
  { muffin: 0, cakes: 0, pies: 0, ... }

const MyApp = ({ order = initialOrder }) => {
  const [muffins, muffinCounter] = useCounter(order.muffins)
  const [cakes, cakeCounter] = useCounter(order.cakes)
  const [pies, piesCounter] = useCounter(order.pies)

  // ...

  return <ul>
    <li>muffins: {muffinCounter}</li>
    <li>cakes: {cakeCounter}</li>
    <li>pies: {piesCounter}</li>
    // ...
  </ul>
}

We can make similar components like this useSlider custom hook -

const useSlider = ({ min = 0, max = 0, init = 0 }) => {
  const [value, setValue] = useState(init)
  return [
    value, 
    <input
      type="range"
      min={min}
      max={max}
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  ]
}

And custom hooks made out of custom hooks, usePicker -

const usePicker = ({ values = [], init = 0 }) =>
  useSlider({ min: 0, max: values.length - 1, init })
const MyApp = ({ order = initialOrder }) => {
  const [muffins, muffinCounter] = useCounter(order.muffins)
  const [cakes, cakeCounter] = useCounter(order.cakes)
  const [pies, piesCounter] = useCounter(order.pies)
  const speeds = ["slow", "medium", "fast", "same-day"]
  const [deliverySpeed, speedPicker] = usePicker({ values: speeds, init: order.deliverySpeed })
  
  // ...
  
  return <ul>
    <li>muffins: {muffinCounter}</li>
    <li>cakes: {cakeCounter}</li>
    <li>pies: {piesCounter}</li>
    <li>speed: {speedPicker} {speeds[deliverySpeed]}</li>
    // ...
  </ul>
}

Run the code below to see useCounter, useSlider, and usePicker in action -

const { useState, useEffect } = React

const useCounter = (init = 0) => {
  const [value, setValue] = useState(init)
  return [
    value, 
    <div>
      <button onClick={_ => setValue(value - 1)}>-</button>
      {value}
      <button onClick={_ => setValue(value + 1)}>+</button>
    </div>
  ]
}

const useSlider = ({ min = 0, max = 0, init = 0 }) => {
  const [value, setValue] = useState(init)
  return [
    value, 
    <input type="range" min={min} max={max} value={value} onChange={e => setValue(e.target.value)} />
  ]
}

const usePicker = ({ values = [], init = 0 }) =>
  useSlider({ min: 0, max: values.length - 1, init })

const initialOrder =
  { muffin: 0, cakes: 0, pies: 0, deliverySpeed: 3 }

const MyApp = ({ order = initialOrder }) => {
  const [muffins, muffinCounter] = useCounter(order.muffins)
  const [cakes, cakeCounter] = useCounter(order.cakes)
  const [pies, piesCounter] = useCounter(order.pies)
  const speeds = ["slow", "medium", "fast", "same-day"]
  const [deliverySpeed, speedPicker] = usePicker({ values: speeds, init: order.deliverySpeed })
  const onSubmit = e =>
    alert("submitted: " + JSON.stringify({ muffins, cakes, pies, deliverySpeed }))
  return <ul>
    <li>muffins: {muffinCounter}</li>
    <li>cakes: {cakeCounter}</li>
    <li>pies: {piesCounter}</li>
    <li>speed: {speedPicker} {speeds[deliverySpeed]} </li>
    <li>{JSON.stringify({ muffins, cakes, pies, deliverySpeed })}</li>
    <li><button onClick={onSubmit}>submit</button></li>
  </ul>
}

ReactDOM.render(<MyApp />, document.body)
body { margin: 0 auto; font-family: monospace; }
ul { padding: 0; }
li { list-style-type: none; background-color: ghostwhite; padding: 0.25rem; margin-bottom: 0.25rem; }
li div { display: inline-block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>


example: useAsync, useJson

A custom useAsync hook allows us to declaratively interact with an asynchronous resources -

function useAsync(func, [_0, _1, _2, _3, _4]) {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const [result, setResult] = useState(null)
  useEffect(_ => {
    setLoading(true)
    Promise.resolve(func(_0, _1, _2, _3, _4))
      .then(setResult, setError)
      .finally(_ => setLoading(false))
  }, [func, _0, _1, _2, _3, _4])
  return {loading, error, result}
}

In a typical example we just want to do a fetch request and parse some JSON. Another custom hook useJson built using our first custom hook, useAsync -

const fetchJson = (url = "") =>
  fetch(url).then(r => r.json())

const useJson = (url = "") =>
  useAsync(fetchJson, [url])

Using useJson in MyComponent looks like this -

const MyComponent = ({ url = "" }) => {
  const { loading, error, result } = useJson(url) // ✨

  if (loading)
    return <pre>loading...</pre>

  if (error)
    return <pre style={{color: "tomato"}}>error: {error.message}</pre>

  return <pre>result: {result}</pre>
}

Run the code demo below to see useJson in action -

useJson

const { useState, useEffect } = React

// fake fetch slows response down so we can see loading
const _fetch = (url = "") =>
  fetch(url).then(x => new Promise(r => setTimeout(r, 2000, x)))

function useAsync(func, [_0, _1, _2, _3, _4]) {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const [result, setResult] = useState(null)
  useEffect(_ => {
    setLoading(true)
    Promise.resolve(func(_0, _1, _2, _3, _4))
      .then(setResult, setError)
      .finally(_ => setLoading(false))
  }, [func, _0, _1, _2, _3, _4])
  return {loading, error, result}
}

const fetchJson = (url = "") =>
  _fetch(url).then(r => r.json())

const useJson = (url = "") =>
  useAsync(fetchJson, [url])

const MyComponent = ({ url = "" }) => {
  const { loading, error, result } = useJson(url)
  if (loading)
    return <pre>loading...</pre>
  if (error)
    return <pre style={{color: "tomato"}}>error: {error.message}</pre>
  return <pre>result: {JSON.stringify(result, null, 2)}</pre>
}

const MyApp = () =>
  <main>
    ex 1 (success):
    <MyComponent url="https://httpbin.org/get?foo=bar" />
    ex 2 (error):
    <MyComponent url="https://httpbin.org/status/500" />
  </main>

ReactDOM.render(<MyApp />, document.body)
pre { background: ghostwhite; padding: 1rem; white-space: pre-wrap; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

Enjoy your cupcakes!

I guess this about where you call useStore from - which you don't show. That call should be in React Function or another custom hook, but probably isn't.

Hooks can be called from within custom hooks as stated here:

Don’t call Hooks from regular JavaScript functions. Instead, you can: ... Call Hooks from custom Hooks

https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions.

It works when removing useState as it is this builtin hook that actually issues the error (see https://github.com/facebook/react/blob/3278d242184a13add3f25f683b77ef9a6a2305f3/packages/react/src/ReactHooks.js).

This is by design. You can only call/use a hook from within a React Function. See more here https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions.

You will want to re-think your approach. Use the different hooks within a component and not within a hook.

import React, {useState} from 'react';
import {useCustomHookOne} from '../hooks/custom-hook';
import {useCustomHookTwo} from '../hooks/custom-hook';
export default function MyComponent(props) {
    const [state, actions] = useCustomHookOne(initialState, {useState});
    const [state, actions] = useCustomHookTwo(initialState, {useState});
}
Related