react-async-states
Version:
A low-level multi paradigm state management library
229 lines (226 loc) • 10.3 kB
JavaScript
import * as React from 'react';
import { createContext } from 'async-states';
import { Context, isServer } from './context.js';
import { isFunction, __DEV__ } from '../shared/index.js';
let idPrefix = "$$as-";
function Provider({ exclude, children, context: contextArg, serverInsertedHtmlHook, }) {
// automatically reuse parent context when there is and no 'context' object
// is provided
let reactId = React.useId();
let parentLibraryProvider = React.useContext(Context);
let libraryContextObject = React.useMemo(() => {
if (!contextArg && contextArg !== null) {
if (parentLibraryProvider) {
return parentLibraryProvider;
}
let libraryContext = createContext({});
libraryContext.name = `__$$${reactId}`;
return libraryContext;
}
let libraryContext = createContext(contextArg);
if (contextArg !== null) {
libraryContext.name = `__$$${reactId}`;
}
else if (isServer) {
throw new Error("Global context cannot be used server");
}
return libraryContext;
}, [reactId, contextArg]);
if (isFunction(serverInsertedHtmlHook)) {
serverInsertedHtmlHook(() => (React.createElement(HydrateRemainingContextInstances, { exclude: exclude, context: libraryContextObject })));
}
React.useEffect(() => ensureOnDemandHydrationExistsInClient(libraryContextObject), [libraryContextObject]);
// memoized children will unlock the React context children optimization:
// if the children reference is the same as the previous render, it will
// bail out and skip the children render and only propagates the context
// change.
return (React.createElement(Context.Provider, { value: libraryContextObject }, children));
}
function ensureOnDemandHydrationExistsInClient(libraryContextObject) {
let ctxName = libraryContextObject.name;
let hydrationDataPropName = !ctxName ? "__$$" : ctxName;
let rehydrationFunctionName = !ctxName ? "__$$_H" : `${ctxName}_H`;
window[rehydrationFunctionName] = function rehydrateContext() {
let hydrationData = window[hydrationDataPropName];
if (!hydrationData || !libraryContextObject) {
return;
}
Object.entries(hydrationData).forEach(([key, instanceHydration]) => {
let instance = libraryContextObject.get(key);
if (!instance || !instanceHydration) {
return;
}
let [state, latestRun, payload] = instanceHydration;
instance.version += 1;
instance.state = state;
let promise = instance.promise;
// next, we may have already hydrated the "pending" state, in this case
// we put a never resolving promise and it probably did suspend a tree
// in this case, we will resolve/reject it imperatively because
// we keep track of this value.
// setting state won't resolve because if this is the first ever component
// render and mount, it won't run any effects and thus no subscribers.
// so, the only way is to inform react that the suspending promise did
// fulfill, via its resolve and reject functions.
if (state.status === "success") {
instance.lastSuccess = state;
if (promise) {
promise.value = state.data;
promise.status = "fulfilled";
}
instance.res?.res(state.data);
}
else if (state.status === "error" && promise) {
promise.status = "rejected";
promise.reason = state.data;
instance.res?.rej(state.data);
}
instance.payload = payload;
instance.latestRun = latestRun;
let subscriptions = instance.subscriptions;
if (subscriptions) {
Object.values(subscriptions).forEach((sub) => sub.props.cb(state));
}
delete hydrationData[key];
});
};
}
function HydrateRemainingContextInstances({ context, exclude, }) {
let sources = context
.getAll()
.filter((instance) => {
// this means we already hydrated this instance in this context with this
// exact same version
if (context.payload[instance.key] === instance.version) {
return false;
}
if (isFunction(exclude)) {
return !exclude(instance.key, instance.state);
}
return true;
})
.map((t) => t.actions);
return (React.createElement(HydrationServer, { useReactId: false, context: context, target: sources }));
}
function HydrationComponent({ target }) {
let context = React.useContext(Context);
if (isServer) {
return React.createElement(HydrationServer, { context: context, target: target });
}
return React.createElement(HydrationClient, { target: target });
}
function HydrationServer({ context, target, useReactId, }) {
let reactId = React.useId();
let hydrationData = buildWindowAssignment(target, context);
if (!hydrationData) {
return null;
}
let id = useReactId === false ? undefined : `${idPrefix}${reactId}`;
return (React.createElement("script", { id: id, dangerouslySetInnerHTML: { __html: hydrationData } }));
}
function HydrationClient(_props) {
let reactId = React.useId();
let id = `${idPrefix}${reactId}`;
let existingHtml = React.useMemo(() => ({ current: { html: null, init: false } }), []);
// We are using the "init" property explicitly to be more precise:
// If we didn't compute, let's do it, or else, just pass.
// In the or else path, it may be difficult to distinguish between falsy
// values and we would end up using two values anyways. So, we better use
// an object to be more explicit and readable.
// For example, we could do:
// if (existingHTML.current === null) ...
// But there is no guarantee that the innerHTML computation will always yield
// non null values. To avoid all of that, let's stick to basic javascript.
if (!existingHtml.current.init) {
let container = document.getElementById(id);
let containerInnerHTML = container?.innerHTML ?? null;
existingHtml.current = { init: true, html: containerInnerHTML };
}
let __html = existingHtml.current.html;
if (__html) {
return React.createElement("script", { id: id, dangerouslySetInnerHTML: { __html } });
}
return null;
}
function buildWindowAssignment(sources, context) {
if (!sources.length) {
return null;
}
let globalHydrationData = null;
let contextHydrationData = null;
for (let source of sources) {
let key = source.key;
let instance = context.get(key);
if (!instance) {
if (__DEV__) {
__DEV__warnInDevAboutHydratingSourceNotInContext(key);
}
throw new Error("Cannot leak server global source");
}
let { state, latestRun, payload, version } = instance;
if (context.payload[key] === version) {
continue;
}
else {
context.payload[key] = version;
}
// instance.global is true only and only if this instance was cloned
// from a server instance:
// ie: You have a global source object in the server, that you clone per
// request. When we perform this clone, we mark these sources as global:
// It was cloned from a globally accessible source.
// We do all of this because when hydrating, there are two types of states:
// Those we were global (not related to this render, but more of was
// created far away and a subscription is performed from this render),
// and those we are bound to the current Context in this particular render.
// When hydrating we distinguish between them so we won't leak source state
// and we properly assign the state to its instance.
// The script will later use __$$ for global context, and <context.name>
// for more granular contexts.
// When using the server, using a context is mandatory, but if your app
// is all global sources, then contextHydrationData will be basically empty
// and your global sources in the client will get hydrated correctly.
if (instance.global) {
if (!globalHydrationData) {
globalHydrationData = {};
}
globalHydrationData[key] = [state, latestRun, payload];
}
else {
if (!contextHydrationData) {
contextHydrationData = {};
}
contextHydrationData[key] = [state, latestRun, payload];
}
}
if (!globalHydrationData && !contextHydrationData) {
return null;
}
let hydrationData = ["var win=window;"];
if (globalHydrationData) {
let globalHydrationDataAsString = JSON.stringify(globalHydrationData);
hydrationData.push(buildHydrationScriptContent("__$$", globalHydrationDataAsString));
}
if (contextHydrationData) {
let contextName = context.name;
if (!contextName) {
throw new Error("Hydrating context without name, this is a bug");
}
let contextHydrationDataAsString = JSON.stringify(contextHydrationData);
hydrationData.push(buildHydrationScriptContent(contextName, contextHydrationDataAsString));
}
return hydrationData.join("");
}
function buildHydrationScriptContent(propName, data) {
return `win["${propName}"]=Object.assign(win["${propName}"]||{},${data});win["${propName}_H"]&&win["${propName}_H"]();`;
}
function __DEV__warnInDevAboutHydratingSourceNotInContext(key) {
if (__DEV__) {
console.error(`[async-states] source '${key}' doesn't exist` +
" in the context, this means that you tried to hydrate it " +
" before using it via hooks. Only hydrate a source after using it" +
" to avoid leaks and passing unused things to the client.");
}
}
export { HydrationComponent, Provider as default };
//# sourceMappingURL=Provider.js.map