React-无法在自定义钩子中使用钩子

时间:2020-05-11 20:19:42

标签: reactjs react-hooks

我有个奇怪的问题。我正在尝试在reactjs中创建新的钩子。但是我不能在自定义钩子中使用useEffect,useState和其他React Hooks。我正在研究它,但找不到任何解决方案。这是我的自定义钩子:

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

  return store;
}

当我尝试使用时:useStore('foo');我收到此错误:

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.

我还尝试在代码中删除useState挂钩。在我看来,这就是为什么这是钩子的原因。有人可以帮我吗?

谢谢:)

3 个答案:

答案 0 :(得分:0)

我猜这是关于您从何处呼叫useStore的信息-您未显示。该调用应该在React Function或另一个自定义钩子中,但可能不是。

可以在自定义钩子中调用钩子,如下所示:

请勿通过常规JavaScript函数调用Hook。相反,您可以: ...从自定义挂钩调用挂钩

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

它在删除useState时有效,因为实际上是此内置钩子发出了错误(请参见https://github.com/facebook/react/blob/3278d242184a13add3f25f683b77ef9a6a2305f3/packages/react/src/ReactHooks.js)。

答案 1 :(得分:0)

您可以在自定义钩子中使用钩子。考虑这两个例子。

ex1:从子组件中取出状态

自定义useCounter挂钩向我们展示了如何编写小的子组件,这些子组件将其状态向上发送给父组件-

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>
  ]
}

MyApp中,当调用useCounter时,我们得到有状态值有状态计数器组件-

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>
}

我们可以制作类似useSlider个自定义钩子的组件-

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

由定制钩子制成的定制钩子,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: {speeds[deliverySpeed]} {speedPicker}</li>
    // ...
  </ul>
}

运行以下代码以查看useCounteruseSliderusePicker的使用情况-

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, 
    <div>
      <input
        type="range"
        min={min}
        max={max}
        value={value}
        onChange={e => setValue(e.target.value)}
      />
    </div>
  ]
}

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 =>
    console.log("submitted", { muffins, cakes, pies, deliverySpeed })
  
  return <ul>
    <li>muffins: {muffinCounter}</li>
    <li>cakes: {cakeCounter}</li>
    <li>pies: {piesCounter}</li>
    <li>speed: {speeds[deliverySpeed]} {speedPicker}</li>
    <li>{JSON.stringify({ muffins, cakes, pies, deliverySpeed })}</li>
    <li><button onClick={onSubmit}>submit</button> (open console to read output)</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;
}
<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>


ex2:useJson

自定义useAsync钩子使我们可以声明性地与异步资源进行交互-

const identity = x => x

const useAsync = (runAsync = identity, deps = []) => {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const [result, setResult] = useState(null)

  useEffect(_ => { 
    Promise.resolve(runAsync(...deps))
      .then(setResult, setError)
      .catch(e => setError(e))
      .finally(_ => setLoading(false))
  }, deps)

  return { loading, error, result }
}

通常,我们只想执行一个fetch请求并解析一些JSON。使用我们的第一个自定义钩子useJson-

构建的另一个自定义钩子useAsync
const fetchJson = (url = "") =>
  fetch(url).then(r => r.json())

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

useJson中使用MyComponent看起来像这样-

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

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

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

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

运行下面的代码演示以查看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)))

const identity = x => x

const useAsync = (runAsync = identity, deps = []) => {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const [result, setResult] = useState(null)

  useEffect(_ => { 
    Promise.resolve(runAsync(...deps))
      .then(setResult, setError)
      .catch(e => setError(e))
      .finally(_ => setLoading(false))
  }, deps)

  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>

答案 2 :(得分:-1)

这是设计使然。您只能在React函数中调用/使用钩子。在https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions处查看更多。

您将要重新考虑您的方法。在组件而不是挂钩中使用不同的挂钩。

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});
}