【问题标题】:Alternative to useState inside of event handlers with useEffect?使用 useEffect 在事件处理程序中替代 useState?
【发布时间】:2021-10-23 06:10:57
【问题描述】:

我试图从事件处理程序(基本上是 onClick)每次渲染只调用一次 axios 调用,所以我使用 useEffect 并且在该 useEffect 内部,我使用的是 useState。问题是 - 调用 onClick 时,出现以下错误:

错误:无效的挂钩调用。 Hooks 只能在函数组件内部调用。

我明白我为什么会得到它,我在事件处理程序中使用 useState - 但我不知道还能做什么。如果没有 useState,我还能如何处理这些变量?

HttpRequest.js

import {useEffect, useState} from 'react'
import axios from 'axios'

export function useAxiosGet(path) {
    const [request, setRequest] = useState({
        loading: false,
        data: null,
        error: false
    });
    
    useEffect(() => {
        setRequest({
            loading: true,
            data: null,
            error: false
        });
        axios.get(path)
            .then(response => {
                setRequest({
                    loading: false,
                    data: response.data,
                    error: false
                })
            })
            .catch((err) => {
                setRequest({
                    loading: false,
                    data: null,
                    error: true
                });
                
                if (err.response) {
                    console.log(err.response.data);
                    console.log(err.response.status);
                    console.log(err.response.headers);
                } else if (err.request) {
                    console.log(err.request);
                } else {
                    console.log('Error', err.message);
                }
                console.log(err.config);
            })
    }, [path])
    
    return request
}

RandomItem.js

import React, {useCallback, useEffect, useState} from 'react';
import Item from "../components/Item";
import Loader from "../../shared/components/UI/Loader";
import {useAxiosGet} from "../../shared/hooks/HttpRequest";
import {useLongPress} from 'use-long-press';

function collectItem(item) {
    return useAxiosGet('collection')
}

function RandomItem() {
    let content = null;
    let item;
    
    item = useAxiosGet('collection');
    console.log(item);
    
    const callback = useCallback(event => {
        console.log("long pressed!");
    }, []);
    const longPressEvent = useLongPress(callback, {
        onStart: event => console.log('Press started'),
        onFinish: event => console.log('Long press finished'),
        onCancel: event => collectItem(),
        //onMove: event => console.log('Detected mouse or touch movement'),
        threshold: 500,
        captureEvent: true,
        cancelOnMovement: false,
        detect: 'both',
    });
    
    if (item.error === true) {
        content = <p>There was an error retrieving a random item.</p>
    }
    
    if (item.loading === true) {
        content = <Loader/>
    }
    
    if (item.data) {
        return (
            content =
                <div {...longPressEvent}>
                    <Item name={item.data.name} image={item.data.filename} description={item.data.description}/>
                </div>
        )
    }
    
    return (
        <div>
            {content}
        </div>
    );
}

export default RandomItem;

use-long-press

它可以很好地加载第一个项目,但是当您尝试取消长按(基本上是 onClick 事件处理程序)时,它会吐出上面的错误。

【问题讨论】:

  • @T.J.Crowder 我同意这很复杂,但我认为我有一个目标:这完全是关于 longPressEvent -> onCancel -> collectItem -> useAxiosGet 链。所以事件处理程序仅在某些事件发生后才尝试实例化钩子(onCancel
  • @skyboyer 这正是发生的事情,你长按一个 div,如果你不按住点击,onCancel 会被调用。 onCancel 调用 collectItem 并调用 useAxiosGet。 onCancel 本质上只是一个 onClick 事件处理程序。问题仅在于从事件处理程序调用的 useAxiosGet 函数。
  • @skyboyer - 啊,很好看。乔恩,minimal reproducible example 会让这很多更清楚。
  • 不是在useEffect 钩子中处理所有这些,您可以创建一个函数,在该函数内部处理它,然后返回该函数以及自定义useAxiosGet 中的所有数据钩。 Here's an example of how it can be done

标签: javascript reactjs


【解决方案1】:

一个不和谐的用户提供了这个解决方案:https://codesandbox.io/s/cool-frog-9vim0?file=/src/App.js

import { useState, useEffect, useCallback } from "react";
import axios from "axios";
import "./styles.css";

const fetchDataFromApi = () => {
  return axios(
    `https://jsonplaceholder.typicode.com/todos/${
      1 + Math.floor(Math.random() * 10)
    }`
  ).then(({ data }) => data);
};

const MyComponent = () => {
  const [data, setData] = useState(undefined);
  const [canCall, setCanCall] = useState(true);

  const handler = {
    onClick: useCallback(() => {
      if (canCall) {
        setCanCall(false); // This makes it so you can't call more than once per button click
        fetchDataFromApi().then((data) => {
          setData(data);
          setCanCall(true); // Unlock button Click
        });
      }
    }, [canCall]),
    onLoad: useCallback(() => {
      if (canCall) {
        setCanCall(false); // This makes it so you can't call more than once per button click
        fetchDataFromApi().then((data) => {
          setData(data);
          setCanCall(true); // Unlock button Click
        });
      }
    }, [canCall])
  };

  useEffect(() => {
    handler.onLoad(); //initial call
  }, []);

  return (
    <div>
      <pre>{JSON.stringify(data, " ", 2)}</pre>
      <button disabled={!canCall} onClick={handler.onClick}>
        fetch my data!
      </button>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <MyComponent />
    </div>
  );
}

【讨论】:

    【解决方案2】:

    你需要重做你的钩子,这样它就不会无条件地开始加载,而是返回一个可能在某个时刻被调用来启动加载的回调:

    const [loadCollections, { isLoading, data, error }] = useLazyAxiosGet('collections');
    
    ....
      onCancel: loadCollections
    

    我建议在useQuery 立即开始加载和useLazyQuery 返回回调以稍后有条件地调用时关注approach that Apollo uses。但两者共享相似的 API,因此无需太多代码更新即可轻松替换。

    请注意,“立即”和“延迟”版本的区别不仅仅在于有条件地调用的能力。说,对于“懒惰”版本,您需要决定对回调的系列调用会发生什么 - 下一次调用是否应该依赖现有数据或重置并发送全新的调用。对于“立即”版本,没有这样的困境,因为组件将在生命周期内多次重新渲染,因此它绝对不应该每次都发送新请求。

    【讨论】:

    • 基本点是:不能从其他钩子或组件函数调用钩子,并且调用必须是同步的。这就是 React 知道组件使用什么钩子的方式。
    猜你喜欢
    • 1970-01-01
    • 2021-01-12
    • 2017-11-18
    • 2019-08-11
    • 2020-08-14
    • 1970-01-01
    • 2013-07-13
    • 1970-01-01
    • 2022-08-13
    相关资源
    最近更新 更多