diff --git a/README.md b/README.md index a91c57ec3c..8e1c09bd2c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # urql
@@ -25,7 +24,6 @@ Steve Urkel - ## ✨ Features - 📦 **One package** to get a working GraphQL client in React diff --git a/src/hooks/useMutation.ts b/src/hooks/useMutation.ts index ff154c8f5a..6c6d37627b 100644 --- a/src/hooks/useMutation.ts +++ b/src/hooks/useMutation.ts @@ -1,5 +1,5 @@ import { DocumentNode } from 'graphql'; -import { useContext, useState } from 'react'; +import { useContext, useEffect, useRef, useState } from 'react'; import { pipe, toPromise } from 'wonka'; import { Context } from '../context'; import { OperationResult } from '../types'; @@ -19,6 +19,7 @@ export type UseMutationResponse = [ export const useMutation = ( query: DocumentNode | string ): UseMutationResponse => { + const isMounted = useRef(true); const client = useContext(Context); const [state, setState] = useState>({ fetching: false, @@ -26,6 +27,13 @@ export const useMutation = ( data: undefined, }); + useEffect( + () => () => { + isMounted.current = false; + }, + [] + ); + const executeMutation = (variables?: V) => { setState({ fetching: true, error: undefined, data: undefined }); @@ -36,7 +44,11 @@ export const useMutation = ( toPromise ).then(result => { const { data, error } = result; - setState({ fetching: false, data, error }); + + if (isMounted.current) { + setState({ fetching: false, data, error }); + } + return result; }); }; diff --git a/src/hooks/useQuery.spec.ts b/src/hooks/useQuery.spec.ts index a5fa1e4e83..e0db51e4b4 100644 --- a/src/hooks/useQuery.spec.ts +++ b/src/hooks/useQuery.spec.ts @@ -130,7 +130,6 @@ describe('useQuery', () => { `; rerender({ query: newQuery }); - await waitForNextUpdate(); expect(client.executeQuery).toBeCalledTimes(2); expect(client.executeQuery).toHaveBeenNthCalledWith( 2, @@ -161,7 +160,6 @@ describe('useQuery', () => { }; rerender({ query: mockQuery, variables: newVariables }); - await waitForNextUpdate(); expect(client.executeQuery).toBeCalledTimes(2); expect(client.executeQuery).toHaveBeenNthCalledWith( 2, @@ -188,7 +186,6 @@ describe('useQuery', () => { expect(client.executeQuery).toBeCalledTimes(1); rerender({ query: mockQuery, variables: mockVariables }); - await waitForNextUpdate(); expect(client.executeQuery).toBeCalledTimes(1); }); @@ -224,7 +221,6 @@ describe('useQuery', () => { variables: mockVariables, requestPolicy: 'network-only', }); - await waitForNextUpdate(); expect(client.executeQuery).toBeCalledTimes(2); expect(client.executeQuery).toHaveBeenNthCalledWith( 2, diff --git a/src/hooks/useQuery.test.tsx b/src/hooks/useQuery.test.tsx index d83a09b4ca..7fe075dd51 100644 --- a/src/hooks/useQuery.test.tsx +++ b/src/hooks/useQuery.test.tsx @@ -20,6 +20,7 @@ jest.mock('../client', () => { import React, { FC } from 'react'; import renderer, { act } from 'react-test-renderer'; +import { pipe, onEnd, interval } from 'wonka'; import { createClient } from '../client'; import { OperationContext } from '../types'; import { useQuery, UseQueryArgs, UseQueryState } from './useQuery'; @@ -162,6 +163,26 @@ describe('on change', () => { }); }); +describe('on unmount', () => { + const unsubscribe = jest.fn(); + + beforeEach(() => { + client.executeQuery.mockReturnValueOnce( + pipe( + interval(400), + onEnd(unsubscribe) + ) + ); + }); + + it('unsubscribe is called', () => { + const wrapper = renderer.create(); + wrapper.unmount(); + + expect(unsubscribe).toBeCalledTimes(1); + }); +}); + describe('execute query', () => { it('triggers query execution', () => { renderer.create(); diff --git a/src/hooks/useQuery.ts b/src/hooks/useQuery.ts index 1bfae0e564..2cb1c10ce8 100644 --- a/src/hooks/useQuery.ts +++ b/src/hooks/useQuery.ts @@ -33,7 +33,8 @@ export type UseQueryResponse = [ export const useQuery = ( args: UseQueryArgs ): UseQueryResponse => { - const unsubscribe = useRef(noop); + const isMounted = useRef(true); + const unsubscribe = useRef<() => void>(noop); const client = useContext(Context); const request = useMemo( @@ -47,27 +48,35 @@ export const useQuery = ( data: undefined, }); + /** Unmount handler */ + useEffect( + () => () => { + isMounted.current = false; + }, + [] + ); + const executeQuery = useCallback( (opts?: Partial) => { unsubscribe.current(); - setState(s => ({ ...s, fetching: true })); + if (args.pause) { + unsubscribe.current = noop; + return; + } - let teardown = noop; + setState(s => ({ ...s, fetching: true })); - if (!args.pause) { - [teardown] = pipe( - client.executeQuery(request, { - requestPolicy: args.requestPolicy, - ...opts, - }), - subscribe(({ data, error }) => { - setState({ fetching: false, data, error }); - }) - ); - } else { - setState(s => ({ ...s, fetching: false })); - } + const [teardown] = pipe( + client.executeQuery(request, { + requestPolicy: args.requestPolicy, + ...opts, + }), + subscribe( + ({ data, error }) => + isMounted.current && setState({ fetching: false, data, error }) + ) + ); unsubscribe.current = teardown; }, @@ -75,8 +84,10 @@ export const useQuery = ( [request.key, client, args.pause, args.requestPolicy] ); + /** Trigger query on arg change. */ useEffect(() => { executeQuery(); + return unsubscribe.current; }, [executeQuery]); diff --git a/src/hooks/useSubscription.ts b/src/hooks/useSubscription.ts index e5b8017fa9..d12f1a303f 100644 --- a/src/hooks/useSubscription.ts +++ b/src/hooks/useSubscription.ts @@ -3,8 +3,8 @@ import { useCallback, useContext, useEffect, - useState, useRef, + useState, useMemo, } from 'react'; import { pipe, subscribe } from 'wonka'; @@ -29,6 +29,7 @@ export const useSubscription = ( args: UseSubscriptionArgs, handler?: SubscriptionHandler ): UseSubscriptionResponse => { + const isMounted = useRef(true); const unsubscribe = useRef(noop); const client = useContext(Context); @@ -42,25 +43,36 @@ export const useSubscription = ( data: undefined, }); + /** Unmount handler */ + useEffect( + () => () => { + isMounted.current = false; + }, + [] + ); + const executeSubscription = useCallback(() => { unsubscribe.current(); const [teardown] = pipe( client.executeSubscription(request), - subscribe(({ data, error }) => { - setState(s => ({ - data: handler !== undefined ? handler(s.data, data) : data, - error, - })); - }) + subscribe( + ({ data, error }) => + isMounted.current && + setState(s => ({ + data: handler !== undefined ? handler(s.data, data) : data, + error, + })) + ) ); unsubscribe.current = teardown; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [request.key, handler, client]); + }, [client, handler, request]); + /** Trigger subscription on query change. */ useEffect(() => { executeSubscription(); + return unsubscribe.current; }, [executeSubscription]);