Skip to content

Commit 118ff73

Browse files
authored
Merge pull request #14 from freckle/aj/fix-types
Fix typescript types for useExtraDeps
2 parents 1b60c85 + 1bab3d9 commit 118ff73

11 files changed

Lines changed: 73 additions & 74 deletions

File tree

dist/use-extra-deps/index.d.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
export declare type PrimitiveDep = boolean | string | number | null | void | symbol;
2-
export declare type CallbackFn<F> = F;
3-
export declare const unCallbackFn: <F>(fn: F) => F;
4-
export declare function unsafeMkCallbackFn<F extends (v: any) => any>(f: F): CallbackFn<F>;
2+
export declare type CallbackFn<F> = {
3+
callback: F;
4+
};
5+
export declare const unCallbackFn: <F>({ callback }: CallbackFn<F>) => F;
6+
export declare function unsafeMkCallbackFn<F extends (v: any) => any>(callback: F): CallbackFn<F>;
57
export declare type ExtraDeps<V> = {
68
value: V;
79
comparator: (a: V, b: V) => boolean;
810
} | CallbackFn<V>;
9-
export declare function useExtraDeps<T extends {
10-
[P in keyof S]: S[P] extends ExtraDeps<infer R> ? R : never;
11-
}, S extends {
12-
[key: string]: ExtraDeps<unknown>;
13-
} = Record<string, unknown>>(deps: ReadonlyArray<PrimitiveDep>, extraDeps: S): {
11+
export declare function useExtraDeps<T extends Record<string, unknown>>(deps: ReadonlyArray<PrimitiveDep>, extraDeps: {
12+
[P in keyof T]: T[P] extends infer R ? ExtraDeps<R> : never;
13+
}): {
1414
allDeps: ReadonlyArray<any>;
1515
extraDepValues: T;
1616
};

dist/use-extra-deps/index.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
2828
Object.defineProperty(exports, "__esModule", { value: true });
2929
exports.useExtraDeps = exports.unsafeMkCallbackFn = exports.unCallbackFn = void 0;
3030
/* eslint @typescript-eslint/no-explicit-any: 0 */
31-
const isFunction_1 = __importDefault(require("lodash/isFunction"));
3231
const mapValues_1 = __importDefault(require("lodash/mapValues"));
3332
const omitBy_1 = __importDefault(require("lodash/omitBy"));
3433
const pickBy_1 = __importDefault(require("lodash/pickBy"));
3534
const values_1 = __importDefault(require("lodash/values"));
3635
const React = __importStar(require("react"));
37-
const unCallbackFn = (fn) => fn;
36+
const unCallbackFn = ({ callback }) => callback;
3837
exports.unCallbackFn = unCallbackFn;
3938
// Used only by `useSafeCallback`
40-
function unsafeMkCallbackFn(f) {
41-
return f;
39+
function unsafeMkCallbackFn(callback) {
40+
return { callback };
4241
}
4342
exports.unsafeMkCallbackFn = unsafeMkCallbackFn;
4443
// Hook used to help avoid pitfalls surrounding misuse of objects and arrays in
@@ -58,8 +57,8 @@ exports.unsafeMkCallbackFn = unsafeMkCallbackFn;
5857
function useExtraDeps(deps, extraDeps) {
5958
const [run, setRun] = React.useState(Symbol());
6059
const nonFnsRef = React.useRef(null);
61-
const fns = (0, pickBy_1.default)(extraDeps, isFunction_1.default);
62-
const nonFns = (0, omitBy_1.default)(extraDeps, isFunction_1.default);
60+
const fns = (0, mapValues_1.default)((0, pickBy_1.default)(extraDeps, dep => 'callback' in dep), fn => fn.callback);
61+
const nonFns = (0, omitBy_1.default)(extraDeps, dep => 'callback' in dep);
6362
const hasChange = () => {
6463
if (nonFnsRef.current === null || nonFnsRef.current === undefined) {
6564
return true;

dist/use-safe-callback/index.d.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { type CallbackFn, type ExtraDeps, type PrimitiveDep } from './../use-extra-deps';
22
export declare function useSafeCallback<F extends (v: any) => any>(f: () => F, deps: ReadonlyArray<PrimitiveDep>): CallbackFn<F>;
3-
export declare function useSafeCallbackExtraDeps<F extends (v: any) => any, T extends {
4-
[P in keyof S]: S[P] extends ExtraDeps<infer R> ? R : never;
5-
}, S extends {
6-
[key: string]: ExtraDeps<unknown>;
7-
} = Record<string, unknown>>(f: (a: T) => F, deps: ReadonlyArray<PrimitiveDep>, extraDeps: S): CallbackFn<F>;
3+
export declare function useSafeCallbackExtraDeps<F extends (v: any) => any, T extends Record<string, unknown>>(f: (a: T) => F, deps: ReadonlyArray<PrimitiveDep>, extraDeps: {
4+
[P in keyof T]: T[P] extends infer R ? ExtraDeps<R> : never;
5+
}): CallbackFn<F>;

dist/use-safe-effect/index.d.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import type { MaybeCleanUpFn } from './../types';
22
import { type ExtraDeps, type PrimitiveDep } from './../use-extra-deps';
33
export declare const useSafeEffect: (effect: () => MaybeCleanUpFn, deps: ReadonlyArray<PrimitiveDep>) => void;
4-
export declare const useSafeEffectExtraDeps: <T extends { [P in keyof S]: S[P] extends ExtraDeps<infer R> ? R : never; }, S extends {
5-
[key: string]: unknown;
6-
} = Record<string, unknown>>(effect: (a: T) => MaybeCleanUpFn, deps: ReadonlyArray<PrimitiveDep>, extraDeps: S) => void;
4+
export declare const useSafeEffectExtraDeps: <T extends Record<string, unknown>>(effect: (a: T) => MaybeCleanUpFn, deps: ReadonlyArray<PrimitiveDep>, extraDeps: { [P in keyof T]: T[P] extends infer R ? ExtraDeps<R> : never; }) => void;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@freckle/react-hooks",
3-
"version": "2.0.1",
3+
"version": "3.0.0",
44
"description": "React hooks used at Freckle",
55
"main": "dist/index.js",
66
"scripts": {

src/use-extra-deps/index.test.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ describe('useExtraDeps', () => {
77
it('works with extra deps', async () => {
88
let symbol
99
const C = ({p1}: {p1: number}) => {
10-
const {allDeps} = useExtraDeps([], {
11-
p1: {value: p1, comparator: (a: number, b: number) => a === b}
10+
const {allDeps} = useExtraDeps<{p1: number}>([], {
11+
p1: {value: p1, comparator: (a, b) => a === b}
1212
})
1313
//The symbol is always the last thing in the allDeps array
1414
symbol = last(allDeps)
@@ -26,4 +26,15 @@ describe('useExtraDeps', () => {
2626
rerender(<C p1={1} />)
2727
expect(lastSymbol).not.toBe(symbol)
2828
})
29+
30+
// This test is only testing the types
31+
// It is expected to throw at runtime due to calling hooks outside of a React component
32+
it('rejects malformed deps at typelevel', () => {
33+
expect(() => {
34+
// @ts-expect-error can't pass object with wrong shape
35+
useExtraDeps([], {a: 1})
36+
// @ts-expect-error can't pass function
37+
useExtraDeps([], {a: () => 'hi'})
38+
}).toThrow()
39+
})
2940
})

src/use-extra-deps/index.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint @typescript-eslint/no-explicit-any: 0 */
2-
import isFunction from 'lodash/isFunction'
32
import mapValues from 'lodash/mapValues'
43
import omitBy from 'lodash/omitBy'
54
import pickBy from 'lodash/pickBy'
@@ -11,13 +10,13 @@ export type PrimitiveDep = boolean | string | number | null | void | symbol
1110

1211
// Wrapper around a function that has been wrapped in `useSafeCallback`. This
1312
// type is here to avoid cyclical dependencies.
14-
export type CallbackFn<F> = F
13+
export type CallbackFn<F> = {callback: F}
1514

16-
export const unCallbackFn = <F>(fn: CallbackFn<F>): F => fn
15+
export const unCallbackFn = <F>({callback}: CallbackFn<F>): F => callback
1716

1817
// Used only by `useSafeCallback`
19-
export function unsafeMkCallbackFn<F extends (v: any) => any>(f: F): CallbackFn<F> {
20-
return f
18+
export function unsafeMkCallbackFn<F extends (v: any) => any>(callback: F): CallbackFn<F> {
19+
return {callback}
2120
}
2221

2322
export type ExtraDeps<V> =
@@ -41,24 +40,21 @@ export type ExtraDeps<V> =
4140
// An object that has the same keys as extraDeps but contains their plain values
4241
// }
4342
//
44-
export function useExtraDeps<
45-
T extends {[P in keyof S]: S[P] extends ExtraDeps<infer R> ? R : never},
46-
S extends {
47-
[key: string]: ExtraDeps<unknown>
48-
} = Record<string, unknown>
49-
>(
43+
export function useExtraDeps<T extends Record<string, unknown>>(
5044
deps: ReadonlyArray<PrimitiveDep>,
51-
extraDeps: S
45+
extraDeps: {[P in keyof T]: T[P] extends infer R ? ExtraDeps<R> : never}
5246
): {
5347
allDeps: ReadonlyArray<any>
5448
extraDepValues: T
5549
} {
5650
const [run, setRun] = React.useState<symbol>(Symbol())
5751
const nonFnsRef = React.useRef<null | any>(null)
5852

59-
const fns: any = pickBy(extraDeps, isFunction)
60-
const nonFns: any = omitBy(extraDeps, isFunction)
61-
53+
const fns: any = mapValues(
54+
pickBy(extraDeps, dep => 'callback' in dep),
55+
fn => (fn as any).callback
56+
)
57+
const nonFns: any = omitBy(extraDeps, dep => 'callback' in dep)
6258
const hasChange = () => {
6359
if (nonFnsRef.current === null || nonFnsRef.current === undefined) {
6460
return true

src/use-safe-callback/index.test.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ import {useSafeCallback, useSafeCallbackExtraDeps} from '.'
44

55
describe('useSafeCallback', () => {
66
it('works with dep', async () => {
7-
let f
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
let f: any
89
const A = ({p1}: {p1: number}) => {
910
f = useSafeCallback(() => () => p1, [p1])
1011
return null
1112
}
1213
const {rerender} = render(<A p1={0} />)
1314
let cbF = f
14-
1515
// f stays the same reference when prop stays the same
1616
rerender(<A p1={0} />)
17-
expect(cbF).toBe(f)
17+
expect(cbF.callback).toBe(f.callback)
1818
cbF = f
1919

2020
// f changes reference when prop changes
2121
rerender(<A p1={1} />)
22-
expect(cbF).not.toBe(f)
22+
expect(cbF.callback).not.toBe(f.callback)
2323
})
2424
})
2525

@@ -28,16 +28,17 @@ describe('useSafeCallbackExtraDeps', () => {
2828
const countTrue = jest.fn((arr: Array<boolean>): number => arr.filter(x => x === true).length)
2929
const arr1 = [true, false, true]
3030
const arr2 = [false, true]
31-
let f
31+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
32+
let f: any
3233
const A = ({p1}: {p1: boolean[]}) => {
33-
f = useSafeCallbackExtraDeps(
34+
f = useSafeCallbackExtraDeps<() => void, {p1: boolean[]}>(
3435
({p1}) =>
3536
() => {
3637
countTrue(p1)
3738
},
3839
[],
3940
{
40-
p1: {value: p1, comparator: (a: boolean[], b: boolean[]) => a.length === b.length}
41+
p1: {value: p1, comparator: (a, b) => a.length === b.length}
4142
}
4243
)
4344
return null
@@ -47,11 +48,11 @@ describe('useSafeCallbackExtraDeps', () => {
4748

4849
// f stays the same reference when prop stays the same
4950
rerender(<A p1={arr1} />)
50-
expect(cbF).toBe(f)
51+
expect(cbF.callback).toBe(f.callback)
5152
cbF = f
5253

5354
// f changes reference when prop changes
5455
rerender(<A p1={arr2} />)
55-
expect(cbF).not.toBe(f)
56+
expect(cbF.callback).not.toBe(f.callback)
5657
})
5758
})

src/use-safe-callback/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ export function useSafeCallback<F extends (v: any) => any>(
1818

1919
export function useSafeCallbackExtraDeps<
2020
F extends (v: any) => any,
21-
T extends {[P in keyof S]: S[P] extends ExtraDeps<infer R> ? R : never},
22-
S extends {
23-
[key: string]: ExtraDeps<unknown>
24-
} = Record<string, unknown>
25-
>(f: (a: T) => F, deps: ReadonlyArray<PrimitiveDep>, extraDeps: S): CallbackFn<F> {
21+
T extends Record<string, unknown>
22+
>(
23+
f: (a: T) => F,
24+
deps: ReadonlyArray<PrimitiveDep>,
25+
extraDeps: {[P in keyof T]: T[P] extends infer R ? ExtraDeps<R> : never}
26+
): CallbackFn<F> {
2627
const {extraDepValues, allDeps} = useExtraDeps<T>(deps, extraDeps)
2728

2829
// eslint-disable-next-line react-hooks/exhaustive-deps

src/use-safe-effect/index.test.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as React from 'react'
33
import {render} from '@testing-library/react'
44
import {useSafeEffect, useSafeEffectExtraDeps} from '.'
55
import {useSafeCallback} from './../use-safe-callback'
6+
import {CallbackFn} from '../use-extra-deps'
67

78
describe('useSafeEffect', () => {
89
it('works with no deps', async () => {
@@ -71,8 +72,8 @@ describe('useSafeEffect', () => {
7172
}
7273
p2: number
7374
}) => {
74-
useSafeEffectExtraDeps(({say}) => sideEffect(say), [], {
75-
say: {value: p1, comparator: (a: {text: string}, b: {text: string}) => a.text === b.text}
75+
useSafeEffectExtraDeps<{say: {text: string}}>(({say}) => sideEffect(say), [], {
76+
say: {value: p1, comparator: (a, b) => a.text === b.text}
7677
})
7778
return <>{p2}</>
7879
}
@@ -105,15 +106,15 @@ describe('useSafeEffect', () => {
105106
const countTrue = jest.fn((arr: Array<boolean>): number => arr.filter(x => x === true).length)
106107

107108
const C = ({p1, p2}: {p1: Array<boolean>; p2: number}) => {
108-
useSafeEffectExtraDeps(
109+
useSafeEffectExtraDeps<{p1: boolean[]}>(
109110
({p1}) => {
110111
// Cannot return anything except a clean-up function
111112
countTrue(p1)
112113
},
113114
[],
114115
{
115116
// Only run effect when array length changes, regardless of contents
116-
p1: {value: p1, comparator: (a: boolean[], b: boolean[]) => a.length === b.length}
117+
p1: {value: p1, comparator: (a, b) => a.length === b.length}
117118
}
118119
)
119120
return <>{p2}</>
@@ -160,14 +161,13 @@ describe('useSafeEffect', () => {
160161

161162
return <C p2={p2} p3={p3} f={cbF} />
162163
}
163-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
164-
const C = ({f, p2, p3}: {f: (v: any) => any; p2: number; p3: number}) => {
165-
useSafeEffectExtraDeps(
164+
const C = ({f, p2, p3}: {f: CallbackFn<(b: number) => void>; p2: number; p3: number}) => {
165+
useSafeEffectExtraDeps<{p3: number; f: (b: number) => void}>(
166166
({p3, f}) => {
167167
return f(p3)
168168
},
169169
[],
170-
{p3: {value: p3, comparator: (a: number, b: number) => a === b}, f}
170+
{p3: {value: p3, comparator: (a, b) => a === b}, f}
171171
)
172172
return <>{p2}</>
173173
}
@@ -213,16 +213,16 @@ describe('useSafeEffect', () => {
213213
p3: number
214214
p4: boolean
215215
}) => {
216-
useSafeEffectExtraDeps(
216+
useSafeEffectExtraDeps<{p1: {text: string}; p2: string[]}>(
217217
({p1, p2}) => {
218218
// Cannot return anything except a clean-up function
219219
computation(p1.text, p2, p3, p4)
220220
},
221221
[p3, p4],
222222
{
223-
p1: {value: p1, comparator: (a: {text: string}, b: {text: string}) => a.text === b.text},
223+
p1: {value: p1, comparator: (a, b) => a.text === b.text},
224224
// Deep comparison of arrays
225-
p2: {value: p2, comparator: (a: string[], b: string[]) => isEqual(a, b)}
225+
p2: {value: p2, comparator: (a, b) => isEqual(a, b)}
226226
}
227227
)
228228
return <>{p1.text}</>
@@ -266,8 +266,8 @@ describe('useSafeEffect', () => {
266266
}
267267
p2: number
268268
}) => {
269-
useSafeEffectExtraDeps(({say}) => sideEffect(say), [], {
270-
say: {value: p1, comparator: (a: {text: string}, b: {text: string}) => a.text === b.text}
269+
useSafeEffectExtraDeps<{say: {text: string}}>(({say}) => sideEffect(say), [], {
270+
say: {value: p1, comparator: (a, b) => a.text === b.text}
271271
})
272272
return <>{p2}</>
273273
}

0 commit comments

Comments
 (0)