Search code examples
reactjsformsremixremix.run

Can remix.run handle complex changing forms without UI state?


I have a dynamic table with many rows. Each rows position can be changed within the table by first clicking the row you want to move and then clicking the row where you want to move it to. Only the updated order of entities is needed for the backend/database update. This doesn't allow adding new entities or updating their info (no CRUD).

Each row also has two action "buttons" (or radio buttons) that just set a boolean value for that specific column. The selected one should of course follow the row when the rows are being re-ordered.

Name Is ready Is in production
Foo [ ] [x]
Bar [x] [ ]

The form should not be valid before both of the radio button columns have a set value.

What I now have is a useState for the order of entities in the table. That is updated solely in the frontend and then looping the updated order of entities and adding a hidden input field with the entity's primary key so that the action can read the updated order after a submit.

I can get the radio buttons work by following the same way of storing the state in UI but that feels like I'm just skipping the Remix altogether. I can't come up with a nice way to communicate each of the clicks (either reordering the rows or clicking radio buttons) to the action handler so that the radio buttons would stay within the row they were activated in.

I guess the question is, can this be done in the way of remix, and if, how?


Solution

  • Here's an example on how to use fetcher.Form to submit actions. I created a component <FetcherCell> that will automatically submit whenever the child input changes.

    Each submit includes an intent value that the action function uses to determine what to do. isReady and isInProduction updates the value for that id.

    Whereas the moveup and movedown intents are used to reorder the data items. And reset uses the Remix Form to reset the data.

    As you can see, there is not a single useState in sight.

    https://stackblitz.com/edit/remix-run-remix-acm3aq?file=app%2Froutes%2F_index.tsx

    export async function action({ request }: DataFunctionArgs) {
      let formData = await request.formData();
      let { id, intent, ...data } = Object.fromEntries(formData.entries());
      switch (intent) {
        case 'isReady':
        case 'isInProduction': {
          let value = data[intent] === 'true';
          await updateData(Number(id), { [intent]: value });
          break;
        }
        case 'moveup':
        case 'movedown': {
          await updateOrder(JSON.parse(String(data.orders)));
          break;
        }
        case 'reset': {
          await resetData();
          break;
        }
        default:
          throw new Response('Bad Request', { status: 400 });
      }
      return json({ success: true });
    }
    
            {data.map((item) => (
              <tr key={item.id}>
                <td>{item.id}</td>
                <td>{item.name}</td>
                <td>
                  <FetcherCell id={item.id} intent="isReady">
                    <input
                      name="isReady"
                      type="checkbox"
                      value="true"
                      defaultChecked={item.isReady}
                    />
                  </FetcherCell>
                </td>
                <td>
                  <FetcherCell id={item.id} intent="isInProduction">
                    <input
                      name="isInProduction"
                      type="checkbox"
                      value="true"
                      defaultChecked={item.isInProduction}
                    />
                  </FetcherCell>
                </td>
                <td style={{ display: 'flex', gap: '1rem' }}>
                  <FetcherCell id={item.id} intent="moveup">
                    <button
                      name="orders"
                      value={getOrderUp(data, item.id)}
                      disabled={item.order < 2}
                    >
                      Up
                    </button>
                  </FetcherCell>
                  <FetcherCell id={item.id} intent="movedown">
                    <button
                      name="orders"
                      value={getOrderDown(data, item.id)}
                      disabled={item.order >= data.length}
                    >
                      Down
                    </button>
                  </FetcherCell>
                </td>
                <td>{item.order}</td>
              </tr>
            ))}
    
    function FetcherCell({ id, intent, children }: FetcherCellProps) {
      const fetcher = useFetcher();
      const handler = useCallback(
        (e: FormEvent) => fetcher.submit(e.currentTarget as HTMLFormElement),
        [fetcher]
      );
      return (
        <fetcher.Form method="post" onChange={handler}>
          <input type="hidden" name="intent" value={intent} />
          <input type="hidden" name="id" value={id} />
          {children}
        </fetcher.Form>
      );
    }