@zag-js/preact
Version:
The preact wrapper for zag
267 lines (266 loc) • 7.87 kB
JavaScript
// src/machine.ts
import {
createScope,
findTransition,
getExitEnterStates,
hasTag,
INIT_STATE,
MachineStatus,
matchesState,
resolveStateValue
} from "@zag-js/core";
import { callAll, ensure, isFunction, isString, toArray, warn } from "@zag-js/utils";
import { flushSync } from "preact/compat";
import { useLayoutEffect, useMemo, useRef } from "preact/hooks";
import { useBindable } from "./bindable.mjs";
import { useRefs } from "./refs.mjs";
import { useTrack } from "./track.mjs";
function useMachine(machine, userProps = {}) {
const scope = useMemo(() => {
const { id, ids, getRootNode } = userProps;
return createScope({ id, ids, getRootNode });
}, [userProps]);
const debug = (...args) => {
if (machine.debug) console.log(...args);
};
const props = machine.props?.({ props: userProps, scope }) ?? userProps;
const prop = useProp(props);
const context = machine.context?.({
prop,
bindable: useBindable,
scope,
flush,
getContext() {
return ctx;
},
getComputed() {
return computed;
},
getRefs() {
return refs;
},
getEvent() {
return getEvent();
}
});
const contextRef = useLiveRef(context);
const ctx = {
get(key) {
return contextRef.current?.[key].get();
},
set(key, value) {
contextRef.current?.[key].set(value);
},
initial(key) {
return contextRef.current?.[key].initial;
},
hash(key) {
const current = contextRef.current?.[key].get();
return contextRef.current?.[key].hash(current);
}
};
const effects = useRef(/* @__PURE__ */ new Map());
const transitionRef = useRef(null);
const previousEventRef = useRef(null);
const eventRef = useRef({ type: "" });
const refs = useRefs(machine.refs?.({ prop, context: ctx }) ?? {});
const getEvent = () => ({
...eventRef.current,
current() {
return eventRef.current;
},
previous() {
return previousEventRef.current;
}
});
const getState = () => ({
...state,
hasTag(tag) {
const currentState = state.get();
return hasTag(machine, currentState, tag);
},
matches(...values) {
const currentState = state.get();
return values.some((value) => matchesState(currentState, value));
}
});
const getParams = () => ({
state: getState(),
context: ctx,
event: getEvent(),
prop,
send,
action,
guard,
track: useTrack,
refs,
computed,
flush,
scope,
choose
});
const action = (keys) => {
const strs = isFunction(keys) ? keys(getParams()) : keys;
if (!strs) return;
const fns = strs.map((s) => {
const fn = machine.implementations?.actions?.[s];
if (!fn) warn(`[zag-js] No implementation found for action "${JSON.stringify(s)}"`);
return fn;
});
for (const fn of fns) {
fn?.(getParams());
}
};
const guard = (str) => {
if (isFunction(str)) return str(getParams());
const fn = machine.implementations?.guards?.[str];
if (!fn) warn(`[zag-js] No implementation found for guard "${JSON.stringify(str)}"`);
return fn?.(getParams());
};
const effect = (keys) => {
const strs = isFunction(keys) ? keys(getParams()) : keys;
if (!strs) return;
const fns = strs.map((s) => {
const fn = machine.implementations?.effects?.[s];
if (!fn) warn(`[zag-js] No implementation found for effect "${JSON.stringify(s)}"`);
return fn;
});
const cleanups = [];
for (const fn of fns) {
const cleanup = fn?.(getParams());
if (cleanup) cleanups.push(cleanup);
}
return () => cleanups.forEach((fn) => fn?.());
};
const choose = (transitions) => {
return toArray(transitions).find((t) => {
let result = !t.guard;
if (isString(t.guard)) result = !!guard(t.guard);
else if (isFunction(t.guard)) result = t.guard(getParams());
return result;
});
};
const computed = (key) => {
ensure(machine.computed, () => `[zag-js] No computed object found on machine`);
const fn = machine.computed[key];
return fn({
context: ctx,
event: getEvent(),
prop,
refs,
scope,
computed
});
};
const state = useBindable(() => ({
defaultValue: resolveStateValue(machine, machine.initialState({ prop })),
onChange(nextState, prevState) {
currentStateRef.current = nextState;
const { exiting, entering } = getExitEnterStates(machine, prevState, nextState, transitionRef.current?.reenter);
exiting.forEach((item) => {
const exitEffects = effects.current.get(item.path);
exitEffects?.();
effects.current.delete(item.path);
});
exiting.forEach((item) => {
action(item.state?.exit);
});
action(transitionRef.current?.actions);
entering.forEach((item) => {
const cleanup = effect(item.state?.effects);
if (cleanup) {
const existing = effects.current.get(item.path);
effects.current.set(item.path, existing ? callAll(existing, cleanup) : cleanup);
}
});
if (prevState === INIT_STATE) {
action(machine.entry);
const cleanup = effect(machine.effects);
if (cleanup) {
const existing = effects.current.get(INIT_STATE);
effects.current.set(INIT_STATE, existing ? callAll(existing, cleanup) : cleanup);
}
}
entering.forEach((item) => {
action(item.state?.entry);
});
}
}));
const currentStateRef = useRef(state.initial);
const hydratedStateRef = useRef(void 0);
const statusRef = useRef(MachineStatus.NotStarted);
useLayoutEffect(() => {
const started = statusRef.current === MachineStatus.Started;
statusRef.current = MachineStatus.Started;
debug(started ? "rehydrating..." : "initializing...");
const initialState = hydratedStateRef.current ?? state.initial;
state.invoke(initialState, started ? state.get() : INIT_STATE);
const fns = effects.current;
return () => {
const currentState = getCurrentState();
debug("unmounting...");
hydratedStateRef.current = currentState;
statusRef.current = MachineStatus.Stopped;
fns.forEach((fn) => fn?.());
effects.current = /* @__PURE__ */ new Map();
transitionRef.current = null;
action(machine.exit);
};
}, []);
const getCurrentState = () => {
return currentStateRef.current;
};
const send = (event) => {
queueMicrotask(() => {
if (statusRef.current !== MachineStatus.Started) return;
previousEventRef.current = eventRef.current;
eventRef.current = event;
let currentState = getCurrentState();
const { transitions, source } = findTransition(machine, currentState, event.type);
const transition = choose(transitions);
if (!transition) return;
transitionRef.current = transition;
const target = resolveStateValue(machine, transition.target ?? currentState, source);
const changed = target !== currentState;
if (changed) {
currentStateRef.current = target;
flushSync(() => state.set(target));
} else if (transition.reenter) {
state.invoke(currentState, currentState);
} else {
action(transition.actions ?? []);
}
});
};
machine.watch?.(getParams());
return {
state: getState(),
send,
context: ctx,
prop,
scope,
refs,
computed,
event: getEvent(),
getStatus: () => statusRef.current
};
}
function useLiveRef(value) {
const ref = useRef(value);
ref.current = value;
return ref;
}
function useProp(value) {
const ref = useLiveRef(value);
return function get(key) {
return ref.current[key];
};
}
function flush(fn) {
queueMicrotask(() => {
flushSync(() => fn());
});
}
export {
useMachine
};