Skip to content

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

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.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    • Status
      Icebox

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions