Jon
Jon

Reputation: 385

Alternative to useState inside of event handlers with useEffect?

I'm trying to only call an axios call once per render from an event handler (onClick basically), so I'm using useEffect and inside that useEffect, I'm using useState. Problem is - when onClick is called, I get the following error:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component.

I understand why I'm getting it, I'm using useState in an event handler - but I don't know what else to do. How else can I handle these variables without 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

It works to load up the first item just fine, but when you try to cancel a long click (Basically the onClick event handler), it spits out the error above.

Upvotes: 0

Views: 735

Answers (2)

Jon
Jon

Reputation: 385

A user in discord provided this solution: 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>
  );
}

Upvotes: 0

skyboyer
skyboyer

Reputation: 23705

You need to redo your hook so it would not start loading unconditionally but instead return a callback that might be called to initiate loading at some moment:

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

....
  onCancel: loadCollections

I propose to follow approach that Apollo uses when there is useQuery that starts loading instantly and useLazyQuery that returns callback to be called later and conditionally. But both share similar API so could be easily replaced without much updates in code.

Just beware that "immediate" and "lazy" version don't just differ by ability to be called conditionally. Say, for "lazy" version you need to decide what will happen on series calls to callback - should next call rely on existing data or reset and send brand new call. For "immediate" version there are no such a dilemma since component will be re-rendered for multiple times per lifetime, so it definitely should not send new requests each time.

Upvotes: 1

Related Questions