diff --git a/.changeset/few-squids-poke.md b/.changeset/few-squids-poke.md new file mode 100644 index 0000000000..e37b62469c --- /dev/null +++ b/.changeset/few-squids-poke.md @@ -0,0 +1,5 @@ +--- +'urql': patch +--- + +Avoid setting state on an unmounted component when useMutation is used diff --git a/exchanges/retry/package.json b/exchanges/retry/package.json index d1e3e2b9b8..78a2514576 100644 --- a/exchanges/retry/package.json +++ b/exchanges/retry/package.json @@ -56,8 +56,7 @@ "react-dom": "^16.13.0" }, "peerDependencies": { - "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", - "react": ">= 16.8.0" + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0" }, "dependencies": { "@urql/core": ">=1.10.4", diff --git a/exchanges/suspense/package.json b/exchanges/suspense/package.json index be5abfff90..c61d4b9cdb 100644 --- a/exchanges/suspense/package.json +++ b/exchanges/suspense/package.json @@ -19,8 +19,8 @@ "react", "suspense" ], - "main": "dist/urql-exchange-suspense", - "module": "dist/urql-exchange-suspense.mjs", + "main": "dist/urql-exchange-suspense.js", + "module": "dist/urql-exchange-suspense.es.js", "types": "dist/types/index.d.ts", "source": "src/index.ts", "exports": { diff --git a/packages/react-urql/src/hooks/useMutation.ts b/packages/react-urql/src/hooks/useMutation.ts index a0c2a789ef..0b59c4920a 100644 --- a/packages/react-urql/src/hooks/useMutation.ts +++ b/packages/react-urql/src/hooks/useMutation.ts @@ -1,5 +1,5 @@ import { DocumentNode } from 'graphql'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useRef, useEffect } from 'react'; import { pipe, toPromise } from 'wonka'; import { @@ -31,6 +31,7 @@ export type UseMutationResponse = [ export const useMutation = ( query: DocumentNode | string ): UseMutationResponse => { + const isMounted = useRef(true); const client = useClient(); const [state, setState] = useState>(initialState); @@ -46,18 +47,26 @@ export const useMutation = ( ), toPromise ).then(result => { - setState({ - fetching: false, - stale: !!result.stale, - data: result.data, - error: result.error, - extensions: result.extensions, - }); + if (isMounted.current) { + setState({ + fetching: false, + stale: !!result.stale, + data: result.data, + error: result.error, + extensions: result.extensions, + }); + } return result; }); }, [client, query, setState] ); + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + return [state, executeMutation]; }; diff --git a/packages/react-urql/src/hooks/useSource.ts b/packages/react-urql/src/hooks/useSource.ts index e9bc42b347..ffc9a0939d 100644 --- a/packages/react-urql/src/hooks/useSource.ts +++ b/packages/react-urql/src/hooks/useSource.ts @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { useMemo, useEffect, useState } from 'react'; +import { useMemo, useEffect, useState, useRef } from 'react'; import { Source, @@ -19,6 +19,8 @@ import { useClient } from '../context'; let currentInit = false; export const useSource = (source: Source, init: T): T => { + const isMounted = useRef(true); + const [state, setState] = useState(() => { currentInit = true; let initialValue = init; @@ -35,11 +37,17 @@ export const useSource = (source: Source, init: T): T => { return initialValue; }); + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + useEffect(() => { return pipe( source, subscribe(value => { - if (!currentInit) { + if (!currentInit && isMounted.current) { setState(value); } }) diff --git a/scripts/prepare/index.js b/scripts/prepare/index.js index d2dafe7836..992b505de0 100755 --- a/scripts/prepare/index.js +++ b/scripts/prepare/index.js @@ -42,7 +42,7 @@ if (hasReact) { invariant( path.normalize(pkg.module) === `dist/${name}.es.js`, - 'package.json:main path must end in `.es.js` for packages depending on React.' + 'package.json:module path must end in `.es.js` for packages depending on React.' ); } else { invariant(