Skip to content

Commit

Permalink
Port useExtraDeps.
Browse files Browse the repository at this point in the history
  • Loading branch information
StevenXL committed Apr 8, 2022
1 parent c194e85 commit c14a7a9
Show file tree
Hide file tree
Showing 8 changed files with 460 additions and 33 deletions.
46 changes: 31 additions & 15 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,35 @@
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.helloWord = void 0;

var helloWord = function helloWord() {
return "Hello, World!";
};

exports.helloWord = helloWord;
"use strict";

var _ = require(".");

describe("helloWord", function () {
test("is hello world", function () {
expect((0, _.helloWord)()).toBe("Hello, World!");
});
Object.defineProperty(exports, "idExtraDep", {
enumerable: true,
get: function get() {
return _useExtraDeps.idExtraDep;
}
});
Object.defineProperty(exports, "isEqualExtraDep", {
enumerable: true,
get: function get() {
return _useExtraDeps.isEqualExtraDep;
}
});
Object.defineProperty(exports, "unCallbackFn", {
enumerable: true,
get: function get() {
return _useExtraDeps.unCallbackFn;
}
});
Object.defineProperty(exports, "unsafeMkCallbackFn", {
enumerable: true,
get: function get() {
return _useExtraDeps.unsafeMkCallbackFn;
}
});
Object.defineProperty(exports, "useExtraDeps", {
enumerable: true,
get: function get() {
return _useExtraDeps.useExtraDeps;
}
});

var _useExtraDeps = require("./use-extra-deps");
125 changes: 125 additions & 0 deletions dist/use-extra-deps/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _typeof = require("@babel/runtime/helpers/typeof");

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.unCallbackFn = exports.isEqualExtraDep = exports.idExtraDep = void 0;
exports.unsafeMkCallbackFn = unsafeMkCallbackFn;
exports.useExtraDeps = useExtraDeps;

var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));

var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));

var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));

var React = _interopRequireWildcard(require("react"));

var _pickBy = _interopRequireDefault(require("lodash/pickBy"));

var _omitBy = _interopRequireDefault(require("lodash/omitBy"));

var _isFunction = _interopRequireDefault(require("lodash/isFunction"));

var _mapValues = _interopRequireDefault(require("lodash/mapValues"));

var _values = _interopRequireDefault(require("lodash/values"));

var _isEqual = _interopRequireDefault(require("lodash/isEqual"));

function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }

function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }

var unCallbackFn = function unCallbackFn(fn) {
return fn;
}; // Used only by `useSafeCallback`


exports.unCallbackFn = unCallbackFn;

function unsafeMkCallbackFn(f) {
return f;
}

// 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
// }
//
function useExtraDeps(deps, extraDeps) {
var _React$useState = React.useState(Symbol()),
_React$useState2 = (0, _slicedToArray2["default"])(_React$useState, 2),
run = _React$useState2[0],
setRun = _React$useState2[1];

var nonFnsRef = React.useRef(null);
var fns = (0, _pickBy["default"])(extraDeps, _isFunction["default"]);
var nonFns = (0, _omitBy["default"])(extraDeps, _isFunction["default"]);

var hasChange = function hasChange() {
if (nonFnsRef.current === null || nonFnsRef.current === undefined) {
return true;
}

for (var _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: [].concat((0, _toConsumableArray2["default"])(deps), (0, _toConsumableArray2["default"])((0, _values["default"])(fns)), [run]),
extraDepValues: _objectSpread(_objectSpread({}, (0, _mapValues["default"])(nonFns, function (_ref) {
var value = _ref.value;
return value;
})), fns)
};
}

var idExtraDep = function idExtraDep(value) {
return {
value: value,
comparator: function comparator(a, b) {
return a.id === b.id;
}
};
};

exports.idExtraDep = idExtraDep;

var isEqualExtraDep = function isEqualExtraDep(value) {
return {
value: value,
comparator: function comparator(a, b) {
return (0, _isEqual["default"])(a, b);
}
};
};

exports.isEqualExtraDep = isEqualExtraDep;
22 changes: 20 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "React hooks used at Freckle",
"main": "dist/index.js",
"scripts": {
"build": "babel src -o dist/index.js",
"build": "babel src -d dist/ --ignore \"src/**/*.test.js\"",
"dev": "watch 'yarn run build' src",
"test": "jest",
"test:watch": "yarn run test -- --watch",
Expand All @@ -13,21 +13,39 @@
"babel": {
"presets": [
"@babel/preset-env",
"@babel/preset-flow"
"@babel/preset-flow",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
},
"jest": {
"testEnvironment": "jsdom"
},
"repository": "git@github.com:freckle/react-hooks.git",
"author": "Freckle",
"license": "MIT",
"private": true,
"peerDependencies": {
"react": ">= 16.8.0"
},
"devDependencies": {
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.9",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-flow": "^7.16.7",
"@babel/preset-react": "^7.16.7",
"flow-bin": "^0.175.1",
"flow-typed": "^3.7.0",
"jest": "^27.5.1",
"watch": "^1.0.2"
},
"dependencies": {
"@babel/runtime": "^7.17.9",
"invariant": "^2.2.4",
"react": "16.14.0",
"react-dom": "16.14.0"
}
}
12 changes: 7 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @flow

const helloWord = (): string => "Hello, World!";

export { helloWord };
export {
unCallbackFn,
unsafeMkCallbackFn,
useExtraDeps,
idExtraDep,
isEqualExtraDep,
} from "./use-extra-deps";
9 changes: 0 additions & 9 deletions src/index.test.js

This file was deleted.

98 changes: 98 additions & 0 deletions src/use-extra-deps/index.js
Original file line number Diff line number Diff line change
@@ -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<PrimitiveDomainDep> =
| 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> = F;

export const unCallbackFn = <F>(fn: CallbackFn<F>): F => fn;

// Used only by `useSafeCallback`
export function unsafeMkCallbackFn<F: (...Array<any>) => any>(
f: F
): CallbackFn<F> {
return f;
}

export type ExtraDeps<V> =
| {| value: V, comparator: (a: V, b: V) => boolean |}
| CallbackFn<V>;

// 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<S: { [key: string]: any }, PrimitiveDomainDep>(
deps: $ReadOnlyArray<PrimitiveDep<PrimitiveDomainDep>>,
extraDeps: S
): {|
allDeps: $ReadOnlyArray<any>,
extraDepValues: $ObjMap<S, <V>(ExtraDeps<V>) => V>,
|} {
const [run, setRun] = React.useState<symbol>(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 = <V: { id: string }>(value: V): ExtraDeps<V> => ({
value,
comparator: (a, b) => a.id === b.id,
});

export const isEqualExtraDep = <V>(value: V): ExtraDeps<V> => ({
value,
comparator: (a: V, b: V): boolean => isEqual(a, b),
});
Loading

0 comments on commit c14a7a9

Please sign in to comment.