have been stuck on this for hours 🙁
I have a hook, useCollection which is supposed to expose data fetched from my API and updated in realtime using a websocket. Works perfectly fine with just realtime, however id like to update my data if my query changes as well. The current solution -> infinite loop.
If anyone can help me on this one, any advice is welcome
// The hook
function useCollection<Collection extends keyof Omit<Schema, 'directus_users'>>(
collection: Collection,
query?: Query<Schema, CollectionType<Schema, Collection>>,
) {
const client = useClient();
const [items, setItems] = useState<Schema[Collection][number][]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [wsConnected, setWsConnected] = useState(false);
const fetchItems = useCallback(async () => {
try {
setLoading(true);
const results: Schema[Collection][number][] = await client.request<
Schema[Collection][number][]
>(
readItems<
Schema,
Collection,
Query<Schema, CollectionType<Schema, Collection>>
>(collection, query ?? {}),
);
setItems(() => results);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, [client, collection]);
useEffect(() => {
fetchItems();
}, [fetchItems, query]);
useEffect(() => {
const connectWs = async () => {
if (wsConnected || !client.initialized) {
return;
}
client.onWebSocket('message', function (message) {
if (
message.type === 'subscription' &&
(message.event === 'init' || 'update')
)
fetchItems();
if (message.type === 'ping') {
client.sendMessage({
type: 'pong',
});
}
});
client.onWebSocket('close', function () {
console.log({ event: 'onclose' });
});
client.onWebSocket('error', function (error) {
console.error({ event: 'onerror', error });
});
await client.subscribe(collection, {
event: 'update',
query: { fields: [] },
});
setWsConnected(true);
};
connectWs();
}, [client, client.initialized, collection, fetchItems, wsConnected]);
return { items, loading };
}
// Where its used
() => {
...
const revisionsHistoryQuery = useMemo<Query<Schema, Revision>>(() => {
if (!selected) return {};
return {
filter: {
prompt: {
_eq: selected.id,
},
},
sort: ['-date_created'],
};
}, [selected]);
const { items: revisionsHistory } = useCollection(
'prompt_revisions',
revisionsHistoryQuery,
);
...
}
Tech stack: TS + Next
What i’ve tried:
Passing the query as memo, not memo, as a param to the fetchItems fn…
I also console logged every state, and it is the query one that seems to cause the loop
2
Answers
thank you for your help. I’ve fixed the issue since then, by removing query from all useEffect dependencies. Weird fact: the data updates when the query state changes.
Haven’t found the root cause of the issue, but I think complex objects / arrays passed as props to a hook that contains useEffect depending on them will cause an infinite loop no matter what.
I don’t know how react manages the value passed as props, the value passed as deps and how it compares both, but it seems like this comparison is always false, triggering infinite rerenders.
Somehow the Component that is calling
useCollection()
is passing inquery
as a new reference on each re-render even though you’ve wrapped it inuseMemo()
. The following change inuseCollection
should break the infinite loop:This should work as long as the query doesn’t return an empty array.
How/ when is the value of
selected
being changed? If it is being set whileloading
equals true then that could be the root cause.