#spawned actors and re-rendering, table example

1 messages Β· Page 1 of 1 (latest)

scenic kite
#

Hey all πŸ‘‹ I'm thinking what is the best approach to model table machine(s). Think of a table that is similar to the notion table, ie has the following features.

  • each column has a pre-determined type
  • column type can be changed
  • each cell is editable but the logic depends on the on the column type.
  • both column and rows can be dragged for re-orders
  • filtering, grouping, sorting...

My first approach was to put it all in one machine and that worked fine but then the machine kept getting bigger and more hard to manage.

After i learned about spawned machines I thought this might be a good fit, so I tried to spawn a columnMachine and a cell Machine for each column and cell and connect them to the table machine. However this approach had issues with re-renering the table machines. as it turned out invoked actors changes don't trigger re-renders for parent machine.

This was a problem as now when column type changes the cell don't react to that change, see code below.

The third approach is maybe what I called a switchboard where columns send events to parent, parent send events to cells, but not sure if this the best approach.

I guess my question is what the "xstate" way to build such a table?

#

here is a code sample of the rendering issue where we are passing the type for each table cell from insdie columnDef

  const [state, send] = useMachine(tableMachine, {
    devTools: true,
  });

  const table = useReactTable({
    data: state.context.data,
    columns: state.context.columns.map(convertFawwazColumnToReactTableColumnDef),
    getCoreRowModel: getCoreRowModel(),
  });

//....

  <TableRow key={row.id} data-state={row.getIsSelected() && "selected"} className="min-h-[1.5rem]">
                {row.getVisibleCells().map((cell) => {
                  switch ((cell.column.columnDef.meta as any).type) {
                    case "title":
                      return (
                        <TitleTd
                          key={cell.id}
                          columnsDef={state.context.columns.map((v) => v.getSnapshot()?.context.column!)}
                        />
                      );
                    case "text":
                      return (
                        <TextTd
                          key={cell.id}
                          columnsDef={state.context.columns.map((v) => v.getSnapshot()?.context.column!)}

                        />
                      );
                    case "number":
                      return (
                        <NumberTd
                          columnsDef={state.context.columns.map((v) => v.getSnapshot()?.context.column!)}
                        />
                      );
                    default:
                      return null;
                  }
                })}
              </TableRow>
spice moon
#

Hey, I'll check this in the morning. Thanks for the detailed problem statement!

spice moon
#

Was the main problem the size of the machine?

#

It may seem counter-intuitive but a single machine for handling a single (perhaps big) data structure is fine IMO

scenic kite
#

that make sense but I kept hitting cases like where it made me question if Xstate is the best tool for the job, or if am I not using Xstate correctly. For example I have this modal that open when an Open event is sent from a table th element. but I need to figure out which column is being edited so had to add a columnIdInEditProperties to the machine context. πŸ€”

spice moon
#

I would have a separate state machine (or otherwise, if you need one) for the modal, but the main machine for modifying the table shouldn't really care about the UI

#

So, in pseudocode, that'd look like:

<EditColumnPropertiesModal>
  // ...
  <form onSubmit={updatedVals => {
    tableActor.send({ type: 'EDIT_COLUMN_PROPERTIES', ... })
  }}>
// ...
#

Separating the data model from the UI alone will greatly simplify the machine

scenic kite
#

Thanks again @spice moon for the quick replay, separation of concerns make sense, so if you understanding you correctly you are seeing the table machine need to extract away the UI logic. that can be it's separate machine, which for the sake of this example going to need a machine as we are going to need to trigger the edit flow from multiple places. In this new Machine for the UI for the edit flow, we would still though need to have columnIdInEditProperties in context to be passed to the from to be able to send to the table machine with the EDIT_COLUMN_PROPERTIES event.

I have three questions:

  • is my understanding above correct?
  • is my approach to model the table machine, is optimum utilization of Xstate
  • my understand of xstate is that meant to model complex flows through state charts. but my current implementations have some complexity but not complex flows, would you agree?
spice moon
#

With a more UI-agnostic model, you wouldn't need columnIdInEditProperties. You can pass that in the event:

tableActor.send({
  type: 'EDIT_...',
  columnId: 'some-col-id',
  // ...
})
#

A simple separate state machine for managing various modals can look like:

createMachine({
  context: {
    currentColumnId: null, // or a column id
  },
  initial: 'idle',
  states: {
    idle: {},
    editingColumn: {}, // for the editing column modal
    editingCell: {}, // etc.
  },
  on: {
    EDIT_COLUMN: {
      target: '.editingColumn',
      actions: assign({ currentColumnId: (_, ev) => ev.columnId })
    }
  }
})
#

And I would design the modal to take columnId as a prop

#
function EditColumnPropertiesModal({ columnId }) {
  // ...
  // on form submit
  tableActor.send({
    type: 'EDIT_COLUMN_...',
    columnId,
    // ...
  })
}
#

But I could further refactor the modal to only call an onChange and handle that higher up too