ariakit-react-utils
Version:
Ariakit React utils
267 lines (258 loc) • 7.91 kB
JavaScript
;
var React = require('react');
var array = require('ariakit-utils/array');
var misc = require('ariakit-utils/misc');
var hooks = require('./hooks.js');
var system = require('./system.js');
var jsxRuntime = require('react/jsx-runtime');
const GET_STATE = Symbol("getState");
const SUBSCRIBE = Symbol("subscribe");
const TIMESTAMP = Symbol("timestamp");
const INITIAL_CONTEXT = Symbol("initialContext");
function getState(state) {
if (!state) return state;
const fn = state[GET_STATE];
if (fn) return fn();
return state;
}
function hasSubscribe(state) {
if (!state) return false;
return !!state[SUBSCRIBE];
}
function getSubscribe(state) {
if (!state) return;
return state[SUBSCRIBE];
}
function getLatest(a, b) {
if (!b) return a;
if (!a) return b;
if (!(TIMESTAMP in b)) return a;
if (!(TIMESTAMP in a)) return b;
if (a[TIMESTAMP] >= b[TIMESTAMP]) return a;
return b;
}
function defineGetState(state, currentState) {
if (currentState === void 0) {
currentState = state;
}
Object.defineProperty(state, GET_STATE, {
value: () => currentState,
writable: true
});
}
function defineSubscribe(state, subscribe) {
if (!(SUBSCRIBE in state)) {
Object.defineProperty(state, SUBSCRIBE, {
value: subscribe
});
}
}
function defineTimestamp(state) {
if (!(TIMESTAMP in state)) {
Object.defineProperty(state, TIMESTAMP, {
value: Date.now(),
writable: true
});
}
}
function patchState(state) {
Object.defineProperty(state, TIMESTAMP, {
value: Date.now(),
writable: true
});
}
function defineInitialContext(context) {
const initialContext = /*#__PURE__*/React.createContext(undefined);
Object.defineProperty(context, INITIAL_CONTEXT, {
value: initialContext
});
return initialContext;
}
function hasInitialContext(stateOrContext) {
return stateOrContext && INITIAL_CONTEXT in stateOrContext;
}
function getInitialContext(context) {
if (!hasInitialContext(context)) return;
const ctx = context;
return ctx[INITIAL_CONTEXT];
}
/**
* Creates a context that can be passed to `useStore` and `useStoreProvider`.
*/
function createStoreContext() {
const context = /*#__PURE__*/React.createContext(undefined);
defineInitialContext(context);
return context;
}
/**
* Creates a type-safe component with the `as` prop, `state` prop,
* `React.forwardRef` and `React.memo`.
*
* @example
* import { Options, createMemoComponent } from "ariakit-react-utils/store";
*
* type Props = Options<"div"> & {
* state?: { customProp: boolean };
* };
*
* const Component = createMemoComponent<Props>(
* ({ state, ...props }) => <div {...props} />
* );
*
* <Component as="button" state={{ customProp: true }} />
*/
function createMemoComponent(render, propsAreEqual) {
if (propsAreEqual === void 0) {
propsAreEqual = misc.shallowEqual;
}
const Role = system.createComponent(render);
return /*#__PURE__*/React.memo(Role, (prev, next) => {
const {
state: prevState,
...prevProps
} = prev;
const {
state: nextState,
...nextProps
} = next;
if (nextState && hasSubscribe(nextState)) {
return propsAreEqual(prevProps, nextProps);
}
return propsAreEqual(prev, next);
});
}
/**
* Returns props with a `wrapElement` function that wraps an element with a
* React Context Provider that provides a store context to consumers.
* @example
* import * as React from "react";
* import { useStoreProvider } from "ariakit-react-utils/store";
*
* const StoreContext = createStoreContext();
*
* function Component({ state, ...props }) {
* const { wrapElement } = useStoreProvider({ state, ...props }, StoreContext);
* return wrapElement(<div {...props} />);
* }
*/
function useStoreProvider(_ref, context) {
let {
state,
...props
} = _ref;
const initialValue = hooks.useInitialValue(state);
const value = state && hasSubscribe(state) ? initialValue : state;
defineGetState(value, state);
const initialContext = getInitialContext(context);
return hooks.useWrapElement(props, element => {
if (value && initialContext) {
element = /*#__PURE__*/jsxRuntime.jsx(initialContext.Provider, {
value: value,
children: element
});
}
if (state) {
element = /*#__PURE__*/jsxRuntime.jsx(context.Provider, {
value: state,
children: element
});
}
return element;
}, [value, initialContext, state, context]);
}
/**
* Adds publishing capabilities to state so it can be passed to `useStore` or
* `useStoreProvider` later.
* @example
* import { useStorePublisher } from "ariakit-react-utils/store";
*
* function useComponentState() {
* const state = React.useMemo(() => ({ a: "a" }), []);
* return useStorePublisher(state);
* }
*/
function useStorePublisher(state) {
const listeners = hooks.useLazyValue(() => new Set());
hooks.useSafeLayoutEffect(() => {
patchState(state);
for (const listener of listeners) {
listener(state);
}
}, [state]);
const subscribe = React.useCallback(listener => {
listeners.add(listener);
return () => listeners.delete(listener);
}, []);
defineSubscribe(state, subscribe);
defineGetState(state);
defineTimestamp(state);
return state;
}
/**
* Handles state updates on the state or context state passed as the first
* argument based on the filter argument.
* @example
* import { useStore } from "ariakit-react-utils/store";
*
* const ContextState = createContextState();
*
* function Component({ state }) {
* state = useStore(state || ContextState, ["stateProp"]);
* }
*/
function useStore(stateOrContext, filter) {
const contextState = React.useContext(getContext(stateOrContext, filter));
const externalState = hasInitialContext(stateOrContext) ? contextState : stateOrContext;
const [internalState, setState] = React.useState(() => getState(externalState));
const state = hasSubscribe(externalState) && hasSubscribe(internalState) ? getLatest(internalState, externalState) : externalState;
const subscribe = getSubscribe(externalState);
const prevStateRef = React.useRef(null);
const deps = array.toArray(filter);
const noFilter = !filter;
hooks.useSafeLayoutEffect(() => {
if (!subscribe || !setState) return;
if (noFilter) return subscribe(setState);
if (!deps.length) return;
return subscribe(nextState => {
const prevState = prevStateRef.current;
prevStateRef.current = nextState;
const filterDep = dep => {
if (typeof dep === "function") {
const result = dep(nextState);
// TODO: We probably need different functions for:
// useStore(context, [(nextState) => nextState.activeId === id]);
// useStore(context, [(nextState) => nextState.booleanProp]);
// Because in the second case we want to compare the result with the
// previous state result.
if (typeof result === "boolean") {
return result || prevState && dep(prevState);
} else if (prevState) {
return result !== dep(prevState);
}
return result;
}
const key = dep;
return prevState?.[key] !== nextState[key];
};
if (deps.some(filterDep)) {
setState(nextState);
}
});
}, [subscribe, setState, noFilter, ...deps]);
return state;
}
const EmptyContext = /*#__PURE__*/React.createContext(undefined);
function getContext(stateOrContext, filter) {
if (!hasInitialContext(stateOrContext)) {
return EmptyContext;
}
if (filter) {
return getInitialContext(stateOrContext);
}
return stateOrContext;
}
exports.createMemoComponent = createMemoComponent;
exports.createStoreContext = createStoreContext;
exports.useStore = useStore;
exports.useStoreProvider = useStoreProvider;
exports.useStorePublisher = useStorePublisher;