skip to Main Content

I have a hook that I use in my components to access an external resource that is fetched through Ajax and saved in the Redux store. What I want is for this hook to handle fetching the resource if it’s not already in the store, but simply returning the resource data if it is. However, I cannot get this to work, as the fetch is performed for every time I call the hook in different components.

This is how my Redux slice looks:

export const fetchResource = createAsyncThunk(
  'slice/resource',
  async () => {
      const { data } = await axios.get(
        'api.com/resource',
      );
      return data;
  },
);


const slice = createSlice({
  name: 'slice',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchResource.pending, (state) => {
        state.resource.status = 'loading';
      })
      .addCase(fetchResource.fulfilled, (state, action) => {
        state.resource.status = 'success';
        state.resource.data = action.payload;
      })
      .addCase(fetchResource.rejected, (state) => {
        state.resource.status = 'error';
      });
  },
});

Then this is my hook:

export function useResource(initialFetch = false) {
 
  const dispatch = useDispatch();
  const status = useSelector(state => state.resource.status);
  const resource = useSelector(state => state.resource.resource);
  const refreshResource = useCallback(() => {
     dispatch(fetchResource());
  }, [dispatch]);

  useEffect(() => {
     if (initialFetch && status === 'idle') refreshResource();
  }, [initialFetch, status, refreshResource]);

  return {
    status,
    resource,
    refreshResource,
  }
}

This hook is meant so that if, in any component, I call useResource(true), the hook will automatically fetch the resource and return it if it’s missing in the store, or simply return it if it’s found. As a side note, I’m wrapping the dispatch in the refreshResource so that I can return this function in the hook so that components will later be able to try to reload this resource if needed.

However, let’s say I have the components Child1 and Child2 that each include a call to useResource(true) , and then I have a Parent component that uses both Child1 and Child2 , the observed result is that the resource is fetched twice. I assumed this wouldn’t happen as I added some logic for the resource to only be fetched if the fetching status is idle , and all components should be looking at and modifying the same status, right?

Why isn’t this working and is there any way to fix it?

Also, this is not caused by StrictMode, as if I change any of the two calls to useResource(true) to be useResource(false), I do get only one fetch.

A workaround I’m using right now is to call useResource(false) in the child components and useResource(true) in the Parent component, but this is not ideal as I want the child components to be independent of the parent, and to be able to reuse them in other components without having to worry about whether the resource has been fetched or not.

2

Answers


  1. I’m not actually sure why your code would not work, but the hook looks a bit convoluted. I’ve written similar code before so I think something like this should do the trick:

    export const fetchResource = createAsyncThunk(
      'slice/resource',
      async (arg, thunkAPI) => {
        const state = thunkAPI.getState();
        const { data, status } = state.resource;
        
        if (!data && status !== 'loading') {
          // Store is empty and there is no other pending request.
          const response = await axios.get(
            'api.com/resource',
          );
          return response.data; 
        }
        
        return data;
      },
    );
    
    export function useResource() {
      const dispatch = useDispatch();
      const { data, status } = useSelector(state => state.resource);
    
      const refreshResource = () => dispatch(fetchResource());
    
      useEffect(() => {
        refreshResource();
      }, []);
    
      return {
        status,
        data,
        refreshResource,
      }
    }
    

    The effect does the initial fetch in case the store is empty. You don’t need to add dispatch to a dependency array btw, it’s a stable reference. Another option is to do the "is it in the store already?" check before calling refreshResource() in the effect. Then the action wouldn’t be dispatched, which is actually cleaner. I’ll leave that up to you.

    Login or Signup to reply.
  2. I believe you should look into Redux Toolkit Query.

    RTK Query is a powerful data fetching and caching tool. It is designed to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself. The data fetching and caching logic is built on top of Redux Toolkit’s createSlice and createAsyncThunk APIs.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search