Search code examples
javascriptreactjsreduxredux-thunkcore-ui

How to make dispatch call synchronous in useeffect?


i am trying to paginate the data from my rest server using CoreUI Table Component. i have problem getting the updated data from redux store after dispatch request in useEffect, i am using redux thunk, i know that dispatch is async, but is there a way to wait for the dispatch to be completed? i tired making the dispatch a Promise but it did not work.

I successfully get the updated result from action and reducer but in ProductsTable its the previous one, i checked redux devtools extension and i can see the state being changed.

i never get the latest value from store.

Also the dispatch is being called so many times i can see in the console window, it nots an infinite loop, it stops after sometime.

const ProductsTable = (props) => {


    const store = useSelector((state) => state.store);
    const dispatch = useDispatch();

    const [items, setItems] = useState([]);
    const [loading, setLoading] = useState(true);

    const [page, setPage] = useState(1);
    const [pages, setPages] = useState(1);
    const [itemsPerPage, setItemsPerPage] = useState(5);
    const [fetchTrigger, setFetchTrigger] = useState(0);


    useEffect(() => {
        setLoading(true);


        const payload = {
            params: {
                page,
            },
        };

        if (page !== 0)
            dispatch(getAllProducts(payload));

        console.log("runs:" + page)
        console.log(store.objects)

        if(!(Object.keys(store.objects).length === 0)){
            setItems(store.objects.results)
            setPages(store.objects.total.totalPages)
            setLoading(false)
        } else{
            console.log("error")
            setFetchTrigger(fetchTrigger + 1);
        }

    }, [page, fetchTrigger]);


    return (
        <CCard className="p-5">
            <CDataTable
                items={items}
                fields={["title", "slug", {
                    key: 'show_details',
                    label: '',
                    _style: { width: '1%' },
                    sorter: false,
                    filter: false
                }]}
                loading={loading}
                hover
                cleaner
                sorter
                itemsPerPage={itemsPerPage}
                onPaginationChange={setItemsPerPage}
              
            <CPagination
                pages={pages}
                activePage={page}
                onActivePageChange={setPage}
                className={pages < 2 ? "d-none" : ""}
            />



        </CCard>




    )
}


export default ProductsTable

Solution

  • The reason the ProductsTable always has the previous state data is because the effect you use to update the ProductsTable is missing the store as dependency or more specifically store.objects.results; when the page and the fetchTrigger change the effect becomes stale because it isn't aware that when those dependencies change the effect should change.

      useEffect(() => {
        // store.objects is a dependency that is not tracked
        if (!(Object.keys(store.objects).length === 0)) {
          // store.objects.results is a dependency that is not tracked
          setItems(store.objects.results);
          // store.objects.total.totalPages is a dependency that is not tracked
          setPages(store.objects.total.totalPages);
          setLoading(false);
        }  
        // add these dependencies to the effect so that everything works as expected
        // avoid stale closures 
      }, [page, fetchTrigger, store.objects, store.objects.results, store.objects.total.totalPages]);
    
    

    The dispatch is being called many times because you have a recursive case where fetchTrigger is a dependency of the effect but you also update it from within the effect. By removing that dependency you'll see much less calls to this effect, namely only when the page changes. I don't know what you need that value for because I dont see it used in the code you've shared, but if you do need it I recommend using the callback version of setState so that you can reference the value of fetchTrigger that you need without needing to add it as a dependency.

     useEffect(() => {
        // code
        if (!(Object.keys(store.objects).length === 0)) {
          // code stuffs
        } else {
          // use the callback version of setState to get the previous/current value of fetchTrigger
          // so you can remove the dependency on the fetchTrigger
          setFetchTrigger(fetchTrigger => fetchTrigger + 1);
        }
        // remove fetchTrigger as a dependency
      }, [page, store.objects, store.objects.results, store.objects.totalPages]);
    

    With those issues explained, you'd be better off not adding new state for your items, pages, or loading and instead deriving that from your redux store, because it looks like thats all it is.

    const items = useSelector((state) => state.store.objects?.results);
    const pages = useSelector((state) => state.store.objects?.total?.totalPages);
    const loading = useSelector((state) => !Object.keys(state.store.objects).length === 0);
    

    and removing the effect entirely in favor of a function to add to the onActivePageChange event.

      const onActivePageChange = page => {
        setPage(page); 
        setFetchTrigger(fetchTrigger => fetchTrigger + 1);
        dispatch(getAllProducts({
          params: {
            page,
          },
        }));
      };
    
      return (
        <CPagination
          // other fields
          onActivePageChange={onActivePageChange}
        />
      );
    

    But for initial results you will still need some way to fetch, you can do this with an effect that only runs once when the component is mounted. This should do that because dispatch should not be changing.

      // on mount lets get the initial results
      useEffect(() => {
        dispatch(
          getAllProducts({
            params: {
              page: 1,
            },
          })
        );
      },[dispatch]);
    

    Together that would look like this with the recommended changes:

    const ProductsTable = props => {
      const items = useSelector(state => state.store.objects?.results);
      const pages = useSelector(state => state.store.objects?.total?.totalPages);
      const loading = useSelector(state => !Object.keys(state.store.objects).length === 0);
      const dispatch = useDispatch();
    
      const [page, setPage] = useState(1);
      const [itemsPerPage, setItemsPerPage] = useState(5);
      const [fetchTrigger, setFetchTrigger] = useState(0);
    
      // on mount lets get the initial results
      useEffect(() => {
        dispatch(
          getAllProducts({
            params: {
              page: 1,
            },
          })
        );
      },[dispatch]);
    
      const onActivePageChange = page => {
        setPage(page);
        setFetchTrigger(fetchTrigger => fetchTrigger + 1);
        dispatch(
          getAllProducts({
            params: {
              page,
            },
          })
        );
      };
    
      return (
        <CCard className="p-5">
          <CDataTable
            items={items}
            fields={[
              'title',
              'slug',
              {
                key: 'show_details',
                label: '',
                _style: { width: '1%' },
                sorter: false,
                filter: false,
              },
            ]}
            loading={loading}
            hover
            cleaner
            sorter
            itemsPerPage={itemsPerPage}
            onPaginationChange={setItemsPerPage}
          />
    
          <CPagination
            pages={pages}
            activePage={page}
            onActivePageChange={onActivePageChange}
            className={pages < 2 ? 'd-none' : ''}
          />
        </CCard>
      );
    };
    
    export default ProductsTable;