Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Housekeeping & improvements, part 2 #24

Merged
merged 9 commits into from
Nov 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"settings": {
"react": {
"version": "detect"
}
},
"extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:react-hooks/recommended",
"plugin:react/recommended",
"prettier"
Expand Down
3 changes: 0 additions & 3 deletions dist/e.d.ts

This file was deleted.

1 change: 0 additions & 1 deletion dist/e.js

This file was deleted.

3 changes: 0 additions & 3 deletions dist/e.native.d.ts

This file was deleted.

1 change: 0 additions & 1 deletion dist/e.native.js

This file was deleted.

14 changes: 7 additions & 7 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { SetStateAction } from "react";
declare type Set<T> = (newState: SetStateAction<T>, callback?: (newState: T) => void) => void;
declare type UseSelector<T> = <TSelected = unknown>(selector: (state: T) => TSelected, equalityFn?: Comparator<TSelected>) => TSelected;
export interface StateWithValue<T> {
use: () => [
T,
(newState: T | ((prev: T) => T), ac?: (newState: T) => void) => void
];
use: () => [T, Set<T>];
useValue: () => T;
get: () => T;
useSelector: <TSelected = unknown>(selector: (state: T) => TSelected, equalityFn?: Comparator<TSelected>) => TSelected;
set: (newState: T | ((prev: T) => T), ac?: (newState: T) => void, ca?: (ns: T) => void) => void;
useSelector: UseSelector<T>;
set: Set<T>;
reset: () => void;
}
interface Options<T> {
onSet?: (newState: T, prevState: T) => void;
}
declare type Comparator<TSelected = unknown> = (a: TSelected, b: TSelected) => boolean;
export declare function newRidgeState<T>(iv: T, o?: Options<T>): StateWithValue<T>;
export declare function newRidgeState<T>(initialValue: T, options?: Options<T>): StateWithValue<T>;
export {};
2 changes: 1 addition & 1 deletion dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"scripts": {
"mocha": "mocha -r jsdom-global/register -r ts-node/register tests/**/*.mocha.tsx",
"test": "jest --maxWorkers=150",
"build": "tsc && esbuild ./src/* --outdir=dist --minify",
"build": "tsc && esbuild ./src/* --outdir=dist --minify --target=es2019",
"lint": "eslint src"
},
"devDependencies": {
Expand Down Expand Up @@ -47,5 +47,8 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0"
},
"jest": {
"testEnvironment": "jsdom"
}
}
5 changes: 0 additions & 5 deletions src/e.native.ts

This file was deleted.

16 changes: 0 additions & 16 deletions src/e.ts

This file was deleted.

96 changes: 56 additions & 40 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { useState, useRef } from "react";
import e from "./e";
import {
useState,
useRef,
useLayoutEffect,
useEffect,
SetStateAction,
} from "react";

type Set<T> = (
newState: SetStateAction<T>, // can be the newState or a function with prevState in params and which needs to return new state
callback?: (newState: T) => void // callback with the newState after state has been set
) => void;

type UseSelector<T> = <TSelected = unknown>(
selector: (state: T) => TSelected,
equalityFn?: Comparator<TSelected>
) => TSelected;

export interface StateWithValue<T> {
use: () => [
T,
(newState: T | ((prev: T) => T), ac?: (newState: T) => void) => void
];
use: () => [T, Set<T>];
useValue: () => T;
get: () => T;
useSelector: <TSelected = unknown>(
selector: (state: T) => TSelected,
equalityFn?: Comparator<TSelected>
) => TSelected;
set: (
newState: T | ((prev: T) => T), // can be the newState or a function with prevState in params and which needs to return new state
ac?: (newState: T) => void, // callback with the newState after state has been set
ca?: (ns: T) => void // caller is used inside react components so we can we do faster updates to the caller
) => void;
useSelector: UseSelector<T>;
set: Set<T>;
reset: () => void;
}

Expand All @@ -28,14 +33,28 @@ interface Options<T> {

type Comparator<TSelected = unknown> = (a: TSelected, b: TSelected) => boolean;

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed

const useIsomorphicLayoutEffect =
typeof window !== "undefined" || typeof document !== "undefined"
? useLayoutEffect
: useEffect;

const equ: Comparator = (a, b) => a === b;

const FR = {}; // an opaque value
function useComparator<T>(v: T, c: Comparator<T> = equ): T {
const f = useRef(FR as T);
let nv = f.current;

e(() => {
useIsomorphicLayoutEffect(() => {
f.current = nv;
});

Expand All @@ -46,71 +65,68 @@ function useComparator<T>(v: T, c: Comparator<T> = equ): T {
return nv;
}

export function newRidgeState<T>(iv: T, o?: Options<T>): StateWithValue<T> {
export function newRidgeState<T>(
initialValue: T,
options?: Options<T>
): StateWithValue<T> {
// subscribers with callbacks for external updates
let sb: SubscriberFunc<T>[] = [];

// internal value of the state
let v: T = iv;
let v: T = initialValue;

// set function
function set(ns: T | ((prev: T) => T), ac?: (ns: T) => void) {
function set(newValue: SetStateAction<T>, callback?: (ns: T) => void) {
const pv = v;
// support previous as argument to new value
v = (ns instanceof Function ? ns(v) : ns) as T;
v = newValue instanceof Function ? newValue(v) : newValue;

// let subscribers know value did change async
setTimeout(() => {
// call subscribers
sb.forEach((c) => c(v));

// callback after state is set
ac && ac(v);
callback?.(v);

// let options function know when state has been set
o && o.onSet && o.onSet(v, pv);
options?.onSet?.(v, pv);
});
}

// subscribe hook
function sub(c: SubscriberFunc<T>) {
function useSubscription(subscriber: SubscriberFunc<T>) {
// subscribe effect
e(() => {
useIsomorphicLayoutEffect(() => {
// update local state only if it has not changed already
// so this state will be updated if it was called outside of this hook
sb.push(c);
sb.push(subscriber);
return () => {
sb = sb.filter((f) => f !== c);
sb = sb.filter((f) => f !== subscriber);
};
}, [c]);
}, [subscriber]);
}

// use hook
function use(): [
T,
(
newState: T | ((prev: T) => T),
ac?: (newState: T) => void,
ca?: (ns: T) => void
) => void
] {
function use(): [T, Set<T>] {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [l, s] = useState<T>(v);

// subscribe to external changes
sub(s);
// eslint-disable-next-line react-hooks/rules-of-hooks
useSubscription(s);

// set callback
return [l, set];
}

// select hook
function useSelector<TSelected = unknown>(
se: (state: T) => TSelected,
eq: Comparator<TSelected> = equ
selector: (state: T) => TSelected,
comparator: Comparator<TSelected> = equ
): TSelected {
const [rv] = use();
return useComparator(se(rv), eq);
return useComparator(selector(rv), comparator);
}

return {
Expand All @@ -119,6 +135,6 @@ export function newRidgeState<T>(iv: T, o?: Options<T>): StateWithValue<T> {
useValue: () => use()[0],
get: () => v,
set,
reset: () => set(iv),
reset: () => set(initialValue),
};
}
3 changes: 0 additions & 3 deletions tests/Counter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import {
act,
fireEvent,
Expand Down
3 changes: 0 additions & 3 deletions tests/CounterSubscriber.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import {
act,
fireEvent,
Expand Down
3 changes: 0 additions & 3 deletions tests/selector.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import * as React from "react";
import { newRidgeState } from "../src";
import { render, waitFor } from "@testing-library/react";
Expand Down
3 changes: 0 additions & 3 deletions tests/selectorIssue5.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import * as React from "react";
import { newRidgeState } from "../src";
import { getNodeText, render, waitFor } from "@testing-library/react";
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"outDir": "./dist",
"removeComments": true,
"jsx": "react",
"emitDeclarationOnly": true
"emitDeclarationOnly": true,
"strict": true
},
"include": ["src/*"]
}