Home > OS >  Custom react hook with useeffect, cant use in non-component function
Custom react hook with useeffect, cant use in non-component function

Time:10-18

I made a custom react hook, which has a useEffect and, for now, returns a set of different states. It's a hook for axios, and the gist of it is this:

export default function useAxios({ url, data = {}, method = "GET"} ) {
  var [loading, setLoading] = useState(true)
  var [data, setData] = useState(null)

  useEffect(function() {
    (async function() {
      // do axios request and set loading and data states
    })()

    return () => // cleanup function that cancels axios request
  }, [])

  return {
    loading,
    data
  }
}

Now, in a simple component I can easily use this custom hook - but my question is: What if I want to use my hook inside an event handler, say:

export default MyComponent() {
  function handleSubmit(e) {
    var { data } = useAxios({
      url: "/my-end-point",
      data: {
        testInput: e.target.testInput.value
      }
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="testInput" />
      <button type="submit">Submit</button>
    </form>
  )
}

Problem is my useAxios hook has a useEffect, and so I cannot use it inside a non-component function, i.e. handleSubmit. So what is the work around? Is there even one? Thanks in advance.

CodePudding user response:

I would take a look at popular libraries like SWR (useSWR) and apollo-client (useQuery). They're approach is something like this when making get requests

const MyComponent = () => {
  const [shouldSkip, setShouldSkip] = useState(true);
  const queryResult = useQuery('my-url', {skip: shouldSkip});

  const handleSubmit = () => {
    setShouldSkip(false);
    // this will cause the component to rerender, and skip will now be false
  }

}

When making post requests, its something like this:

const MyComponent = () => {
  //useMutation returns a callable function whenever you want
  const callFunction = useMutation('my-url');

  const handleSubmit = () => {
    await callFunction()
  }

}

You can also take a look at axios-specific hooks like https://github.com/simoneb/axios-hooks, another common pattern they use is to include a refetch function as a result of the hook, that can be called at anytime (like in an event handler)

CodePudding user response:

As to React's Only Call Hooks from React Functions, you should always:

✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks.

Fail to satisfy these two rules leads to unexpected render result out of React.

With those rules in mind, you should return a submitHanlder from react hook instead of just passing the hook function into another component as a callback function.

I might guess that your intention is to trigger the axios request on the submit event. If so, it is possible to achieve that without passing whole hook into event handler.

First of all, as the rules say, you have to make sure your hook got called in every render. So the MyComponent can be rewrite in the below way:

export default function MyComponent() {
  var startRequest = useAxios({url: "/my-end-point"}) //<---- useAxios now returns the startRequest function, and will always be called on every render

  return (
    <form onSubmit={(e) => {
      e.preventDefault()
      startRequest({testInput: e.target.testInput.value})  // <----- call your startRequest here in the submit hanlder
      .then(data => {
        //process your data here
      })
    }}>
      <input type="text" name="testInput" />
      <button type="submit">Submit</button>
    </form>
  )
}

Please note that now the hook returns a function startRequest which you can put in your handler, and trigger that handler any time appropriated.

And rearrange your hook's code like below:

export function useAxios({ url, method = "GET"} ) {
  var [loading, setLoading] = useState(true)  
                                            // <------ no setData here

  var startRequest = async function(body = {}) { // <------ Move your input here
    // do axios request and set loading and data states
    setLoading(true)
    await data = axios.post(body)
    setLoading(false)
    return data             // <------- return data as promise
  }

  var cancelRequest = () => // cleanup function that cancels axios request

  useEffect(function() {
    return cancelRequest
  }, []) // useEffect only helps your cancel request on unmounted.

  return startRequest
}

The useEffect now only helps you cleanup axios request without the need to start one, since firing a request should be an event handler's job.

And since the data return by axios is in a promise, you don't need to explicitly setData to store your response data so I removed the line of useState(null).

CodePudding user response:

The point of the hook is not to make the request for you, the point of the hook is to communicate the internal state of stuff (the axios request, in your case) to the component, so that you can render stuff based around that state (like loading states, or the data).

In your case, you can change the value of the query based on the component state, and have the hook return the data to the component based on its parameters. Something like this:

const useAxios = ({ query }) => {
    var [loading, setLoading] = useState(true)
    var [data, setData] = useState(null)

    useEffect(function () {
        (async function () {
            setLoading(true)
            // do axios request and set loading and data states
            const request = await axios.get('endpoint', { query })

            setData(request.data)
            setLoading(false)
        })()

        return () => { }// cleanup function that cancels axios request
    }, [])

    return {
        loading,
        data
    }
}

const Component = () => {
    const [query, setQuery] = useState('')
    const { loading, data } = useAxios({ query });

    const submitHandler = (event) => { setQuery(event.target.testInput.value) }

    return (
        <>
            <form onSubmit={submitHandler}>
                <input name="testInput" />
                <input type="submit" />
            </form>
            {loading && (
                <>a spinner</>
            )}
            {data && (
                <DataRenderer data={data} />
            )}
        </>
    )
}
  • Related