fluidstate-preact
Version:
Library for using fine-grained reactivity state management library fluidstate in Preact
128 lines (115 loc) • 5.63 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useSignals = void 0;
var _hooks = require("preact/hooks");
var _onceRef = require("./once-ref");
var _signals = require("@preact/signals");
var _fluidstate = require("fluidstate");
/**
* A Preact hook that bridges `fluidstate` reactivity with `@preact/signals`.
* It returns a `get` function that can be used to create memoized Preact signals
* from `fluidstate` reactive state. This is the most performant way to use
* `fluidstate` in Preact, as it avoids component re-renders and updates only
* the specific parts of the DOM that depend on the state.
*
* @param Context - Optional. A Preact Context object. If provided, the state
* from this context will be available to the selector functions passed to `get`.
* This is typically used with `createReactiveSetup`.
* @returns A `get` function that takes a selector and returns a Preact Signal.
*/
const useSignals = Context => {
// A ref to store the context object and its latest value.
// This helps in detecting when the context value itself changes.
const context = (0, _onceRef.useOnceRef)(() => ({
Context,
contextValue: null
}));
// Get the current context value. If a context was provided, use `useContext`.
const contextValue = context.current.Context ? (0, _hooks.useContext)(context.current.Context) : null;
// Check if the context value has changed since the last render.
const isContextValueChanged = contextValue !== context.current.contextValue;
context.current.contextValue = contextValue;
// This ref holds all the reactive primitives for each `get` call.
// We use arrays and rely on the call index, similar to how React hooks work.
const data = (0, _onceRef.useOnceRef)(() => ({
latestIndex: 0,
fns: [],
fnAtoms: [],
resultSignals: [],
computeds: [],
reactions: []
}));
// Reset the index at the beginning of each render. The `get` function will increment it for each call.
data.current.latestIndex = -1;
// This effect runs after every render to clean up any reactive primitives
// that are no longer used (e.g., due to conditional rendering).
(0, _hooks.useEffect)(() => {
const fnsLength = data.current.latestIndex + 1;
// If the number of `get` calls decreased, stop the reactions for the orphaned signals.
for (let i = fnsLength; i < data.current.reactions.length; i++) {
data.current.reactions[i].stop();
}
// Truncate the arrays to the new length to release memory.
data.current.fns.length = fnsLength;
data.current.fnAtoms.length = fnsLength;
data.current.resultSignals.length = fnsLength;
data.current.reactions.length = fnsLength;
});
// This effect runs only once on mount and cleans up all reactions when the component unmounts.
(0, _hooks.useEffect)(() => {
return () => {
for (let i = 0; i < data.current.reactions.length; i++) {
data.current.reactions[i].stop();
}
};
}, []);
/**
* Creates and returns a memoized Preact signal for a given piece of `fluidstate` data.
* @param fn A selector function that accesses `fluidstate` reactive state and returns a value.
* @returns A Preact `Signal` that will automatically update when the underlying state changes.
*/
return fn => {
const currentIndex = ++data.current.latestIndex;
// Check if we've already set up the reactive primitives for this index.
if (data.current.fns[currentIndex]) {
// If so, this is a subsequent render. Check if the selector function or context value has changed.
if (data.current.fns[currentIndex] !== fn || isContextValueChanged) {
// If they have changed, update the function and notify `fluidstate` that this dependency has changed.
// This will trigger the computed atom to re-evaluate.
data.current.fns[currentIndex] = fn;
data.current.fnAtoms[currentIndex].reportChanged();
}
} else {
// This is the first time this `get` call is being made (at this index).
// We need to set up the entire reactive chain.
data.current.fns[currentIndex] = fn;
data.current.fnAtoms[currentIndex] = (0, _fluidstate.createAtom)(`useSignals/fnAtom@${currentIndex}`);
data.current.resultSignals[currentIndex] = (0, _signals.signal)(null);
// The core of the bridge: a `fluidstate` computed that, when run,
// executes the selector `fn` and updates the Preact signal's value.
// It depends on the `fnAtom` (for selector changes) and any reactive
// state accessed inside `fn`.
data.current.computeds[currentIndex] = (0, _fluidstate.createComputedAtom)(`useSignals/computed@${currentIndex}`, () => {
data.current.fnAtoms[currentIndex].reportObserved();
let result = data.current.fns[currentIndex](context.current.contextValue);
if (Array.isArray(result)) {
result.length;
result = Object.create(result);
}
data.current.resultSignals[currentIndex].value = result;
});
// A reaction is needed to keep the computed atom "hot" or "active".
// This ensures that the computed re-evaluates automatically whenever
// its dependencies change, thus keeping the Preact signal in sync.
data.current.reactions[currentIndex] = (0, _fluidstate.createReaction)(() => {
data.current.computeds[currentIndex].get();
});
}
// Return the memoized Preact signal for this index.
return data.current.resultSignals[currentIndex];
};
};
exports.useSignals = useSignals;
//# sourceMappingURL=signals.js.map