@uxf/core-react 11.114.0 → 11.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -623,6 +623,50 @@ const {
623
623
 
624
624
  ---
625
625
 
626
+ ## Global Context
627
+
628
+ `createGlobalContext` is a thin wrapper around `React.createContext` that caches the resulting Context object on `globalThis`, keyed by a unique name. Use it instead of `createContext` for any Context exported from a library package.
629
+
630
+ ### Why it exists
631
+
632
+ Under Turbopack a single `"use client"` module can be evaluated more than once — typically once per `"use client"` boundary that imports it. Each evaluation calls `createContext(...)` separately, producing distinct Context identities in the same JS realm. The `Provider` ends up writing into one identity and `useContext` reads from another, so consumers silently fall back to the default value. Caching the Context on `globalThis` collapses all evaluations to a single identity.
633
+
634
+ Webpack does not exhibit this because its chunking layer deduplicates by module ID; Turbopack does not (yet) guarantee that for client-boundary modules.
635
+
636
+ ### Usage
637
+
638
+ ```tsx
639
+ import { createGlobalContext } from "@uxf/core-react/global-context";
640
+ import { useContext } from "react";
641
+
642
+ const themeContext = createGlobalContext<"light" | "dark">("ui/theme", "light");
643
+
644
+ export const ThemeProvider = themeContext.Provider;
645
+
646
+ export function useTheme() {
647
+ return useContext(themeContext);
648
+ }
649
+ ```
650
+
651
+ ### Naming convention
652
+
653
+ The `name` argument must be unique across the monorepo. Use `"<package>/<context>"`, for example:
654
+
655
+ - `"core-react/translations"`
656
+ - `"localize/locale"`
657
+ - `"ui/color-scheme"`
658
+
659
+ The same name on subsequent calls returns the original Context — the `defaultValue` argument is honored only on the first call.
660
+
661
+ ### When to use it
662
+
663
+ Use `createGlobalContext` for any Context that is:
664
+
665
+ - exported from a library package (`@uxf/*`), or
666
+ - consumed from more than one `"use client"` boundary.
667
+
668
+ For Contexts that are private to a single app file and never imported elsewhere, plain `React.createContext` is fine.
669
+
626
670
  ## Translations
627
671
 
628
672
  Provides a translation system that can be used in both single-language and multi-language projects.
@@ -14,10 +14,12 @@ function useAnchorProps(props, options = {}) {
14
14
  const isSubmitType = type === "submit";
15
15
  const isButton = isSubmitType || (0, is_not_nil_1.isNotNil)(onClick);
16
16
  const tabIndexInteractive = isBusyOrDisabled ? -1 : tabIndex;
17
+ const isClickable = !isBusyOrDisabled && isButton;
18
+ const isHyperlink = !isBusyOrDisabled && Boolean(href);
17
19
  const simulatedButton = (0, _use_simulated_button_1._useSimulatedButton)({
18
20
  analyticsCallback,
19
- isClickable: !isBusyOrDisabled || isButton,
20
- isHyperlink: !isBusyOrDisabled || Boolean(href),
21
+ isClickable,
22
+ isHyperlink,
21
23
  onClick,
22
24
  onKeyDown,
23
25
  onKeyUp,
@@ -13,9 +13,10 @@ function useClickableProps(props, options = {}) {
13
13
  const isBusyOrDisabled = isBusy || isDisabledOrAriaDisabled;
14
14
  const isSubmitType = type === "submit";
15
15
  const isButton = isSubmitType || (0, is_not_nil_1.isNotNil)(onClick);
16
+ const isClickable = !isBusyOrDisabled && isButton;
16
17
  const simulatedButton = (0, _use_simulated_button_1._useSimulatedButton)({
17
18
  analyticsCallback,
18
- isClickable: !isBusyOrDisabled || isButton,
19
+ isClickable,
19
20
  isHyperlink: false,
20
21
  onClick,
21
22
  onKeyDown,
@@ -0,0 +1,16 @@
1
+ import { Context } from "react";
2
+ /**
3
+ * Creates a React Context cached on globalThis. Under Turbopack a single
4
+ * "use client" module can be evaluated more than once (per chunk/boundary),
5
+ * which would otherwise produce different Context identities — Provider and
6
+ * useContext would not meet. Caching by `name` guarantees a single identity
7
+ * across evaluations within one React runtime.
8
+ *
9
+ * Note: the cache is partitioned per React runtime (server vs client),
10
+ * because the two runtimes have incompatible Context shapes.
11
+ *
12
+ * `name` must be unique across the monorepo; recommended convention:
13
+ * "<package>/<context>", e.g. "core-react/translations", "localize/locale",
14
+ * "ui/color-scheme".
15
+ */
16
+ export declare function createGlobalContext<T>(name: string, defaultValue: T): Context<T>;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGlobalContext = createGlobalContext;
4
+ // NOTE: This file intentionally has no "use client" directive.
5
+ // createContext is a server-safe React API (not a hook), and adding "use client"
6
+ // here would turn this module into a client reference, making module-scope calls
7
+ // to createGlobalContext from non-"use client" Context files (e.g. ui/context.ts)
8
+ // resolve to a proxy object on the server — Provider would then no longer be a
9
+ // real component and prerender would fail with "Element type is invalid".
10
+ // Consumers that need useContext put "use client" on their own module.
11
+ const react_1 = require("react");
12
+ // Why two-level keying:
13
+ // In Next App Router (Next 16 + React 19) the server runtime loads a different
14
+ // React build than the client runtime (the `react-server` export condition).
15
+ // Each build has its own `createContext` function and produces Context objects
16
+ // with different internal shapes — they are NOT interchangeable. A naive
17
+ // `globalThis`-only singleton would serve the server-React Context to client
18
+ // code (or vice versa) and React rejects the foreign object with
19
+ // "Element type is invalid: ... got: object" at prerender.
20
+ //
21
+ // Outer WeakMap is keyed by the calling React's `createContext` function —
22
+ // distinct per React runtime, so server and client get separate registries.
23
+ // Inner Map then dedupes contexts by `name` within one runtime, which is the
24
+ // actual fix for the Turbopack chunk-duplication problem we set out to solve.
25
+ const REGISTRY_KEY = "__uxf_react_context_registry__";
26
+ function getRegistry() {
27
+ const g = globalThis;
28
+ if (!g[REGISTRY_KEY]) {
29
+ g[REGISTRY_KEY] = new WeakMap();
30
+ }
31
+ let perRuntime = g[REGISTRY_KEY].get(react_1.createContext);
32
+ if (!perRuntime) {
33
+ perRuntime = new Map();
34
+ g[REGISTRY_KEY].set(react_1.createContext, perRuntime);
35
+ }
36
+ return perRuntime;
37
+ }
38
+ /**
39
+ * Creates a React Context cached on globalThis. Under Turbopack a single
40
+ * "use client" module can be evaluated more than once (per chunk/boundary),
41
+ * which would otherwise produce different Context identities — Provider and
42
+ * useContext would not meet. Caching by `name` guarantees a single identity
43
+ * across evaluations within one React runtime.
44
+ *
45
+ * Note: the cache is partitioned per React runtime (server vs client),
46
+ * because the two runtimes have incompatible Context shapes.
47
+ *
48
+ * `name` must be unique across the monorepo; recommended convention:
49
+ * "<package>/<context>", e.g. "core-react/translations", "localize/locale",
50
+ * "ui/color-scheme".
51
+ */
52
+ function createGlobalContext(name, defaultValue) {
53
+ const registry = getRegistry();
54
+ const existing = registry.get(name);
55
+ if (existing) {
56
+ return existing;
57
+ }
58
+ const ctx = (0, react_1.createContext)(defaultValue);
59
+ ctx.displayName = `UxfGlobal(${name})`;
60
+ registry.set(name, ctx);
61
+ return ctx;
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const react_1 = require("@testing-library/react");
37
+ const react_2 = __importStar(require("react"));
38
+ const index_1 = require("./index");
39
+ const REGISTRY_KEY = "__uxf_react_context_registry__";
40
+ function clearRegistry() {
41
+ const g = globalThis;
42
+ delete g[REGISTRY_KEY];
43
+ }
44
+ describe("createGlobalContext", () => {
45
+ beforeEach(() => {
46
+ clearRegistry();
47
+ });
48
+ it("returns a React Context with a Provider", () => {
49
+ const ctx = (0, index_1.createGlobalContext)("test/basic", "default");
50
+ expect(ctx).toBeDefined();
51
+ expect(ctx.Provider).toBeDefined();
52
+ });
53
+ it("returns the same Context instance for the same name", () => {
54
+ const first = (0, index_1.createGlobalContext)("test/identity", "default");
55
+ const second = (0, index_1.createGlobalContext)("test/identity", "default");
56
+ expect(second).toBe(first);
57
+ });
58
+ it("returns different Context instances for different names", () => {
59
+ const a = (0, index_1.createGlobalContext)("test/a", "a");
60
+ const b = (0, index_1.createGlobalContext)("test/b", "b");
61
+ expect(a).not.toBe(b);
62
+ });
63
+ it("caches by name and ignores subsequent defaultValue arguments", () => {
64
+ const first = (0, index_1.createGlobalContext)("test/cache", "first-default");
65
+ const second = (0, index_1.createGlobalContext)("test/cache", "second-default");
66
+ expect(second).toBe(first);
67
+ const { result } = (0, react_1.renderHook)(() => (0, react_2.useContext)(first));
68
+ expect(result.current).toBe("first-default");
69
+ });
70
+ it("sets displayName for React DevTools", () => {
71
+ const ctx = (0, index_1.createGlobalContext)("test/display-name", 0);
72
+ expect(ctx.displayName).toBe("UxfGlobal(test/display-name)");
73
+ });
74
+ it("returns the provided value through Provider/useContext", () => {
75
+ const ctx = (0, index_1.createGlobalContext)("test/provider-consumer", "default");
76
+ function Consumer() {
77
+ const value = (0, react_2.useContext)(ctx);
78
+ return react_2.default.createElement("span", null, value);
79
+ }
80
+ function Wrapper(props) {
81
+ return react_2.default.createElement(ctx.Provider, { value: "provided" }, props.children);
82
+ }
83
+ (0, react_1.render)(react_2.default.createElement(Wrapper, null,
84
+ react_2.default.createElement(Consumer, null)));
85
+ expect(react_1.screen.getByText("provided").textContent).toBe("provided");
86
+ });
87
+ it("returns the default value when no Provider is mounted", () => {
88
+ const ctx = (0, index_1.createGlobalContext)("test/no-provider", "fallback");
89
+ const { result } = (0, react_1.renderHook)(() => (0, react_2.useContext)(ctx));
90
+ expect(result.current).toBe("fallback");
91
+ });
92
+ it("persists context identity on globalThis across registry lookups", () => {
93
+ const first = (0, index_1.createGlobalContext)("test/persist", "x");
94
+ const g = globalThis;
95
+ const perRuntime = g[REGISTRY_KEY].get(react_2.createContext);
96
+ const stored = perRuntime === null || perRuntime === void 0 ? void 0 : perRuntime.get("test/persist");
97
+ expect(stored).toBe(first);
98
+ });
99
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/core-react",
3
- "version": "11.114.0",
3
+ "version": "11.120.0",
4
4
  "description": "UXF Core",
5
5
  "author": "UX Fans s.r.o",
6
6
  "license": "MIT",
@@ -4,8 +4,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.TranslationsProvider = void 0;
5
5
  exports.useUxfTranslation = useUxfTranslation;
6
6
  const react_1 = require("react");
7
+ const global_context_1 = require("../global-context");
7
8
  /* intellisense and linting is broken with any other more specific type */
8
- const translationsContext = (0, react_1.createContext)((key) => key); // eslint-disable-line @typescript-eslint/no-explicit-any
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ const translationsContext = (0, global_context_1.createGlobalContext)("core-react/translations", (key) => key);
9
11
  exports.TranslationsProvider = translationsContext.Provider;
10
12
  function useUxfTranslation() {
11
13
  return (0, react_1.useContext)(translationsContext);