skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.


  2. Somehow the Component that is calling useCollection() is passing in query as a new reference on each re-render even though you’ve wrapped it in useMemo(). The following change in useCollection should break the infinite loop:

        useEffect(() => {
            if (!items.length && !loading && !error) fetchItems();
        }, [fetchItems, query, items.length, loading, error]);
    

    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 while loading equals true then that could be the root cause.

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