skip to Main Content

I have a react project where I’m trying to separate logic from the UI by moving it to a hook. The hook in question collects paged data from an RTK Query and aggregates it into a state variable transactions. If I setTransactions as this aggregate, I don’t have a problem, but if I try to change it in any way, I see the component rerender many times, causing a Maximum Depth Exceeded error.

I’ve distilled this problem down to the bare essentials, removing the UI and all logic not causing the error.

AccountTransactions component:

export default function AccountTransactions() {
    const { transactions, handleMoreTransactionsClick, handleDeleteTransaction } = useTransactions(68);
    console.log('transactions', transactions); // I see this log many times

    return (
        <div></div>
    );
}

TransactionsHook custom hook:

export default function useTransactions (accountId) {
    const [page, setPage] = useState(1);
    const [transactions, setTransactions] = useState([]);

    // load transactions
    const {
        data = [],
        isSuccess,
    } = useGetTransactionsQuery({accountId: accountId, page: page});

    useEffect(() => {
        // setTransactions(transactions); // this doesn't cause rerenders 
        setTransactions([]); // this causes rerenders
    }, [data]);

    return { transactions };
}

I’m using data as a dependency for the hook’s useEffect because isLoading remains true after the first page is loaded and subsequent pages won’t load. However, some combination of using data as a dependency and changing transactions triggers a rerender of the component. Even more confusing for me is that in this cut-down code, transactions is [], but assigning it a new value of [] also causes the rerender.

For completeness, here’s the RTK reducer, though I don’t believe that’s contributing to the problem.

        getTransactions: build.query({
            query: ({accountId, page}) => ({
                url: `/api/transactions/account/${accountId}`,
                method: 'get',
                params: {page}
            }),
            providesTags: (result = [], error, arg) => [
                'Transaction',
                ...result.data.map(({ id }) => ({ type: 'Transaction', id })),
            ],
        }),

I’m still getting my head around hooks (and even the rendering process) but I can’t see how I’ve broken any of the rules. Can anyone else spot anything?

2

Answers


  1. setTransactions(transactions); doesn’t cause render, because previous state transactions and new state transactions are the same object (same object reference). Therefore hook useGetTransactionsQuery called only once.

    setTransactions([]) causes render loop, because [] creates new object every time(state changed), which causes render, which causes useGetTransactionsQuery hook call, which changes data and causes effect.

    setTransactions([])(state changed) => render => useGetTransactionsQuery hook call => data changed => effect called => setTransactions([])(state changed)

    Login or Signup to reply.
  2. setTransactions(transactions); does cause a render loop because transactions is a stable reference to a value, whereas setTransactions([]); the value is a new object reference each time and this fails React’s object.is reconciliation process.

    In any case though, you’d pretty much never want or need to enqueue a React state update to its own current value, there’s really no point.

    It is actually a general React anti-pattern to duplicate the fetched data into the local transactions state, the problem being you’ve now two "sources of truth" that need to be kept synchronized.

    The useTransactions hook should just return data directly.

    Example:

    export default function useTransactions(accountId) {
      const [page, setPage] = useState(1);
    
      // load transactions
      const {
        data: transactions = [],
        isSuccess,
      } = useGetTransactionsQuery({ accountId, page });
    
      ...
    
      return {
        transactions,
    
        // And presumably these handlers, and anything else the hook returns
        handleMoreTransactionsClick,
        handleDeleteTransaction,
      };
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search