diff --git a/dist/index.js.flow b/dist/index.js.flow new file mode 100644 index 0000000..273e9df --- /dev/null +++ b/dist/index.js.flow @@ -0,0 +1,7 @@ +export { + unCallbackFn, + unsafeMkCallbackFn, + useExtraDeps, + idExtraDep, + isEqualExtraDep, +} from "./use-extra-deps"; diff --git a/dist/use-extra-deps/index.js.flow b/dist/use-extra-deps/index.js.flow new file mode 100644 index 0000000..c54fe1a --- /dev/null +++ b/dist/use-extra-deps/index.js.flow @@ -0,0 +1,98 @@ +// @flow + +import * as React from "react"; +import pickBy from "lodash/pickBy"; +import omitBy from "lodash/omitBy"; +import isFunction from "lodash/isFunction"; +import mapValues from "lodash/mapValues"; +import values from "lodash/values"; +import isEqual from "lodash/isEqual"; + +export type PrimitiveLangDep = boolean | string | number | null | void | symbol; + +// Dependencies that are safe to use in the normal `useEffect` deps array +// +// `PrimitiveDep` is a generic type. Typically usage is to bind the type +// variable `PrimitiveDomainDep` to opaque types that wrap primitives. +export type PrimitiveDep = + | PrimitiveLangDep + | PrimitiveDomainDep; + +// Wrapper around a function that has been wrapped in `useSafeCallback`. This type is here to avoid +// cyclical dependencies. +export opaque type CallbackFn = F; + +export const unCallbackFn = (fn: CallbackFn): F => fn; + +// Used only by `useSafeCallback` +export function unsafeMkCallbackFn) => any>( + f: F +): CallbackFn { + return f; +} + +export type ExtraDeps = + | {| value: V, comparator: (a: V, b: V) => boolean |} + | CallbackFn; + +// Hook used to help avoid pitfalls surrounding misuse of objects and arrays in the deps of +// `useEffect` et. al. +// +// By only allowing `PrimitiveDep`s in the `deps` array and forcing functions and non-primitives +// through `extraDeps`, we can ensure that we are not doing naive reference equality like React +// does for the `deps` array. +// +// See `useSafeEffect` for usage of this hook +// +// Returns an object based upon deps and extraDeps: +// { allDeps: An array that is suitable to use as a deps array for things like `useEffect` +// , extraDepValues: An object that has the same keys as extraDeps but contains their plain values +// } +// +export function useExtraDeps( + deps: $ReadOnlyArray>, + extraDeps: S +): {| + allDeps: $ReadOnlyArray, + extraDepValues: $ObjMap(ExtraDeps) => V>, +|} { + const [run, setRun] = React.useState(Symbol()); + const nonFnsRef = React.useRef(null); + + const fns = pickBy(extraDeps, isFunction); + const nonFns = omitBy(extraDeps, isFunction); + + const hasChange = () => { + if (nonFnsRef.current === null || nonFnsRef.current === undefined) { + return true; + } + for (const key in nonFns) { + if ( + !nonFns[key].comparator(nonFns[key].value, nonFnsRef.current[key].value) + ) { + return true; + } + } + return false; + }; + + if (hasChange()) { + setRun(Symbol()); + nonFnsRef.current = nonFns; + } + + return { + allDeps: [...deps, ...values(fns), run], + extraDepValues: { ...mapValues(nonFns, ({ value }) => value), ...fns }, + }; +} + +export const idExtraDep = (value: V): ExtraDeps => ({ + value, + comparator: (a, b) => a.id === b.id, +}); + +export const isEqualExtraDep = (value: V): ExtraDeps => ({ + value, + comparator: (a: V, b: V): boolean => isEqual(a, b), +}); diff --git a/package.json b/package.json index bd9b4f2..99578af 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "dev": "watch 'yarn run build' src", "test": "jest", "test:watch": "yarn run test -- --watch", - "check-git-clean": "./check-git-clean.sh" + "check-git-clean": "./check-git-clean.sh", + "gen-flow": "yarn run flow-copy-source --ignore '**/*.test.js' src dist" }, "babel": { "presets": [ @@ -38,6 +39,7 @@ "@babel/preset-flow": "^7.16.7", "@babel/preset-react": "^7.16.7", "flow-bin": "^0.175.1", + "flow-copy-source": "^2.0.9", "flow-typed": "^3.7.0", "jest": "^27.5.1", "watch": "^1.0.2" diff --git a/yarn.lock b/yarn.lock index b9097d5..75a6a70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1805,7 +1805,7 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= -chokidar@^3.4.0: +chokidar@^3.0.0, chokidar@^3.4.0: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -2212,6 +2212,17 @@ flow-bin@^0.175.1: resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.175.1.tgz#72237070ba4f293f9e04113481e18929c1de50df" integrity sha512-zMCP0BPa9BrfBSR7QTcyT/XBwzUbyLdNG0eXvBuNxfHCbMRkUzSceRoOaEZIw+R+GH0UHjVfUvPJ30hXxz1Nfw== +flow-copy-source@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/flow-copy-source/-/flow-copy-source-2.0.9.tgz#0c94ad842f2ae544d5a6b8ae720cee0b8678d742" + integrity sha512-7zX/oHSIHe8YRGiA9QIcC4SW6KF667ikdmiDfbST15up1Ona8dn7Xy0PmSrfw6ceBWDww8sRKlCLKsztStpYkQ== + dependencies: + chokidar "^3.0.0" + fs-extra "^8.1.0" + glob "^7.0.0" + kefir "^3.7.3" + yargs "^15.0.1" + flow-typed@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/flow-typed/-/flow-typed-3.7.0.tgz#c3e087cb39de1a3ba303f5c646daca74c9fa8a21" @@ -3083,6 +3094,11 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +kefir@^3.7.3: + version "3.8.8" + resolved "https://registry.yarnpkg.com/kefir/-/kefir-3.8.8.tgz#235932ddfbed422acebf5d7cba503035e9ea05c5" + integrity sha512-xWga7QCZsR2Wjy2vNL3Kq/irT+IwxwItEWycRRlT5yhqHZK2fmEhziP+LzcJBWSTAMranGKtGTQ6lFpyJS3+jA== + keyv@^4.0.0: version "4.2.2" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.2.2.tgz#4b6f602c0228ef4d8214c03c520bef469ed6b768" @@ -4201,7 +4217,7 @@ yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs@^15.1.0: +yargs@^15.0.1, yargs@^15.1.0: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==