nukejs 0.0.26 → 0.0.28

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
@@ -25,6 +25,7 @@ npm create nuke@latest
25
25
  - [Configuration](#configuration)
26
26
  - [Link Component & Navigation](#link-component--navigation)
27
27
  - [useRequest() — URL Params, Query & Headers](#userequest--url-params-query--headers)
28
+ - [cache() — Request-Scoped Data Caching](#cache--request-scoped-data-caching)
28
29
  - [Error Pages](#error-pages)
29
30
  - [Building & Deploying](#building--deploying)
30
31
 
@@ -1104,6 +1105,73 @@ Changing `?lang=fr` in the URL re-renders client components automatically.
1104
1105
 
1105
1106
  ---
1106
1107
 
1108
+ ## cache() — Request-Scoped Data Caching
1109
+
1110
+ Server components render independently, so if two components in the same page tree both need the same data, NukeJS has no way to know they're asking for the same thing — each one triggers its own database call, fetch, or computation, even though they're part of the same request:
1111
+
1112
+ ```tsx
1113
+ // app/components/Nav.tsx
1114
+ export default async function Nav() {
1115
+ const user = await db.getUser(useRequest().params.id); // DB call #1
1116
+ return <nav>{user.name}</nav>;
1117
+ }
1118
+
1119
+ // app/pages/dashboard.tsx
1120
+ export default async function Dashboard() {
1121
+ const user = await db.getUser(useRequest().params.id); // DB call #2 — same data
1122
+ return <main>{user.email}</main>;
1123
+ }
1124
+ ```
1125
+
1126
+ `cache()` wraps an async function so repeated calls with the same arguments, **within the same request**, share a single call:
1127
+
1128
+ ```ts
1129
+ // lib/data.ts
1130
+ import { cache } from 'nukejs';
1131
+
1132
+ export const getUser = cache(async (id: string) => {
1133
+ return db.getUser(id); // only runs once per request, however many
1134
+ // components call getUser(id) with that id
1135
+ });
1136
+ ```
1137
+
1138
+ ```tsx
1139
+ // app/components/Nav.tsx
1140
+ import { getUser } from '../../lib/data';
1141
+
1142
+ export default async function Nav() {
1143
+ const user = await getUser(useRequest().params.id as string);
1144
+ return <nav>{user.name}</nav>;
1145
+ }
1146
+
1147
+ // app/pages/dashboard.tsx
1148
+ import { getUser } from '../../lib/data';
1149
+
1150
+ export default async function Dashboard() {
1151
+ const user = await getUser(useRequest().params.id as string); // same call → no extra DB hit
1152
+ return <main>{user.email}</main>;
1153
+ }
1154
+ ```
1155
+
1156
+ Whichever component calls `getUser('42')` first triggers the real call; every other call with `'42'` in that same request — even ones that fire concurrently, before the first has resolved — awaits that same in-flight promise instead of starting a new one.
1157
+
1158
+ ### Why not just use React's `cache()`?
1159
+
1160
+ React ships its own `cache()`, but it relies on the React Server Components renderer to signal "this is a new request, reset the memoization table." NukeJS doesn't run the RSC protocol — it renders each request to a single HTML string (see [Pages & Routing](#pages--routing)) — so nothing would ever tell React's `cache()` to reset, risking one request's cached data leaking into another's response. NukeJS's `cache()` has the same signature and mental model as React's, but is tied to NukeJS's own per-request lifecycle instead, so it resets correctly on every request.
1161
+
1162
+ ### Scope & behaviour
1163
+
1164
+ - **One request, one cache.** The cache is created fresh before rendering starts and discarded once the response is sent. There's no cross-request caching and nothing to invalidate — the next request simply starts with an empty cache.
1165
+ - **Keyed by function + arguments.** Two different `cache()`-wrapped functions never share entries, even if called with identical arguments. Arguments are compared by value (via JSON serialization), so `getUser('42')` and `getUser('42')` hit the same entry, but `getUser('42')` and `getUser('43')` don't.
1166
+ - **Concurrent calls are deduped too.** If two components call the same cached function before the first call has settled, both await the same pending promise — only one underlying call is made.
1167
+ - **Failed calls aren't stuck.** If a cached call rejects, that entry is removed immediately, so a later call with the same arguments in the same request retries instead of replaying the same error.
1168
+ - **Non-serializable arguments skip caching.** Arguments that can't be safely compared by value (functions, Symbols, cyclic objects) just call straight through, unmemoized, rather than being cached under an incorrect key.
1169
+ - **Safe to call anywhere.** Outside of an active request — a script, a test, client-side code — a `cache()`-wrapped function simply calls straight through with no memoization. It's always safe to import and call.
1170
+
1171
+ `cache()` is a pure optimization: removing it (or calling the wrapped function directly) never changes what your app returns, only how many times the underlying work runs.
1172
+
1173
+ ---
1174
+
1107
1175
  ## Error Pages
1108
1176
 
1109
1177
  NukeJS supports custom error pages for **404 Not Found** and **500 Internal Server Error**. Place them directly in `app/pages/` — they are standard server components and support everything regular pages do: layouts, `useHtml()`, client components, and HMR in dev.
@@ -387,6 +387,46 @@ async function runWithRequestStore<T>(ctx: any, fn: () => Promise<T>): Promise<T
387
387
  try { return await fn(); } finally { __setReq(null); }
388
388
  }
389
389
 
390
+ // \u2500\u2500\u2500 cache-store (inlined) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
391
+ // See src/cache-store.ts for the full explanation. Request-scoped memoisation
392
+ // for async data loaders, keyed on the wrapped function + its (serialisable)
393
+ // arguments. Reset per request exactly like the request/html stores above.
394
+ const __CACHE_KEY__ = Symbol.for('__nukejs_cache_store__');
395
+ const __getCache = (): { entries: Map<string, Promise<unknown>> } | null =>
396
+ (globalThis as any)[__CACHE_KEY__] ?? null;
397
+ const __setCache = (v: { entries: Map<string, Promise<unknown>> } | null): void => {
398
+ (globalThis as any)[__CACHE_KEY__] = v;
399
+ };
400
+ async function runWithCacheStore<T>(fn: () => Promise<T>): Promise<T> {
401
+ __setCache({ entries: new Map() });
402
+ try { return await fn(); } finally { __setCache(null); }
403
+ }
404
+ let __cacheIdCounter = 0;
405
+ function __serialiseArgs(args: unknown[]): string | null {
406
+ try {
407
+ return JSON.stringify(args, (_k, v) => {
408
+ if (typeof v === 'function' || typeof v === 'symbol') throw new Error('non-serialisable');
409
+ return v;
410
+ });
411
+ } catch { return null; }
412
+ }
413
+ function cache<Args extends unknown[], R>(fn: (...args: Args) => Promise<R>): (...args: Args) => Promise<R> {
414
+ const id = \`c\${++__cacheIdCounter}\`;
415
+ return function cached(...args: Args): Promise<R> {
416
+ const store = __getCache();
417
+ if (!store) return fn(...args);
418
+ const argsKey = __serialiseArgs(args);
419
+ if (argsKey === null) return fn(...args);
420
+ const key = \`\${id}:\${argsKey}\`;
421
+ const existing = store.entries.get(key);
422
+ if (existing) return existing as Promise<R>;
423
+ const result = fn(...args);
424
+ store.entries.set(key, result);
425
+ result.catch(() => { store.entries.delete(key); });
426
+ return result;
427
+ };
428
+ }
429
+
390
430
  // \u2500\u2500\u2500 HTML helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
391
431
  function escapeHtml(s: string): string {
392
432
  return String(s)
@@ -495,27 +535,83 @@ function buildWrapperAttrString(attrs: Record<string, any>): string {
495
535
  return parts.length ? ' ' + parts.join(' ') : '';
496
536
  }
497
537
 
498
- function serializeProps(value: any): any {
499
- if (typeof value === 'function') return undefined; // must come before the object check
500
- if (value == null || typeof value !== 'object') return value;
501
- if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);
502
- if ((value as any).$$typeof) {
503
- const { type, props: p } = value as any;
504
- if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };
505
- if (typeof type === 'function') {
506
- const cid = (type as any).__nukeClientId ?? CLIENT_COMPONENTS[type.name];
507
- if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };
508
- }
509
- return undefined;
510
- }
511
- const out: any = {};
512
- for (const [k, v] of Object.entries(value as Record<string, any>)) {
513
- const s = serializeProps(v);
514
- if (s !== undefined) out[k] = s;
538
+ function prepareProps(props, hydrated) {
539
+ if (!props || typeof props !== 'object') return Promise.resolve({ real: props, json: props });
540
+ const entries = Object.entries(props);
541
+ return Promise.all(entries.map(([, v]) => prepareValue(v, hydrated))).then((results) => {
542
+ const real = {};
543
+ const json = {};
544
+ entries.forEach(([key], i) => {
545
+ real[key] = results[i].real;
546
+ if (results[i].json !== undefined) json[key] = results[i].json;
547
+ });
548
+ return { real, json };
549
+ });
550
+ }
551
+
552
+ async function prepareValue(value, hydrated) {
553
+ if (value === null || value === undefined) return { real: value, json: value };
554
+ if (typeof value === 'function') return { real: value, json: undefined };
555
+ if (typeof value !== 'object') return { real: value, json: value };
556
+
557
+ if (Array.isArray(value)) {
558
+ const items = await Promise.all(value.map((v) => prepareValue(v, hydrated)));
559
+ return {
560
+ real: items.map((i) => i.real),
561
+ json: items.map((i) => i.json).filter((i) => i !== undefined),
562
+ };
515
563
  }
564
+
565
+ if (value.$$typeof) return prepareElement(value, hydrated);
566
+
567
+ const out = await prepareProps(value, hydrated);
516
568
  return out;
517
569
  }
518
570
 
571
+ // Resolves a single React element found inside a client component's props.
572
+ // Native elements and fragments recurse into their own props. Client
573
+ // components are left untouched for 'real' (react-dom/server renders them
574
+ // normally within the boundary's own renderToString call below) and wired
575
+ // through as { __re: 'client', componentId, props } for the browser to
576
+ // mount for real. Server components can't run in the browser at all, so
577
+ // they're rendered once with this same file's renderNode() (handles async
578
+ // components and any nested client boundaries inside them) and wrapped in
579
+ // an inert <span style="display:contents"> carrying that HTML \u2014
580
+ // identically on both 'real' and 'json', so the SSR markup and the
581
+ // browser's reconstructed tree have the same shape and hydrateRoot() can
582
+ // reconcile them without a mismatch.
583
+ async function prepareElement(element, hydrated) {
584
+ const { type, props } = element;
585
+
586
+ if (type === Symbol.for('react.fragment')) {
587
+ const p = await prepareProps(props, hydrated);
588
+ return { real: __createElement__(Symbol.for('react.fragment'), p.real), json: { __re: 'fragment', props: p.json } };
589
+ }
590
+
591
+ if (typeof type === 'string') {
592
+ const p = await prepareProps(props, hydrated);
593
+ return { real: __createElement__(type, p.real), json: { __re: 'html', tag: type, props: p.json } };
594
+ }
595
+
596
+ if (typeof type === 'function') {
597
+ const cid = type.__nukeClientId ?? CLIENT_COMPONENTS[type.name];
598
+ if (cid) {
599
+ const p = await prepareProps(props, hydrated);
600
+ return { real: element, json: { __re: 'client', componentId: cid, props: p.json } };
601
+ }
602
+
603
+ const html = await renderNode(element, hydrated);
604
+ const wrapperProps = {
605
+ style: { display: 'contents' },
606
+ 'data-n-static': true,
607
+ dangerouslySetInnerHTML: { __html: html },
608
+ };
609
+ return { real: __createElement__('span', wrapperProps), json: { __re: 'static', html } };
610
+ }
611
+
612
+ return { real: element, json: undefined };
613
+ }
614
+
519
615
  async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
520
616
  if (node == null || typeof node === 'boolean') return '';
521
617
  if (typeof node === 'string') return escapeHtml(node);
@@ -557,10 +653,10 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
557
653
  hydrated.add(clientId);
558
654
  const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
559
655
  const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
560
- const serializedProps = serializeProps(componentProps ?? {});
656
+ const { real: hydrationSafeProps, json: serializedProps } = await prepareProps(componentProps ?? {}, hydrated);
561
657
  let ssrHtml: string;
562
658
  try {
563
- ssrHtml = __renderToString__(__createElement__(type as any, componentProps || {}));
659
+ ssrHtml = __renderToString__(__createElement__(type as any, hydrationSafeProps || {}));
564
660
  } catch {
565
661
  ssrHtml = PRERENDERED_HTML[clientId] ?? '';
566
662
  }
@@ -632,7 +728,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
632
728
  let appHtml = '';
633
729
  const store = await runWithRequestStore(
634
730
  { url, pathname, params, query, headers: normHeaders },
635
- () => runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); }),
731
+ () => runWithCacheStore(() => runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); })),
636
732
  );
637
733
 
638
734
  const pageTitle = resolveTitle(store.titleOps, 'NukeJS');
package/dist/bundle.js CHANGED
@@ -50,6 +50,20 @@ async function reconstructElement(node, mods) {
50
50
  const React = await import("react");
51
51
  return React.default.createElement(n.tag, await reconstructProps(n.props, mods));
52
52
  }
53
+ if (node.__re === "fragment") {
54
+ const n = node;
55
+ const React = await import("react");
56
+ return React.default.createElement(React.default.Fragment, await reconstructProps(n.props, mods));
57
+ }
58
+ if (node.__re === "static") {
59
+ const n = node;
60
+ const React = await import("react");
61
+ return React.default.createElement("span", {
62
+ style: { display: "contents" },
63
+ "data-n-static": true,
64
+ dangerouslySetInnerHTML: { __html: n.html }
65
+ });
66
+ }
53
67
  return node;
54
68
  }
55
69
  async function reconstructProps(props, mods) {
@@ -0,0 +1,48 @@
1
+ const KEY = /* @__PURE__ */ Symbol.for("__nukejs_cache_store__");
2
+ const getGlobal = () => globalThis[KEY] ?? null;
3
+ const setGlobal = (store) => {
4
+ globalThis[KEY] = store;
5
+ };
6
+ async function runWithCacheStore(fn) {
7
+ setGlobal({ entries: /* @__PURE__ */ new Map() });
8
+ try {
9
+ return await fn();
10
+ } finally {
11
+ setGlobal(null);
12
+ }
13
+ }
14
+ let cacheIdCounter = 0;
15
+ function serialiseArgs(args) {
16
+ try {
17
+ return JSON.stringify(args, (_key, value) => {
18
+ if (typeof value === "function" || typeof value === "symbol") {
19
+ throw new Error("non-serialisable value");
20
+ }
21
+ return value;
22
+ });
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ function cache(fn) {
28
+ const cacheId = `c${++cacheIdCounter}`;
29
+ return function cached(...args) {
30
+ const store = getGlobal();
31
+ if (!store) return fn(...args);
32
+ const argsKey = serialiseArgs(args);
33
+ if (argsKey === null) return fn(...args);
34
+ const key = `${cacheId}:${argsKey}`;
35
+ const existing = store.entries.get(key);
36
+ if (existing) return existing;
37
+ const result = fn(...args);
38
+ store.entries.set(key, result);
39
+ result.catch(() => {
40
+ store.entries.delete(key);
41
+ });
42
+ return result;
43
+ };
44
+ }
45
+ export {
46
+ cache,
47
+ runWithCacheStore
48
+ };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export { default as useRouter } from './use-router';
7
7
  export { useRequest } from './use-request';
8
8
  export type { RequestContext } from './use-request';
9
9
  export { normaliseHeaders, sanitiseHeaders, getRequestStore } from './request-store';
10
+ export { cache } from './cache-store';
10
11
  export { default as Link } from './Link';
11
12
  export { setupLocationChangeMonitor, initRuntime } from './bundle';
12
13
  export type { RuntimeData } from './bundle';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { useHtml } from "./use-html.js";
3
3
  import { default as default2 } from "./use-router.js";
4
4
  import { useRequest } from "./use-request.js";
5
5
  import { normaliseHeaders, sanitiseHeaders, getRequestStore } from "./request-store.js";
6
+ import { cache } from "./cache-store.js";
6
7
  import { default as default3 } from "./Link.js";
7
8
  import { setupLocationChangeMonitor, initRuntime } from "./bundle.js";
8
9
  import { escapeHtml } from "./utils.js";
@@ -11,6 +12,7 @@ export {
11
12
  default3 as Link,
12
13
  ansi,
13
14
  c,
15
+ cache,
14
16
  createPersistedStore,
15
17
  createStore,
16
18
  escapeHtml,
package/dist/renderer.js CHANGED
@@ -90,9 +90,9 @@ async function renderFunctionComponent(type, props, ctx) {
90
90
  ctx.hydrated.add(id);
91
91
  const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
92
92
  const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
93
- const serializedProps = serializePropsForHydration(componentProps, ctx.registry);
93
+ const { real: hydrationSafeProps, json: serializedProps } = await prepareProps(componentProps, ctx);
94
94
  log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);
95
- const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, componentProps));
95
+ const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, hydrationSafeProps));
96
96
  return `<span data-hydrate-id="${id}"${wrapperAttrStr} data-hydrate-props="${escapeHtml(
97
97
  JSON.stringify(serializedProps)
98
98
  )}">${html}</span>`;
@@ -105,51 +105,72 @@ async function renderFunctionComponent(type, props, ctx) {
105
105
  const resolved = result?.then ? await result : result;
106
106
  return renderElementToHtml(resolved, ctx);
107
107
  }
108
- function serializePropsForHydration(props, registry) {
109
- if (!props || typeof props !== "object") return props;
110
- const out = {};
108
+ async function prepareProps(props, ctx) {
109
+ if (!props || typeof props !== "object") return { real: props, json: props };
110
+ const real = {};
111
+ const json = {};
111
112
  for (const [key, value] of Object.entries(props)) {
112
- const s = serializeValue(value, registry);
113
- if (s !== void 0) out[key] = s;
113
+ const p = await prepareValue(value, ctx);
114
+ real[key] = p.real;
115
+ if (p.json !== void 0) json[key] = p.json;
114
116
  }
115
- return out;
117
+ return { real, json };
116
118
  }
117
- function serializeValue(value, registry) {
118
- if (value === null || value === void 0) return value;
119
- if (typeof value === "function") return void 0;
120
- if (typeof value !== "object") return value;
121
- if (Array.isArray(value))
122
- return value.map((v) => serializeValue(v, registry)).filter((v) => v !== void 0);
123
- if (value.$$typeof)
124
- return serializeReactElement(value, registry);
125
- const out = {};
119
+ async function prepareValue(value, ctx) {
120
+ if (value === null || value === void 0) return { real: value, json: value };
121
+ if (typeof value === "function") return { real: value, json: void 0 };
122
+ if (typeof value !== "object") return { real: value, json: value };
123
+ if (Array.isArray(value)) {
124
+ const items = await Promise.all(value.map((v) => prepareValue(v, ctx)));
125
+ return {
126
+ real: items.map((i) => i.real),
127
+ json: items.map((i) => i.json).filter((i) => i !== void 0)
128
+ };
129
+ }
130
+ if (value.$$typeof) return prepareElement(value, ctx);
131
+ const real = {};
132
+ const json = {};
126
133
  for (const [k, v] of Object.entries(value)) {
127
- const s = serializeValue(v, registry);
128
- if (s !== void 0) out[k] = s;
134
+ const p = await prepareValue(v, ctx);
135
+ real[k] = p.real;
136
+ if (p.json !== void 0) json[k] = p.json;
129
137
  }
130
- return out;
138
+ return { real, json };
131
139
  }
132
- function serializeReactElement(element, registry) {
140
+ async function prepareElement(element, ctx) {
133
141
  const { type, props } = element;
142
+ if (type === Fragment) {
143
+ const p = await prepareProps(props, ctx);
144
+ return { real: createElement(Fragment, p.real), json: { __re: "fragment", props: p.json } };
145
+ }
134
146
  if (typeof type === "string") {
135
- return { __re: "html", tag: type, props: serializePropsForHydration(props, registry) };
147
+ const p = await prepareProps(props, ctx);
148
+ return { real: createElement(type, p.real), json: { __re: "html", tag: type, props: p.json } };
136
149
  }
137
150
  if (typeof type === "function") {
138
151
  const componentCache = getComponentCache();
139
- for (const [id, filePath] of registry.entries()) {
152
+ for (const [id, filePath] of ctx.registry.entries()) {
140
153
  const info = componentCache.get(filePath);
141
154
  if (!info?.isClientComponent) continue;
142
155
  if (type.__nukeClientId === id || info.exportedName && type.name === info.exportedName) {
143
156
  type.__nukeClientId = id;
157
+ const p = await prepareProps(props, ctx);
144
158
  return {
145
- __re: "client",
146
- componentId: id,
147
- props: serializePropsForHydration(props, registry)
159
+ real: element,
160
+ // untouched — React renders it directly within the outer renderToString call
161
+ json: { __re: "client", componentId: id, props: p.json }
148
162
  };
149
163
  }
150
164
  }
165
+ const html = await renderElementToHtml(element, ctx);
166
+ const wrapperProps = {
167
+ style: { display: "contents" },
168
+ "data-n-static": true,
169
+ dangerouslySetInnerHTML: { __html: html }
170
+ };
171
+ return { real: createElement("span", wrapperProps), json: { __re: "static", html } };
151
172
  }
152
- return void 0;
173
+ return { real: element, json: void 0 };
153
174
  }
154
175
  export {
155
176
  renderElementToHtml
package/dist/ssr.js CHANGED
@@ -8,6 +8,7 @@ import { matchRoute, findLayoutsForRoute } from "./router.js";
8
8
  import { findClientComponentsInTree } from "./component-analyzer.js";
9
9
  import { renderElementToHtml } from "./renderer.js";
10
10
  import { runWithRequestStore, normaliseHeaders, sanitiseHeaders } from "./request-store.js";
11
+ import { runWithCacheStore } from "./cache-store.js";
11
12
  import {
12
13
  runWithHtmlStore,
13
14
  resolveTitle
@@ -130,9 +131,9 @@ async function renderFile(filePath, params, url, pagesDir, isDev, res, req, stat
130
131
  query: queryParams,
131
132
  headers: normHeaders
132
133
  },
133
- () => runWithHtmlStore(async () => {
134
+ () => runWithCacheStore(() => runWithHtmlStore(async () => {
134
135
  appHtml = await renderElementToHtml(wrappedElement, ctx);
135
- })
136
+ }))
136
137
  );
137
138
  const pageTitle = resolveTitle(store.titleOps, "NukeJS");
138
139
  const headLines = [
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  const SSR_ROUTER = { push: () => {
3
3
  }, replace: () => {
4
4
  }, back: () => {
@@ -28,7 +28,10 @@ function useRouter() {
28
28
  const refresh = useCallback(() => {
29
29
  window.dispatchEvent(new Event("locationchange"));
30
30
  }, []);
31
- return { path, push, replace, back, refresh };
31
+ return useMemo(
32
+ () => ({ path, push, replace, back, refresh }),
33
+ [path, push, replace, back, refresh]
34
+ );
32
35
  }
33
36
  export {
34
37
  useRouter as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nukejs",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",