Skip to content

CUSTOM REACT: expose per-cell loading and form/action pending state (reactively, via data) #1669

@AlexKirkouski

Description

@AlexKirkouski

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions