Skip to content
Draft
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 lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import PendingWrites from './PendingWrites';
import logMessages from './logMessages';
import onyxSubscriptionManager from './OnyxSubscriptionManager';
import OnyxMerge from './OnyxMerge';
import {clearSharedResults} from './sharedResults';

/** Initialize the store with actions and listening for storage events */
function init({
Expand Down Expand Up @@ -439,6 +440,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {

const clearPromise = PendingWrites.trackPendingGlobalWrite(
OnyxUtils.afterInit(() => {
clearSharedResults();
const pendingMergeEntries = OnyxUtils.getPendingMergeEntries(keysToPreserve);
const defaultKeyStates = OnyxUtils.getDefaultKeyStates();
const initialKeys = Object.keys(defaultKeyStates);
Expand Down
93 changes: 93 additions & 0 deletions lib/createDerivedValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type {OnyxKey, OnyxValue} from './types';
import type {OnyxStateViewFor} from './useOnyxState';

import {areSameValues, registerDerivedValueReset} from './sharedResults';

type DerivedValuePrevious<T, TDependencies extends readonly OnyxKey[] = readonly OnyxKey[]> = {
/** The dependencies behind `result`, so `compute` can diff them against the current ones. */
state: OnyxStateViewFor<TDependencies>;

result: T;

/** Returns `result` and changes nothing, so a compute that waits or fails runs again on the next `get`. */
keep: () => T;
};

type CreateDerivedValueOptions<T, TDependencies extends readonly OnyxKey[] = readonly OnyxKey[]> = {
/** Keys the value is computed from. Every `useOnyxState` hook that calls `get` must declare at least these. */
dependencies: TDependencies;

/** Annotate the return type when reading `previous`, or `T` is inferred as `unknown`. `previous` is undefined on the first compute. */
compute: (state: OnyxStateViewFor<TDependencies>, previous: DerivedValuePrevious<T, TDependencies> | undefined) => T;
};

type DerivedValue<T, TDependencies extends readonly OnyxKey[] = readonly OnyxKey[]> = {
dependencies: TDependencies;

/** Pass the `state` a `useOnyxState` selector gets, never `previousState`. */
get: (state: OnyxStateViewFor<TDependencies>) => T;

/** Makes the next `get` compute from scratch. Onyx.clear calls it too. */
reset: () => void;
};

type Computed<T> = {
depValues: Array<OnyxValue<OnyxKey>>;
result: T;
};

/** A value derived from several keys, computed once per change for every `useOnyxState` selector that calls `get`, inline ones included. */
function createDerivedValue<T, const TDependencies extends readonly OnyxKey[]>({dependencies, compute}: CreateDerivedValueOptions<T, TDependencies>): DerivedValue<T, TDependencies> {
let lastRead: Computed<T> | undefined;
// What `compute` diffs against, which is the last compute that did not call `keep`.
let baseline: Computed<T> | undefined;
// Bumped by `reset`, so a compute that called it does not write its result back as the next baseline.
let generation = 0;

function get(state: OnyxStateViewFor<TDependencies>): T {
const view = state as Record<string, OnyxValue<OnyxKey>>;
const depValues = dependencies.map((key) => view[key]);
if (lastRead && areSameValues(depValues, lastRead.depValues)) {
return lastRead.result;
}

let isKept = false;
let previous: DerivedValuePrevious<T, TDependencies> | undefined;
if (baseline) {
const kept = baseline;
const previousState: Record<string, OnyxValue<OnyxKey>> = {};
for (let index = 0; index < dependencies.length; index++) {
previousState[dependencies[index]] = kept.depValues[index];
}
previous = {
state: Object.freeze(previousState) as OnyxStateViewFor<TDependencies>,
result: kept.result,
keep: () => {
isKept = true;
return kept.result;
},
};
}

const computeGeneration = generation;
const result = compute(state, previous);
if (!isKept && computeGeneration === generation) {
lastRead = {depValues, result};
baseline = {depValues, result};
}
return result;
}

function reset() {
generation++;
lastRead = undefined;
baseline = undefined;
}

registerDerivedValueReset(reset);

return {dependencies, get, reset};
}

export default createDerivedValue;
export type {CreateDerivedValueOptions, DerivedValue, DerivedValuePrevious};
7 changes: 6 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import type {FetchStatus, ResultMetadata, UseOnyxResult, UseOnyxOptions} from '.
import useOnyx from './useOnyx';
import type {OnyxStateView, UseOnyxStateOptions, UseOnyxStateSelector} from './useOnyxState';
import useOnyxState from './useOnyxState';
import type {CreateDerivedValueOptions, DerivedValue, DerivedValuePrevious} from './createDerivedValue';
import createDerivedValue from './createDerivedValue';
import type {OnyxSQLiteKeyValuePair} from './storage/providers/SQLiteProvider';

export default Onyx;
export {useOnyx, useOnyxState};
export {useOnyx, useOnyxState, createDerivedValue};
export type {
ConnectOptions,
CustomTypeOptions,
Expand Down Expand Up @@ -53,5 +55,8 @@ export type {
OnyxStateView,
UseOnyxStateOptions,
UseOnyxStateSelector,
CreateDerivedValueOptions,
DerivedValue,
DerivedValuePrevious,
OnyxSQLiteKeyValuePair,
};
47 changes: 47 additions & 0 deletions lib/sharedResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {OnyxKey, OnyxValue} from './types';

type SharedResult = {
depsKey: string;
depValues: Array<OnyxValue<OnyxKey>>;
value: unknown;
readPreviousState: boolean;
hadPreviousView: boolean;
};

// Results shared between hooks live in module state, so Onyx.clear drops them with the data they came from.
let sharedResults = new WeakMap<object, SharedResult>();
const derivedValueResets = new Set<() => void>();

function getSharedResult(selector: object): SharedResult | undefined {
return sharedResults.get(selector);
}

function setSharedResult(selector: object, result: SharedResult) {
sharedResults.set(selector, result);
}

function registerDerivedValueReset(reset: () => void) {
derivedValueResets.add(reset);
}

function clearSharedResults() {
sharedResults = new WeakMap();
for (const reset of derivedValueResets) {
reset();
}
}

function areSameValues(a: Array<OnyxValue<OnyxKey>>, b: Array<OnyxValue<OnyxKey>>): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!Object.is(a[i], b[i])) {
return false;
}
}
return true;
}

export {areSameValues, clearSharedResults, getSharedResult, registerDerivedValueReset, setSharedResult};
export type {SharedResult};
27 changes: 20 additions & 7 deletions lib/useOnyxState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {useCallback, useEffect, useMemo, useRef, useSyncExternalStore} from 'rea
import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxKey, OnyxValue} from './types';

import onyxSubscriptionManager from './OnyxSubscriptionManager';
import {areSameValues, getSharedResult, setSharedResult} from './sharedResults';
import OnyxUtils from './OnyxUtils';

/**
Expand Down Expand Up @@ -215,18 +216,30 @@ function useOnyxState<T, const TDependencies extends readonly OnyxKey[]>(selecto
}

const depValues = depsArray.map((dep) => onyxSubscriptionManager.getState(dep));
previousStateReadRef.current = false;
// The views carry every key at runtime; the narrowing is a constraint on what a selector may
// read, so hand them over as the declared shape.
const output = selector(liveStateView as OnyxStateViewFor<TDependencies>, previousView as OnyxStateViewFor<TDependencies> | undefined);
const readPreviousState = previousStateReadRef.current;

// Shared by stable selectors only. A run that read `previousState`, or saw it missing, depends on its own instance.
const hadPreviousView = previousView !== undefined;
const shared = getSharedResult(selector);
let output: T;
let readPreviousState: boolean;
if (shared && !shared.readPreviousState && shared.hadPreviousView === hadPreviousView && shared.depsKey === depsKey && areSameValues(shared.depValues, depValues)) {
output = shared.value as T;
readPreviousState = false;
} else {
previousStateReadRef.current = false;
// The views carry every key at runtime; the narrowing is a constraint on what a selector may
// read, so hand them over as the declared shape.
output = selector(liveStateView as OnyxStateViewFor<TDependencies>, previousView as OnyxStateViewFor<TDependencies> | undefined);
readPreviousState = previousStateReadRef.current;
setSharedResult(selector, {depsKey, depValues, value: output, readPreviousState, hadPreviousView});
}

// Hand back the previous reference when the output is equal, so React bails out via `===`
// and an unchanged store keeps returning the same value even as `getSnapshot`'s identity moves.
const value = lastSnapshot !== null && selectorEquality(lastSnapshot.value, output) ? lastSnapshot.value : output;
lastSnapshotRef.current = {value, selector, previousView, readPreviousState, depValues};
return value;
}, [selector, selectorEquality, depsArray]);
}, [selector, selectorEquality, depsArray, depsKey]);

const value = useSyncExternalStore(subscribe, getSnapshot);

Expand All @@ -250,4 +263,4 @@ function useOnyxState<T, const TDependencies extends readonly OnyxKey[]>(selecto
}

export default useOnyxState;
export type {OnyxStateView, UseOnyxStateOptions, UseOnyxStateSelector};
export type {OnyxStateView, OnyxStateViewFor, UseOnyxStateOptions, UseOnyxStateSelector};
Loading
Loading