Part of #1666 (CUSTOM custom views — phase 4).
Description
A React custom container (custom = 'Component') cannot currently tell which cells are busy or whether the form is doing async work. This state must be reactive — a button that lights up while its action runs, a cell that shows a spinner while saving — so it has to flow through the pushed data snapshot. A render-time controller.isPending(...) getter is the wrong shape: a plain method read creates no subscription, so nothing re-renders when the flag flips.
Deliver async state as part of the projected data:
- per-cell / per-row loading (primary): cell values stay scalar, loading is stored as sibling metadata keyed by
(row, property) (e.g. data.<group>.loading[row][prop]), so a custom view can spin/dim exactly the affected cells;
- form-level / action pending: a slice the consumer reads directly (e.g.
data.pending.form, data.pending.actions[actionId]), so a button reads its own pending and updates without involving the grid.
Incremental re-rendering relies on structural sharing + disciplined slicing: unchanged groups/rows/loading slices keep object identity, and consumers slice early (const g = data.line; const p = data.pending.actions[id]) so React Compiler / React.memo can skip children that don't depend on the changed slice. This is memoized render-skipping, not field-level subscription — a component that reads a high-churn slice (e.g. form-level pending) is expected to re-render; the point is to keep the grid from reading it.
Provide useFormPending(selector) / useFormLoading(selector) as an opt-in fallback for non-compiled or third-party custom views and for very high-churn narrow subscriptions.
(The classic CUSTOM controller already surfaces some of this; part of this task is bringing React to parity.)
DESIGN myForm { dataBox { custom = 'MyGrid'; } }
// shapes illustrative — exact keys decided in implementation
function MyGrid({ data }) {
const lines = data.line; // slice early
return lines.list.map(r =>
<input key={r.value} value={r.qty ?? ""}
className={lines.loading?.[r.value]?.qty ? "saving" : ""} />);
}
function PostButton({ data, controller }) {
return <button disabled={data.pending?.actions?.post} // reactive: re-renders when this slice flips
onClick={() => controller.changeProperty({ o: { post: true } })}>Post</button>;
}
Reason
Async state must update the UI when it flips (a busy button, a saving cell); a non-reactive controller getter can't do that. Putting per-cell loading and form/action pending in the pushed data makes them reactive, while structural sharing + early slicing + React Compiler / memo keep re-renders incremental — with selector hooks as a fallback where the compiler can't be assumed.
Part of #1666 (CUSTOM custom views — phase 4).
Description
A React custom container (
custom = 'Component') cannot currently tell which cells are busy or whether the form is doing async work. This state must be reactive — a button that lights up while its action runs, a cell that shows a spinner while saving — so it has to flow through the pusheddatasnapshot. A render-timecontroller.isPending(...)getter is the wrong shape: a plain method read creates no subscription, so nothing re-renders when the flag flips.Deliver async state as part of the projected
data:(row, property)(e.g.data.<group>.loading[row][prop]), so a custom view can spin/dim exactly the affected cells;data.pending.form,data.pending.actions[actionId]), so a button reads its own pending and updates without involving the grid.Incremental re-rendering relies on structural sharing + disciplined slicing: unchanged groups/rows/loading slices keep object identity, and consumers slice early (
const g = data.line; const p = data.pending.actions[id]) so React Compiler /React.memocan skip children that don't depend on the changed slice. This is memoized render-skipping, not field-level subscription — a component that reads a high-churn slice (e.g. form-level pending) is expected to re-render; the point is to keep the grid from reading it.Provide
useFormPending(selector)/useFormLoading(selector)as an opt-in fallback for non-compiled or third-party custom views and for very high-churn narrow subscriptions.(The classic
CUSTOMcontroller already surfaces some of this; part of this task is bringing React to parity.)Reason
Async state must update the UI when it flips (a busy button, a saving cell); a non-reactive controller getter can't do that. Putting per-cell loading and form/action pending in the pushed
datamakes them reactive, while structural sharing + early slicing + React Compiler / memo keep re-renders incremental — with selector hooks as a fallback where the compiler can't be assumed.