diff --git a/gitbook/reactjrx/SUMMARY.md b/gitbook/reactjrx/SUMMARY.md index 21868b5..0e42dc3 100644 --- a/gitbook/reactjrx/SUMMARY.md +++ b/gitbook/reactjrx/SUMMARY.md @@ -37,11 +37,8 @@ * [Javascript](utils/javascript/README.md) * [isServer](utils/javascript/isserver.md) * [isPromiseLike](utils/javascript/ispromiselike.md) - * [isPlainObject](utils/javascript/isplainobject.md) - * [shallowEqual](utils/javascript/shallowequal.md) + * [isShallowEqual](utils/javascript/isshallowequal.md) * [isDefined](utils/javascript/isdefined.md) - * [filterObjectByKey](utils/javascript/filterobjectbykey.md) - * [difference](utils/javascript/difference.md) * [arrayEqual](utils/javascript/arrayequal.md) * [React](utils/react/README.md) * [useMount](utils/react/usemount.md) @@ -49,6 +46,4 @@ * [useConstant](utils/react/useconstant.md) * [useLiveRef](utils/react/useliveref.md) * [RxJS](utils/rxjs/README.md) - * [trackSubscriptions](utils/rxjs/tracksubscriptions.md) * [retryBackoff](utils/rxjs/retrybackoff.md) - * [emitToSubject](utils/rxjs/emittosubject.md) diff --git a/gitbook/reactjrx/state-signal/about.md b/gitbook/reactjrx/state-signal/about.md index 65c6307..37db958 100644 --- a/gitbook/reactjrx/state-signal/about.md +++ b/gitbook/reactjrx/state-signal/about.md @@ -67,14 +67,21 @@ The `key` property enables powerful extensions like persistence: const theme = signal({ default: 'light', key: 'settings.theme' }); const volume = signal({ default: 75, key: 'settings.volume' }); +// A stable reference: a new adapter starts persistence over +const adapter = createLocalStorageAdapter(); + // Set up persistence for these signals -persistSignals({ - entries: [ - { signal: theme, version: 1 }, - { signal: volume, version: 1 } - ], - adapter: localStorageAdapter -}); +const App = () => { + const { isHydrated } = usePersistSignals({ + entries: [ + { signal: theme, version: 1 }, + { signal: volume, version: 1 } + ], + adapter + }); + + // ... +}; ``` This configuration-based approach allows you to add functionality like persistence, logging, or synchronization without complicating your state management code. diff --git a/gitbook/reactjrx/utils/javascript/arrayequal.md b/gitbook/reactjrx/utils/javascript/arrayequal.md index 2f908c0..eba7a19 100644 --- a/gitbook/reactjrx/utils/javascript/arrayequal.md +++ b/gitbook/reactjrx/utils/javascript/arrayequal.md @@ -1,3 +1,18 @@ # arrayEqual -todo +Returns true when two arrays have the same length and the same items at each index, compared with `===`. + +### Usage + +```typescript +import { arrayEqual } from "reactjrx" + +arrayEqual([1, 2], [1, 2]) // true +arrayEqual([{ id: 1 }], [{ id: 1 }]) // false, the two objects are different references +``` + +### Reference + +```typescript +function arrayEqual(a: readonly A[], b: readonly B[]): boolean +``` diff --git a/gitbook/reactjrx/utils/javascript/difference.md b/gitbook/reactjrx/utils/javascript/difference.md deleted file mode 100644 index a368224..0000000 --- a/gitbook/reactjrx/utils/javascript/difference.md +++ /dev/null @@ -1,3 +0,0 @@ -# difference - -todo diff --git a/gitbook/reactjrx/utils/javascript/filterobjectbykey.md b/gitbook/reactjrx/utils/javascript/filterobjectbykey.md deleted file mode 100644 index 653d653..0000000 --- a/gitbook/reactjrx/utils/javascript/filterobjectbykey.md +++ /dev/null @@ -1,3 +0,0 @@ -# filterObjectByKey - -todo diff --git a/gitbook/reactjrx/utils/javascript/isdefined.md b/gitbook/reactjrx/utils/javascript/isdefined.md index cb0f94e..31a9840 100644 --- a/gitbook/reactjrx/utils/javascript/isdefined.md +++ b/gitbook/reactjrx/utils/javascript/isdefined.md @@ -1,3 +1,21 @@ # isDefined -todo +Returns true when the value is neither `null` nor `undefined`. + +{% hint style="info" %} +This is a type guard function: it removes `null` and `undefined` from the type, which makes it handy with `filter`. +{% endhint %} + +### Usage + +```typescript +import { isDefined } from "reactjrx" + +const ids = [1, undefined, 3, null].filter(isDefined) // number[] +``` + +### Reference + +```typescript +function isDefined(arg: T | null | undefined): arg is T extends null | undefined ? never : T +``` diff --git a/gitbook/reactjrx/utils/javascript/isplainobject.md b/gitbook/reactjrx/utils/javascript/isplainobject.md deleted file mode 100644 index 09dcfd2..0000000 --- a/gitbook/reactjrx/utils/javascript/isplainobject.md +++ /dev/null @@ -1,25 +0,0 @@ -# isPlainObject - -Returns true if the given value is an object created by the Object constructor. - -{% hint style="info" %} -This is a type guard function -{% endhint %} - -### Usage - -```typescript -import { isPlainObject } from "reactjrx" - -const something = { ... } - -if (isPlainObject(something)) { - // object -} -``` - -### Reference - -```typescript -function isPlainObject(value: any): boolean -``` diff --git a/gitbook/reactjrx/utils/javascript/ispromiselike.md b/gitbook/reactjrx/utils/javascript/ispromiselike.md index b2ac154..bc92753 100644 --- a/gitbook/reactjrx/utils/javascript/ispromiselike.md +++ b/gitbook/reactjrx/utils/javascript/ispromiselike.md @@ -23,5 +23,5 @@ if (isPromiseLike(maybePromise)) { ### Reference ```typescript -function isPromiseLike(value: any): boolean +function isPromiseLike(value: T): value is T & Promise ``` diff --git a/gitbook/reactjrx/utils/javascript/isserver.md b/gitbook/reactjrx/utils/javascript/isserver.md index 5efcd84..3d70091 100644 --- a/gitbook/reactjrx/utils/javascript/isserver.md +++ b/gitbook/reactjrx/utils/javascript/isserver.md @@ -7,7 +7,7 @@ Tells you whether the current script is running on a server or browser environme ```typescript import { isServer } from "reactjrx" -if (!isServer()) { +if (!isServer) { // only access browser API when not on server console.log(`Your device is using ${ navigator.language } languages`) } @@ -16,5 +16,5 @@ if (!isServer()) { ### Reference ```typescript -isServer(): boolean +const isServer: boolean ``` diff --git a/gitbook/reactjrx/utils/javascript/isshallowequal.md b/gitbook/reactjrx/utils/javascript/isshallowequal.md new file mode 100644 index 0000000..9acec94 --- /dev/null +++ b/gitbook/reactjrx/utils/javascript/isshallowequal.md @@ -0,0 +1,20 @@ +# isShallowEqual + +Returns true when two values are equal one level deep: the same primitive, or two objects with the same `constructor` and the same own enumerable string keys holding `===` values. Symbol and non-enumerable properties are not compared. + +### Usage + +```typescript +import { isShallowEqual } from "reactjrx" + +isShallowEqual({ a: 1, b: "x" }, { a: 1, b: "x" }) // true +isShallowEqual({ a: { b: 1 } }, { a: { b: 1 } }) // false, `a` holds two different objects +``` + +Pass it as the `compareFn` of [useObserve](../../bindings/react/useobserve.md) to skip the render of a new object that holds the same values. + +### Reference + +```typescript +function isShallowEqual(objA: A, objB: B): boolean +``` diff --git a/gitbook/reactjrx/utils/javascript/shallowequal.md b/gitbook/reactjrx/utils/javascript/shallowequal.md deleted file mode 100644 index 55e1a2b..0000000 --- a/gitbook/reactjrx/utils/javascript/shallowequal.md +++ /dev/null @@ -1,3 +0,0 @@ -# shallowEqual - -todo diff --git a/gitbook/reactjrx/utils/react/useconstant.md b/gitbook/reactjrx/utils/react/useconstant.md index 60141df..e89d50c 100644 --- a/gitbook/reactjrx/utils/react/useconstant.md +++ b/gitbook/reactjrx/utils/react/useconstant.md @@ -1,3 +1,24 @@ # useConstant -todo +Creates a value on the first render and returns that same value on every render after it. + +### Usage + +```tsx +import { useConstant } from "reactjrx" +import { BehaviorSubject } from "rxjs" + +const Counter = () => { + const count$ = useConstant(() => new BehaviorSubject(0)) + + // ... +} +``` + +The value lives in a ref, so it is never recomputed. React can drop a `useMemo` value and compute it again, which would create a second subject. + +### Reference + +```typescript +function useConstant(fn: () => T): T +``` diff --git a/gitbook/reactjrx/utils/react/useliveref.md b/gitbook/reactjrx/utils/react/useliveref.md index 4952d1a..6533693 100644 --- a/gitbook/reactjrx/utils/react/useliveref.md +++ b/gitbook/reactjrx/utils/react/useliveref.md @@ -1,3 +1,30 @@ # useLiveRef -todo +Returns a ref that holds the value passed on the latest render. A callback or a subscription that outlives the render reads the current value from it, without listing that value as a dependency. + +### Usage + +```tsx +import { useEffect } from "react" +import { useLiveRef } from "reactjrx" + +const MessageLogger = ({ onMessage }: { onMessage: (message: string) => void }) => { + const onMessageRef = useLiveRef(onMessage) + + useEffect(() => { + const subscription = messages$.subscribe((message) => onMessageRef.current(message)) + + return () => subscription.unsubscribe() + }, [onMessageRef]) + + return null +} +``` + +The subscription stays open across renders and calls the latest `onMessage`, so a new callback doesn't resubscribe. + +### Reference + +```typescript +function useLiveRef(value: T): RefObject +``` diff --git a/gitbook/reactjrx/utils/react/usemount.md b/gitbook/reactjrx/utils/react/usemount.md index 76a34da..684e30a 100644 --- a/gitbook/reactjrx/utils/react/usemount.md +++ b/gitbook/reactjrx/utils/react/usemount.md @@ -1,6 +1,6 @@ # useMount -React lifecycle hook that calls a function after the component is mounted. Use `useLifecycles` if you need both a mount and unmount function. +React lifecycle hook that calls a function after the component is mounted. ### Usage diff --git a/gitbook/reactjrx/utils/rxjs/emittosubject.md b/gitbook/reactjrx/utils/rxjs/emittosubject.md deleted file mode 100644 index 2dbb738..0000000 --- a/gitbook/reactjrx/utils/rxjs/emittosubject.md +++ /dev/null @@ -1,3 +0,0 @@ -# emitToSubject - -todo diff --git a/gitbook/reactjrx/utils/rxjs/retrybackoff.md b/gitbook/reactjrx/utils/rxjs/retrybackoff.md index 11aba3f..448a835 100644 --- a/gitbook/reactjrx/utils/rxjs/retrybackoff.md +++ b/gitbook/reactjrx/utils/rxjs/retrybackoff.md @@ -1,3 +1,42 @@ # retryBackoff -todo +An operator that resubscribes to the source when it errors, after a delay that grows with each error. By default it retries forever: pass `retry` to stop. + +### Usage + +```typescript +import { ajax } from "rxjs/ajax" +import { retryBackoff } from "reactjrx" + +const user$ = ajax.getJSON("/api/user").pipe( + retryBackoff({ + initialInterval: 500, + maxInterval: 5_000, + retry: (attempt) => attempt < 5, + }), +) +``` + +This retries 5 times, after 500 ms, 1 s, 2 s, 4 s and 5 s. The sixth error goes through. + +### Options + +| Option | Default | Description | +| --- | --- | --- | +| `initialInterval` | `100` | Base delay in ms. Retry `n` (from 0) waits `2^n * initialInterval`. | +| `maxInterval` | `Infinity` | Longest delay in ms. | +| `retry` | always `true` | `(attempt, error) => boolean`, with `attempt` from 0. Return `false` to stop retrying and let the error through. | +| `resetOnSuccess` | `false` | Each value the source emits resets the delay and the attempt count. | +| `backoffDelay` | `exponentialBackoffDelay` | `(attempt, initialInterval) => number`: the delay before retry `attempt`, capped at `maxInterval`. | +| `retryAfter` | none | `() => Observable`: each retry waits for its first value before its delay starts, to wait for the network to come back, for example. | +| `retryAfterDelay` | none | `(attempt, error) => boolean`, checked once the delay has passed. Return `false` to let the error through instead of retrying. | +| `caughtError` | none | `(errorCount, error) => Observable \| undefined`, called for each error that will be retried. The values of the Observable it returns are emitted before the retry. | +| `catchError` | none | `(errorCount, error) => Observable`, called once retrying stops. The Observable it returns replaces the error. | + +### Reference + +```typescript +function retryBackoff( + config: RetryBackoffConfig, +): (source: Observable) => Observable +``` diff --git a/gitbook/reactjrx/utils/rxjs/tracksubscriptions.md b/gitbook/reactjrx/utils/rxjs/tracksubscriptions.md deleted file mode 100644 index 030fa97..0000000 --- a/gitbook/reactjrx/utils/rxjs/tracksubscriptions.md +++ /dev/null @@ -1,3 +0,0 @@ -# trackSubscriptions - -todo diff --git a/src/lib/utils/arrayEqual.test.ts b/src/lib/utils/arrayEqual.test.ts new file mode 100644 index 0000000..78220cd --- /dev/null +++ b/src/lib/utils/arrayEqual.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest" +import { arrayEqual } from "./arrayEqual" + +describe("arrayEqual", () => { + it("should compare the items at each index", () => { + expect(arrayEqual([1, 2], [1, 2])).toBe(true) + expect(arrayEqual([1, 2], [2, 1])).toBe(false) + expect(arrayEqual([1], [1, 2])).toBe(false) + }) + + it("should compare the holes of a sparse array too", () => { + expect(arrayEqual(new Array(1), [1])).toBe(false) + expect(arrayEqual([1], new Array(1))).toBe(false) + }) +}) diff --git a/src/lib/utils/arrayEqual.ts b/src/lib/utils/arrayEqual.ts index b011665..11542d7 100644 --- a/src/lib/utils/arrayEqual.ts +++ b/src/lib/utils/arrayEqual.ts @@ -1,2 +1,10 @@ -export const arrayEqual = (a: readonly A[], b: readonly B[]) => - a.length === b.length && a.every((v, i) => (v as unknown) === b[i]) +export const arrayEqual = (a: readonly A[], b: readonly B[]) => { + if (a.length !== b.length) return false + + // A loop rather than `every`, which skips the holes of a sparse array. + for (let index = 0; index < a.length; index++) { + if ((a[index] as unknown) !== b[index]) return false + } + + return true +}