reactjsaxiosreact-state

React custom hook issues


I am new to React. I have created a custom hook in react for fetching the data from api. Below is the code.

import { useState, useEffect } from 'react';
import useAxiosInstance from './useAxiosInstance';

const useGetApi = (initialurl = null, initialparams = {}) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [url, setUrl] = useState(initialurl);
  const [params, setParams] = useState(initialparams);
  const [loading, setLoading] = useState(false);
  const [refreshKey, setRefreshKey] = useState(true); // used bool state to refresh the data due to simplicity.
  const axiosInstance = useAxiosInstance();

  useEffect(() => {
    if (!url) return;
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await axiosInstance.get(url, {params});
        setData(response.data);
        setLoading(false);

      } catch (err) {
        setError({
          message: err.response?.data?.message || err.message || 'An unexpected error occurred',
          details: err.response?.data || null,
        });
      } finally {
        setLoading(false);
      }
    };


    fetchData();
  }, [url, JSON.stringify(params), refreshKey]);

  const triggerRefresh = () => setRefreshKey(prev=>!prev);

  return { data, error, loading, setUrl, setParams, triggerRefresh };
};

export default useGetApi;

The problem with this is that, the data still remains in the initial state(null) even if loading updates to false.it take some time to update the data variable? How to fix this so that the data is updated before the loading is made false again ?

In my component where I am using this hook, if I try the below, it does not work, the data is null initially!

//this fails
import React from 'react';
import useGetApi from '../hooks/requestMethods/useGetApi';

const PostList = () => {
  // Initialize the useGetApi hook with the API URL
  const { data, error, loading, triggerRefresh } = useGetApi('https://jsonplaceholder.typicode.com/posts');

  // Handle loading state
  if (loading) {
    return <p>Loading posts...</p>;
  }

  // Handle error state
  if (error) {
    return <p>Error fetching posts: {error.message}</p>;
  }

  // Render the list of posts
  return (
    <div>
      <h1>Posts</h1>
      <button onClick={triggerRefresh} disabled={loading}>
        {loading ? 'Refreshing...' : 'Refresh Posts'}
      </button>
      <ul>
        { data.map(post => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default PostList;

Error :

PostList.jsx:26 
 Uncaught TypeError: Cannot read properties of null (reading 'map')
    at PostList (PostList.jsx:26:16)
@react-refresh:247 
 The above error occurred in the <PostList> component:

    at PostList (http://localhost:5175/src/pages/PostList.jsx?t=1735830812705:22:52)
    at RenderedRoute (http://localhost:5175/node_modules/.vite/deps/react-router-dom.js?v=8d379189:4069:5)
    at Routes (http://localhost:5175/node_modules/.vite/deps/react-router-dom.js?v=8d379189:4539:5)
    at div
    at Router (http://localhost:5175/node_modules/.vite/deps/react-router-dom.js?v=8d379189:4482:15)
    at BrowserRouter (http://localhost:5175/node_modules/.vite/deps/react-router-dom.js?v=8d379189:5228:5)
    at Provider (http://localhost:5175/node_modules/.vite/deps/chunk-6DOCI2SE.js?v=8d379189:1097:3)
    at App

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.

Solution

  • The code is already well set. A conditional property access will improve the code. Please see below a version of an improved code.

    {data?.map((post) => (
      <li key={post.id}>
         <h2>{post.title}</h2>
          <p>{post.body}</p>
       </li>
     ))}
    

    The reason for proposing a conditional property access is discussed here.

    Let us see the rendering process step by step:

    1. On initial render of the App.
    2. The custom hook is invoked.
    3. It returns loading false and data null.
    4. Since loading is false, it skips its immediate return.
    5. And the render goes to the JSX which accesses map method of data.

    **** Since data now is null, it will end up in error ****

    Please take note that useEffect is an event which is not invoked during the render of the component. It only gets invoked right after the render.

    It means useEffect is invoked only after the JSX has been returned. Since useEffect has not been executed while rendering, the loading will remain false. Therefore the JSX which accesses map of data will fail since data is null. The conditional property access will safe guard the access and avoid the error.

    Now when the initial render is followed by useEffect, the state setter invoked in the useEffect will trigger another render. This is the second render altogether. During this render, loading will be true, and data will have value, provided there is no error. Therefore the conditional access will pass the test, it will proceed with map function, and prepare the JSX successfully. This second render will also be followed by another useEffect, and the dependency check then will prevent it from running the code inside it.

    Please see a sample code illustrating the sequence of actions with useEffect.

    App.js

    import React from 'react';
    
    export default function App() {
      const [someState, setSomeState] = React.useState();
    
      console.log('Rendering the component');
      React.useEffect(() => {
        console.log('Running the useEffect code');
        if (!someState) {
          setSomeState('X');
        }
      }, [someState]);
    
      return 'Please check console log to see the Rendering sequences';
    }
    

    Test run

    Console log generated on loading the app

    // Rendering the component
    // Running the useEffect code
    // Rendering the component
    // Running the useEffect code