I am creating React Web application with Yii2 API and Redux-Saga which calls Yii2 API (asynchronously).

I am stuck with one especially large and complex user action (initiated by the selection of good, entry of amount and pressing Enter in the amount field) which (for the form of invoice and invoice lines) is doing all the following sub-actions in the mentioned sequence:

  1. async call getNextId('INVOICE_LINE') to the Yii2 API to get new Id for the new invoice line;
  2. sync local (w.r.t. browser) process addInvoiceLine that creates new InvoiceLine object and appends it to the invoice.invoiceLines and assigns previously retrieved Id to the invoiceLine.id;
  3. async call 'getGoodById(5555)' to the Yii2 API to get Good object;
  4. sync local (w.r.t. browser) process 'assignGood(invoiceLine, good)', that assigns goods to the invoiceLine object and also that updates invoiceLine.invoice parent object (e.g. about the number of lines and some other summary);
  5. sync local (w.r.t. browser) process updateAmount(invoiceLine, amount), that assigns user-entered amount to the invoiceLine object and also that updates invoiceLine.invoice parent object (e.g. about the total amount of goods and total sum for payment);
  6. proper Redux store action updateInvoice(invoice, invoiceLines) = ... return {...state, invoice: invoice, invoiceLines: invoiceLines), that writes all the prepared data in the Redux store for the final presentation in the GUI (React components).

Of course, one can say that my design/separation of processes into actions are too complex. E.g. simple solutions would be:

  1. Create complex API function that does multiple things at once: it receives the good Id and it returns all the possible data from the server, e.g. new Id for the new InvoiceLine and Good object. This ccomplex API can be called inside Saga action and the saga action can dispatch new action with all the data from the server;
  2. Create complex Reducer function that does multiple things at once (this action is the one which is dispatched by the Saga action): receives Id for the new InvoiceLine, received Good data, this action with the simple JavaScript functions can do all all the sync points (2., 4., 5.) from my initial design and as the last step it can do the 6th point as well and write (immutably, of course, with ... pattern) newly updated invoice and invoiceLines objects into state.

So - my initial design with myriad of actions (and with unknowns how to organize and execute them) vs. the design with just 2 standard actions (Saga action and Reduced action).

But I will loose a lot if I will opt for the second design. I have many business objects in my application (e.g. Invoice+InvoiceLines, InvoiceOrder+InvoiceOrderLines, Purchase+PurchaseLines, Waybill+WaybillLines, Inventory+InventoryLines, etc.) and all those objects in one or more event are retrieving both Id for their lines and Good for their lines. So - I would like to keep getNextId and getGoodById completely unaware whether they are used for Invoice or Purchase. The same goes with assignGood(invoiceLine, good) - I have different use-cases in which the good can be assigned to the invoice line (for new line, for existing line (which already have Id)).

So, I am stuck with 2 design decisions:

  1. Is it OK to make small, focused synchronous and asynchronous actions that are highly reusable? From the one hand - reusability is the must. From the other hand - if fine-grained design results in the multiple calls to the server or in the complex chain of actions, then maybe I should opt for the coarse grained actions with one call to the server and with just 2 actions: 1 Saga action that gets all the data from the server; 1 Reducer action that does the final calculation and writes everything into the state.
  2. The second design decision is relevant only in the case if I opt for the fine-grained design (the first proposal). How I code those synchronous actions? I can not code them as Reducer actions because the last operation of the Reducer action always is writing the data into store. By in my case I am building the update-package step-by-step with each sync action. So - should I create some sync (without side effects) middleware for my sync actions?

The call of suc actions is tricky.

One can imagine that the event handler can be made async function and then the code can be quite simple:

async handleGoodAmountEntered(goodId, amount) {
  id = yield getNextId('INVOICE_LINE');
  invoiceLine = addInvoiceLine(invoice, id);
  good = yield getGoodById(goodId);
  assignGood(invoiceLine, good);
  updateAmount(invoiceLine, amount);
  dispatch('UPDATE_INVOICE', {invoice: invoice, invoiceLines: invoiceLines});

This is very nice, readable and modular code. But, of course, such code is not possible with actions, because actions does not return anything meaningfule, actions just can invoke other actions (passing results to them) and end all the invocation chaing with invokeing the final action - Reducer update function.

That is why in reality my code contains the plan for the action logic and actions themselves fontain the logic can can read plan, and select next action based on the plane and data calculated so far:

/* no async! */ handleGoodAmountEntered(goodId, amount) {
  let plan = {actions: [
    {name: 'ADD_INVOICE_LINE', /* instructions how to handle arguments and results */},
    {name: 'GET_GOOD_BY_ID', /* instructions how to handle arguments and results */},
    {name: 'ASSIGN_GOOD_TO_INVOICE_LINE', /* instructions how to handle arguments and results */},
    {name: 'UPDATE_AMOUNT_TO_INVOICE_LINE', /* instructions how to handle arguments and results */},
    {name: 'UPDATE_INVOICE', /* instructions how to handle arguments and results */},
  dispatch('GET_NEXT_ID', {plan: plan});

The use of plan and the brancing logic that is put at the end of every action guarantess (in such a design) that GET_GOOD_BY_ID action knows that in this case it should call ASSIGN_GOOD_TO_INVOICE_LINE and in different case (with different plan) it should call ASSIGN_GOOD_TO_PURCHASE_LINE or ASSIGN_GOOD_TO_INVOICE_ORDER_LINE or whatever.

So, I am gravitating towards this design with fine-grained/reusable/compositional actions, with chain-of-actions execution pattern and with the use of plan-of-actions-and-instructions. I have the chain of actions and in every payload of every action I am forwarding ever-growing data structure (invoice, invoiceLines) that I am enhancing with every action and that I will write in the Redux store with the final - Reducer - action.

So - is my design correct and should I build sync/local (w.r.t browser) middleward for the handling of sync actions?

My similar questions about the chain actions (somewhat outdated) are https://stackoverflow.com/questions/69551949/react-saga-method-calls-certain-redux-method-but-is-it-possible-to-decouple-th and https://github.com/redux-saga/redux-saga/discussions/2226


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.