All files / react-tao/src useTaoDataState.js

100% Statements 30/30
100% Branches 16/16
100% Functions 7/7
100% Lines 30/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114                                                                              3x 3x 1x 3x 3x     2x 2x 7x 7x                                 33x     24x             33x 33x 33x 33x     33x 9x 9x 9x 9x         3x 3x 3x         9x 1x     8x 5x         33x 33x   33x    
import { useCallback, useRef, useState } from 'react';
import { AppCtx } from '@tao.js/core';
 
import { getPermutations } from './helpers';
import useTaoInlineSubscription from './useTaoInlineSubscription';
 
/** @typedef {import('./helpers').TrigramProps} TrigramProps */
/** @typedef {import('./helpers').TaoSignal} TaoSignal */
 
/**
 * Derives the next handler-managed data from a matching AppCon. Used by
 * `DataHandler`, `createContextHandler`, and `withContext`.
 *
 * Three ways to produce state (in precedence order):
 * 1. call `set(next)` — replaces state with `next`, clearing any previous
 *    keys `next` omits (`set(null)` clears every key to undefined); once
 *    `set` is used, a non-AppCtx return value is ignored;
 * 2. return the next state directly (nullish return = no state change);
 * 3. return an {@link AppCtx} — it is chained on the network instead of
 *    becoming state (works alongside a prior `set` call).
 *
 * @template [S=any]
 * @callback TaoDataHandler
 * @param {TaoSignal} tao - the dispatched AppCon's concrete trigram
 * @param {*} data - the AppCon's data (always an object, may be empty)
 * @param {(next: ?S) => void} set - imperative state setter (see above)
 * @param {S} current - the state at dispatch time
 * @returns {S|AppCtx|void} next state, an AppCtx to chain, or nothing
 */
 
/**
 * Build the next state from `newState`, clearing (to `undefined`) every key
 * of `previousState` that `newState` omits; `null`/`undefined` clears all
 * previous keys.
 * @param {Object<string, *>} previousState
 * @param {?Object<string, *>} [newState]
 * @returns {Object<string, *>}
 */
export function cleanState(previousState, newState) {
  const keys = Object.keys(previousState);
  if (newState == null) {
    return keys.reduce((rv, key) => {
      rv[key] = void 0;
      return rv;
    }, {});
  }
  keys.push(...Object.keys(newState));
  return keys.reduce((rv, key) => {
    rv[key] = newState[key];
    return rv;
  }, {});
}
 
/**
 * Local state driven by Kernel inline handlers for a trigram (+ optional handler).
 * Without a `handler`, each matching AppCon's data replaces the state.
 * @template [S=any]
 * @param {?TrigramProps} trigramProps - trigram(s) to subscribe to (array
 *        parts multi-match; null/empty = one all-wildcard subscription)
 * @param {?TaoDataHandler<S>} [handler] - derives the next state per signal;
 *        omitted = the AppCon data becomes the state
 * @param {S|(() => S)} [defaultValue] - initial state or lazy initializer
 *        (defaults to `{}`)
 * @returns {S} the current handler-managed state
 */
export default function useTaoDataState(trigramProps, handler, defaultValue) {
  const [state, setState] = useState(
    /** @type {() => S} */ (
      () =>
        typeof defaultValue === 'function'
          ? /** @type {() => S} */ (defaultValue)()
          : defaultValue != null
            ? defaultValue
            : {}
    ),
  );
  const stateRef = useRef(state);
  stateRef.current = state;
  const handlerRef = useRef(handler);
  handlerRef.current = handler;
 
  // Stryker disable all: stable onSignal; handlerRef keeps latest handler; deps ArrayDeclaration equiv
  const onSignal = useCallback((tao, data) => {
    let usedSet = false;
    const current = stateRef.current;
    const activeHandler = handlerRef.current;
    const dataUpdate = activeHandler
      ? activeHandler(
          tao,
          data,
          (next) => {
            const update = /** @type {S} */ (cleanState(current, next));
            setState(update);
            usedSet = true;
          },
          current,
        )
      : data;
    if (dataUpdate instanceof AppCtx) {
      return dataUpdate;
    }
    // Stryker disable next-line ConditionalExpression: usedSet precedence covered in tests
    if (!usedSet && dataUpdate != null) {
      setState(dataUpdate);
    }
  }, []);
  // Stryker restore all
 
  const trigrams = getPermutations(trigramProps || {});
  useTaoInlineSubscription(trigrams, onSignal);
 
  return state;
}