@mohasinac/appkit 4.63.2 → 4.63.3

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "stampVersion": 1,
3
- "version": "4.63.2",
4
- "head": "dd37d5175bd7a2f614092da8fe0cc71892f942a1",
3
+ "version": "4.63.3",
4
+ "head": "aea6affd6821cc7b43256c25614e42422df7177c",
5
5
  "branch": "main",
6
6
  "dirty": false,
7
- "builtAt": "2026-10-23T08:39:40.479Z"
7
+ "builtAt": "2026-10-23T10:12:11.248Z"
8
8
  }
@@ -209,6 +209,13 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
209
209
  // validator on every render and re-fire the effect below in a loop.
210
210
  const onValidationChangeRef = useRef(onValidationChange);
211
211
  onValidationChangeRef.current = onValidationChange;
212
+ /**
213
+ * The last error map actually HANDED TO the caller.
214
+ *
215
+ * 🛑 This is what stops an infinite render loop, and the loop froze the whole
216
+ * app — see `emitValidation` below.
217
+ */
218
+ const lastEmittedRef = useRef(null);
212
219
  /**
213
220
  * Are two field-error maps the same set of messages?
214
221
  *
@@ -232,6 +239,44 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
232
239
  .filter((s) => s.fields?.some((f) => keys.some((k) => k === f || k.startsWith(`${f}.`))))
233
240
  .map((s) => s.id);
234
241
  }, [ordered]);
242
+ /**
243
+ * Hand the result to the caller, but ONLY when it actually changed.
244
+ *
245
+ * ## 🛑 The loop this closes froze every admin editor drawer
246
+ *
247
+ * `runValidation` runs on EVERY render by design: `values` is a fresh object
248
+ * each time, so `ordered` → `erroringSectionIds` → `runValidation` are all
249
+ * new identities and the effect below re-fires. The comment there argues
250
+ * that is safe, and for this component's OWN state it is — `sameErrors` and
251
+ * the `forceOpenIds` length check both return the previous object, so React
252
+ * bails out.
253
+ *
254
+ * It was NOT safe for the caller. `onValidationChange` was invoked
255
+ * unconditionally on every run, and the real call site is
256
+ * `onValidationChange={() => form.validate(draft)}` — which setStates. So:
257
+ * render → effect → emit → caller setState → render → … forever. React
258
+ * reported "Maximum update depth exceeded" ~4x/second.
259
+ *
260
+ * The visible symptom was not a validation bug at all. A React tree stuck in
261
+ * an update loop never commits a transition, and App Router navigation IS a
262
+ * transition — so with the drawer open, Cancel, the X, the sort dropdown and
263
+ * even plain nav links all did nothing. Measured on production: `router.replace`
264
+ * produced ZERO `history.replaceState` calls, no error, no failed request.
265
+ * The only escape was a full page reload.
266
+ *
267
+ * Emitting only on a real change is also what the prop NAME promises; firing
268
+ * it with an identical map on every render was never meaningful to a caller.
269
+ */
270
+ const emitValidation = useCallback((next, index) => {
271
+ const prev = lastEmittedRef.current;
272
+ if (prev && sameErrors(prev, next))
273
+ return;
274
+ lastEmittedRef.current = next;
275
+ onValidationChangeRef.current?.(next, index);
276
+ },
277
+ // `sameErrors` is a pure local helper; `lastEmittedRef`/`onValidationChangeRef`
278
+ // are refs. Nothing here needs to re-create this callback.
279
+ []);
235
280
  const runValidation = useCallback((nextValues) => {
236
281
  const activeSchema = schema ?? inheritedSchema;
237
282
  if (!activeSchema)
@@ -243,7 +288,7 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
243
288
  if (parsed.success) {
244
289
  setFieldErrors((prev) => (Object.keys(prev).length === 0 ? prev : {}));
245
290
  setForceOpenIds((prev) => (prev.length === 0 ? prev : []));
246
- onValidationChangeRef.current?.({}, fieldToSectionIndex);
291
+ emitValidation({}, fieldToSectionIndex);
247
292
  return;
248
293
  }
249
294
  const next = {};
@@ -266,7 +311,7 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
266
311
  const kept = prev.filter((id) => stillBad.has(id));
267
312
  return kept.length === prev.length ? prev : kept;
268
313
  });
269
- onValidationChangeRef.current?.(next, fieldToSectionIndex);
314
+ emitValidation(next, fieldToSectionIndex);
270
315
  }, [schema, inheritedSchema, fieldToSectionIndex, erroringSectionIds]);
271
316
  /*
272
317
  * 🛑 `values` MUST be a dependency, and leaving it out cost the entire admin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.63.2",
3
+ "version": "4.63.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"