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
setTransactions(transactions);
doesn’t cause render, because previous statetransactions
and new statetransactions
are the same object (same object reference). Therefore hookuseGetTransactionsQuery
called only once.setTransactions([])
causes render loop, because[]
creates new object every time(state changed), which causes render, which causesuseGetTransactionsQuery
hook call, which changesdata
and causes effect.setTransactions([])
(state changed) => render =>useGetTransactionsQuery
hook call =>data
changed => effect called =>setTransactions([])
(state changed)setTransactions(transactions);
does cause a render loop becausetransactions
is a stable reference to a value, whereassetTransactions([]);
the value is a new object reference each time and this fails React’sobject.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 localtransactions
state, the problem being you’ve now two "sources of truth" that need to be kept synchronized.The
useTransactions
hook should just returndata
directly.Example: