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

Expose .subscribe() to allow subscribing to state changes outside React #25

Merged
merged 6 commits into from
Feb 14, 2022
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
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
tests/StateRacing.mocha.tsx
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"project": ["./tsconfig.json", "./tests/tsconfig.json"],
"sourceType": "module"
},
"settings": {
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const cartProducts = cartProductsState.useSelector(
```

### Supported functions outside of React
The following functions work outside of React e.g. in your middleware but you can also use them in your component. (but these functions are not subscribed to changes)
The following functions work outside of React e.g. in your middleware but you can also use them in your component.

```typescript
import { cartProductsState } from "../cartProductsState";
Expand All @@ -108,6 +108,15 @@ cartProductsState.set(

// you can reset to initial state too
cartProductsState.reset()

// you can also subscribe to state changes outside React

const unsubscribe = cartProductsState.subscribe((newState, oldState) => {
console.log("State changed");
});

// call the returned unsubscribe function to unsubscribe.
unsubscribe();
```

### Example
Expand Down
4 changes: 3 additions & 1 deletion dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ export interface StateWithValue<T> {
useSelector: UseSelector<T>;
set: Set<T>;
reset: () => void;
subscribe(subscriber: SubscriberFunc<T>): () => void;
}
declare type SubscriberFunc<T> = (newState: T, previousState: T) => void;
interface Options<T> {
onSet?: (newState: T, prevState: T) => void;
onSet?: SubscriberFunc<T>;
}
declare type Comparator<TSelected = unknown> = (a: TSelected, b: TSelected) => boolean;
export declare function newRidgeState<T>(initialValue: T, options?: Options<T>): StateWithValue<T>;
Expand Down
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.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"mocha": "mocha -r jsdom-global/register -r ts-node/register tests/**/*.mocha.tsx",
"test": "jest --maxWorkers=150",
"build": "tsc && esbuild ./src/* --outdir=dist --minify --target=es2019",
"lint": "eslint src"
"lint": "eslint src tests"
},
"devDependencies": {
"@babel/core": "^7.16.0",
Expand All @@ -23,6 +23,7 @@
"@types/jest": "^27.0.2",
"@types/node": "^16.11.6",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.11",
"@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
Expand Down
29 changes: 16 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ export interface StateWithValue<T> {
useSelector: UseSelector<T>;
set: Set<T>;
reset: () => void;
subscribe(subscriber: SubscriberFunc<T>): () => void;
}

type SubscriberFunc<T> = (newState: T) => void;
type SubscriberFunc<T> = (newState: T, previousState: T) => void;

interface Options<T> {
onSet?: (newState: T, prevState: T) => void;
onSet?: SubscriberFunc<T>;
}

type Comparator<TSelected = unknown> = (a: TSelected, b: TSelected) => boolean;
Expand Down Expand Up @@ -76,35 +77,36 @@ export function newRidgeState<T>(
let v: T = initialValue;

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

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

// callback after state is set
callback?.(v);
callback?.(v, pv);

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

// subscribe function; returns unsubscriber function
function subscribe(subscriber: SubscriberFunc<T>): () => void {
sb.push(subscriber);
return () => {
sb = sb.filter((f) => f !== subscriber);
};
}

// subscribe hook
function useSubscription(subscriber: SubscriberFunc<T>) {
// subscribe effect
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(subscriber);
return () => {
sb = sb.filter((f) => f !== subscriber);
};
}, [subscriber]);
useIsomorphicLayoutEffect(() => subscribe(subscriber), [subscriber]);
}

// use hook
Expand Down Expand Up @@ -136,5 +138,6 @@ export function newRidgeState<T>(
get: () => v,
set,
reset: () => set(initialValue),
subscribe,
};
}
2 changes: 1 addition & 1 deletion tests/Counter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
act,
fireEvent,
getNodeText,
render,
waitFor,
} from "@testing-library/react";
import { CounterComponent, CounterViewer } from "./Counter";
import * as React from "react";
import { act } from "react-dom/test-utils";

test("Both counters and global state change after click and global +", async () => {
const counters = render(
Expand Down
2 changes: 1 addition & 1 deletion tests/CounterState.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { newRidgeState } from "../src";

export const globalCounterState = newRidgeState<number>(0, {
onSet: async (newState) => {
onSet: (newState) => {
try {
localStorage.setItem("@key", JSON.stringify(newState));
} catch (e) {}
Expand Down
15 changes: 15 additions & 0 deletions tests/ProductState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { newRidgeState } from "../src";

export interface Product {
id: string;
name: string;
}

export const defaultState = {
id: "1",
name: "Test",
};

export function newProductState() {
return newRidgeState<Product>(defaultState);
}
10 changes: 5 additions & 5 deletions tests/StateRacing.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ test("Test if global state is not shared between files in Jest", async () => {
globalCounterState.set((prev) => prev + 1);
expect(globalCounterState.get()).toBe(5);
});
function getRndInteger(min, max) {

function getRndInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}
function sleeper(ms) {
return function (x) {
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
};

function sleeper(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
15 changes: 3 additions & 12 deletions tests/reset.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { newRidgeState } from "../src";
import { defaultState, newProductState } from "./ProductState";

interface Product {
id: string;
name: string;
}
const defaultState = {
id: "1",
name: "Test",
};
const productState = newRidgeState<Product>(defaultState);

test("Test if reset works", async () => {
test("Test if reset works", () => {
const productState = newProductState();
const newState = {
id: "2",
name: "Test2",
Expand Down
2 changes: 1 addition & 1 deletion tests/selectorIssue5.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface RouterState {
page: "pageOne" | "pageTwo" | "pageThree";
}

const { set, useValue, useSelector } = newRidgeState<RouterState>({
const { set, useSelector } = newRidgeState<RouterState>({
page: "pageOne",
});

Expand Down
27 changes: 27 additions & 0 deletions tests/subscribe.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { newProductState } from "./ProductState";
import { waitFor } from "@testing-library/react";

test("Test if subscribe works", async () => {
const productState = newProductState();
const subscriber = jest.fn();
const unsub = productState.subscribe(subscriber);
productState.set({
id: "2",
name: "Test2",
});
await waitFor(() => expect(subscriber).toHaveBeenCalledTimes(1));
unsub();
let done = false;
productState.set(
{
id: "3",
name: "Test3",
},
() => {
// this callback is called after subscribers
expect(subscriber).toHaveBeenCalledTimes(1);
done = true;
}
);
await waitFor(() => done);
});
4 changes: 4 additions & 0 deletions tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["./*"]
}
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,13 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==

"@types/react-dom@^17.0.11":
version "17.0.11"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466"
integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==
dependencies:
"@types/react" "*"

"@types/react-test-renderer@^17.0.1":
version "17.0.1"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b"
Expand Down