Skip to content

Commit

Permalink
Merge pull request #7 from smithki/development
Browse files Browse the repository at this point in the history
v0.5.0
  • Loading branch information
smithki authored Feb 10, 2020
2 parents eeedfb6 + 33a5525 commit 6e14e1c
Show file tree
Hide file tree
Showing 20 changed files with 2,345 additions and 1,820 deletions.
17 changes: 17 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": ["@ikscodes/eslint-config"],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"import/extensions": 0,
"@typescript-eslint/no-empty-function": 0
},
"settings": {
"import/resolver": {
"typescript": {
"directory": ["tsconfig.json"]
}
}
}
}
1 change: 1 addition & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@ikscodes/prettier-config');
33 changes: 20 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "usable-react",
"version": "0.4.1",
"version": "0.5.0",
"description": "Basic React hooks to get any project off the ground.",
"author": "Ian K Smith <[email protected]>",
"license": "MIT",
Expand All @@ -19,37 +19,44 @@
"scripts": {
"dev": "npm-run-all -s clean:dist -p compile_watch",
"build": "npm-run-all -s clean:dist -p compile",
"lint": "tslint --fix -p .",
"lint": "eslint --fix src/**/*.{ts,tsx}",
"compile": "microbundle build src/index.ts --name UsableReact --target web --external react",
"compile_watch": "microbundle watch src/index.ts --name UsableReact --target web --external react",
"clean": "npm-run-all -s clean:*",
"clean:dist": "rimraf dist",
"clean:test-dist": "rimraf test-dist",
"clean:cache": "rimraf .rts2_cache_*",
"test": "npm-run-all -s clean:* test:*",
"test:compile": "tsc -p ./tsconfig.test.json",
"test:run": "alsatian ./test-dist/**/*.spec.js",
"test_watch": "npm-run-all -s test:compile -p test_watch:*",
"test_watch:compile": "tsc -w -p ./tsconfig.test.json",
"test_watch:run": "chokidar \"./test-dist/**/*.spec.js\" -c \"npm run test:run\" --initial \"npm run test:run\""
"test": "echo \"No tests to run!\""
},
"devDependencies": {
"@ikscodes/tslint-config": "^5.3.1",
"@ikscodes/eslint-config": "^6.2.0",
"@ikscodes/prettier-config": "^0.1.0",
"@types/node": "^12.11.5",
"@types/object-hash": "^1.3.1",
"@types/react": "^16.9.9",
"@typescript-eslint/eslint-plugin": "^2.15.0",
"alsatian": "^2.4.0",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"microbundle": "^0.8.4",
"eslint": "^6.7.2",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.15.1",
"eslint-plugin-react-hooks": "^1.7.0",
"microbundle": "next",
"mock-browser": "^0.92.14",
"npm-run-all": "^4.1.5",
"prettier": "^1.14.0",
"prettier": "^1.19.1",
"react": "^16.11.0",
"rimraf": "^2.6.1",
"tslint": "^5.11.0",
"typescript": "^3.0.3"
},
"peerDependencies": {
"react": "^16.11.0"
},
"dependencies": {
"fuse.js": "^3.4.6",
"object-hash": "^2.0.1"
}
}
2 changes: 1 addition & 1 deletion src/hooks/useDebounced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
* Debounces the given value. If `delay` is zero, the value is updated
* synchronously.
*/
export function useDebounced<T>(value: T, delay: number = 300) {
export function useDebounced<T>(value: T, delay = 300) {
const isSynchronous = delay === 0;
const [debouncedValue, setDebouncedValue] = useState<T>((undefined as unknown) as T);

Expand Down
44 changes: 26 additions & 18 deletions src/hooks/useDomEvent.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { MutableRefObject, RefObject, useEffect, useRef } from 'react';
/* eslint-disable no-shadow */

import { MutableRefObject, RefObject, useCallback, useEffect, useRef } from 'react';
import { isDocument, isElement, isRefObject, isWindow } from '../utils/type-guards';

export type UseDomEventRemoveListenerFunction = () => void;
export type UseDomEventAddListenerFunction<T extends HTMLElement | Window | Document> = T extends HTMLElement
? <K extends keyof HTMLElementEventMap>(
eventName: K,
listener: (this: T, event: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions | undefined,
) => void
: (T extends Window
? <K extends keyof WindowEventMap>(
eventName: K,
listener: (this: T, event: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
) => void
: (T extends Document
? <K extends keyof DocumentEventMap>(
eventName: K,
listener: (this: T, event: DocumentEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
) => void
: never));
) => UseDomEventRemoveListenerFunction
: T extends Window
? <K extends keyof WindowEventMap>(
eventName: K,
listener: (this: T, event: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
) => UseDomEventRemoveListenerFunction
: T extends Document
? <K extends keyof DocumentEventMap>(
eventName: K,
listener: (this: T, event: DocumentEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
) => UseDomEventRemoveListenerFunction
: never;

/**
* Returns a `boolean` indicating whether the given `value` has changed since
Expand All @@ -32,6 +35,7 @@ export function useDomEvent<T extends HTMLElement | Window | Document>(
const [eventName, listener, options] = eventListenerParams as Parameters<T['addEventListener']>;
const savedListener = useRef(listener);
const savedOptions = useRef(options);
const removeListenerRef = useRef(() => {});

useEffect(() => {
savedListener.current = listener;
Expand All @@ -45,23 +49,27 @@ export function useDomEvent<T extends HTMLElement | Window | Document>(
if (isWindow(element) || isDocument(element) || isElement(element)) {
const listener = (e: any) => (savedListener.current as any)(e);
element.addEventListener(eventName, listener, savedOptions.current);
return () => {
removeListenerRef.current = () => {
element.removeEventListener(eventName, listener, savedOptions.current);
};
return removeListenerRef.current;
}

if (isRefObject<T>(element)) {
if (!!element.current && isElement(element.current)) {
const listener = (e: any) => (savedListener.current as any)(e);
element.current.addEventListener(eventName, listener, savedOptions.current);
return () => {
removeListenerRef.current = () => {
element.current!.removeEventListener(eventName, listener, savedOptions.current);
};
return removeListenerRef.current;
}
}

return;
return undefined;
}, [eventName, element]);

return useCallback(() => removeListenerRef.current(), [eventName, element]);
}) as UseDomEventAddListenerFunction<T>;

return addListener;
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useEffectAfterMount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ export function useEffectAfterMount(effect: EffectCallback, deps?: readonly any[
if (!isInitialRender) {
return savedCallback.current();
}

return undefined;
}, deps);
}
2 changes: 2 additions & 0 deletions src/hooks/useEffectTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export function useEffectTrigger(effect: EffectCallback, deps: readonly any[] =
if (didTriggerUpdate) {
return savedCallback.current();
}

return undefined;
}, [trigger, ...deps]);

return useCallback(() => setTrigger(trigger + 1), [trigger]);
Expand Down
50 changes: 50 additions & 0 deletions src/hooks/useFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Fuse from 'fuse.js';
import { useEffect, useMemo, useState } from 'react';
import { useCompare } from './useCompare';
import { useHash } from './useHash';
import { useTimer } from './useTimer';

interface UseFilterOptions<TData> {
needle?: string;
haystack?: TData[];
debounce?: number;
/* Fuse.js options -> see https://fusejs.io/ */
searchOptions?: Fuse.FuseOptions<TData>;
}

/**
* Peform a fuzzy search on a dataset (`haystack`), returning the results that
* match most closely to the given `needle`
*/
export function useFilter<TData = any>({ needle, haystack, debounce, searchOptions }: UseFilterOptions<TData> = {}) {
const [results, setResults] = useState<TData[]>([]);
const cooldownTimer = useTimer({ length: 0, tick: 100, autoStart: false });

const optionsWithDefaults: Fuse.FuseOptions<TData> = useMemo(
() => ({
keys: [],
...searchOptions,
}),
[searchOptions],
);

const haystackHash = useHash(haystack);
const optionsHash = useHash(optionsWithDefaults);

const didNeedleChange = useCompare(needle);
const didHaystackChange = useCompare(haystackHash);
const didOptionsChange = useCompare(optionsHash);

// Execute a search if the needle/haystack changes.
useEffect(() => {
if (!cooldownTimer.isRunning && (didNeedleChange || didHaystackChange || didOptionsChange)) {
cooldownTimer.reset(debounce);
cooldownTimer.start();

const fuse = new Fuse<TData, Fuse.FuseOptions<TData>>(haystack || [], optionsWithDefaults);
if (needle) setResults(fuse.search(needle) as TData[]);
}
}, [needle, haystackHash, debounce, optionsHash]);

return results;
}
15 changes: 15 additions & 0 deletions src/hooks/useHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import createHash from 'object-hash';
import { useEffect, useState } from 'react';

/**
* Returns a hash of the given `value`.
*/
export function useHash<T>(value: T) {
const [hash, setHash] = useState<string>(createHash(typeof value === 'undefined' ? null : ''));
useEffect(() => {
if (value) {
setHash(createHash(value));
}
}, [value]);
return hash;
}
12 changes: 12 additions & 0 deletions src/hooks/useHashCompare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useCompare } from './useCompare';
import { useHash } from './useHash';

/**
* Returns a `boolean` indicating whether the given `value` has changed since
* the previous update based on a hash of its contents.
*/
export function useHashCompare<T = any>(value: T) {
const hash = useHash(value);
const didHashChange = useCompare(hash);
return didHashChange;
}
3 changes: 2 additions & 1 deletion src/hooks/useInterval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useInitialRender } from './useInitialRender';
/**
* Executes the given effect on an interval.
*/
export function useInterval(effect: EffectCallback, deps: readonly any[], interval: number = 1000) {
export function useInterval(effect: EffectCallback, deps: readonly any[], interval = 1000) {
const savedCallback = useRef(effect);
const isInitialRender = useInitialRender();
const isCleared = useRef(false);
Expand All @@ -18,6 +18,7 @@ export function useInterval(effect: EffectCallback, deps: readonly any[], interv
// Set up the interval.
const triggerInterval = useEffectTrigger(() => {
if (!isInitialRender && !isCleared.current) return savedCallback.current();
return undefined;
}, [...deps]);

useEffect(() => {
Expand Down
25 changes: 17 additions & 8 deletions src/hooks/useTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export function useTimer(options: { length: number; tick?: number; autoStart?: b
lengthRef.current = length;
}, [length]);

// Build timer functionality callbacks.

const start = useCallback(() => {
if (!isRunning && !isStarted.current) {
setIsRunning(true);
Expand All @@ -78,13 +80,16 @@ export function useTimer(options: { length: number; tick?: number; autoStart?: b
if (!isRunning && isStarted.current) setIsRunning(true);
}, [isRunning]);

const reset = useCallback((newLength?: number, newTick?: number) => {
if (newTick) tickRef.current = newTick;
if (newLength) lengthRef.current = newLength;
if (isRunning) setIsRunning(false);
setRemaining(newLength || lengthRef.current);
isStarted.current = false;
}, []);
const reset = useCallback(
(newLength?: number, newTick?: number) => {
if (newTick) tickRef.current = newTick;
if (newLength) lengthRef.current = newLength;
if (isRunning) setIsRunning(false);
setRemaining(newLength || lengthRef.current);
isStarted.current = false;
},
[isRunning],
);

// Update the timer.
useEffect(() => {
Expand All @@ -105,7 +110,7 @@ export function useTimer(options: { length: number; tick?: number; autoStart?: b
setIsRunning(false);
}

return;
return undefined;
}, [remaining, isRunning]);

return {
Expand Down Expand Up @@ -139,6 +144,8 @@ export function useTimerEffect(timer: TimerHook, effect: EffectCallback, deps: r
if (isRunning && didTimerChange && remaining > 0) {
return savedCallback.current();
}

return undefined;
}, [remaining, isRunning, ...deps]);
}

Expand All @@ -162,5 +169,7 @@ export function useTimerComplete(timer: TimerHook, effect: EffectCallback, deps:
if (didTimerChange && remaining <= 0) {
return savedCallback.current();
}

return undefined;
}, [remaining, ...deps]);
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ export { useDeferredChildren } from './hooks/useDeferredChildren';
export { useDomEvent } from './hooks/useDomEvent';
export { useInitialRender } from './hooks/useInitialRender';
export { useEffectTrigger } from './hooks/useEffectTrigger';
export { useFilter } from './hooks/useFilter';
export { useDebounced } from './hooks/useDebounced';
export { useForceUpdate } from './hooks/useForceUpdate';
export { useHash } from './hooks/useHash';
export { useHashCompare } from './hooks/useHashCompare';
export { useEffectAfterMount } from './hooks/useEffectAfterMount';
export { useInterval } from './hooks/useInterval';
export { useTimer, useTimerComplete, useTimerEffect, TimerHook } from './hooks/useTimer';
20 changes: 0 additions & 20 deletions test/mocks/browser.ts

This file was deleted.

3 changes: 0 additions & 3 deletions test/shims.d.ts

This file was deleted.

10 changes: 0 additions & 10 deletions test/usable-react.spec.ts

This file was deleted.

Loading

0 comments on commit 6e14e1c

Please sign in to comment.