Best Practices for Synchronous Form Submission

tl;dr - I'm fairly new to building Retool apps and looking for guidance on best practices (and bad ones to watch out for! :sweat_smile: ). Particularly, how do you manage synchronous query dependencies?


Now for the context...

I'm building a fairly simple app that displays a single table connected to a Firestore database. I followed the Firebase integration documentation to get that setup and all was :pinched_fingers:.

Everything worked mostly as expected. I wanted to allow the user to add a row so I went and enabled the "Show add row button" for the table and all of a sudden a user could add a row. But this is where the challenge began...

We have a field in the database schema for a mediaUrl of an uploaded image. Obviously our table doesn't just magically know that we're storing the images in Firebase Storage.

So I added a button onto the app that opened a form that had all of the fields. Here is our form:

Doing some digging around, I found that Retool doesn't have a direct integration with Firebase Storage but that you can work around that by uploading images via the Google Cloud Storage Integration because Firebase Storage is really just GCS under the hood.

So what do we need to do on form submission to make this work?

- Generate a unique name for the image so that there are not name collisions
- Upload the image to Firebase Storage
- Generate a signed URL for the image such that it is accessible from the client
- Use the signed URL to create a new object in the Firestore database

Obviously this flow includes some synchronous dependencies. Due to the multiple steps involved, I assumed that it would be best to manage all of this flow with a single Javascript component. Question: Is using a single Javascript component as a flow controller an anti-pattern in Retool?

Initially, I tried using onSuccess to manage the synchronous flow but wasn't happy with the look of the code and was really longing for a way to use async/await.

More digging around and I I found this post which included a pattern for managing synchronous calls using async/await. Giving it a try, it worked mostly as I had hoped. I ended up with the following:

(async () => {
  // Generate the unique name for the file by pre-pending a timestamp onto the file name
  let uploadName = `${moment.now()}-${feedItemImageDropZone.files[0].name}`;

  // Trigger the query to upload the image and wait for it to complete.
  await homeImageUpload.trigger({
    additionalScope: {
      fileNameForUpload: uploadName,
    },
    onFailure: (err) => console.log(`Failed homeImageUpload ${err}`),
  });
  
  await getSignedUrlForImage.trigger({
    additionalScope: {
      fileKey: uploadName
    },
    onFailure: (err) => console.log(`Failed getSignedUrlForImage ${err}`),
    onSuccess: console.log(`Signed: ${getSignedUrlForImage.data.signedUrl}`),
  });
    
  await newHomeSubmit.trigger({
    additionalScope: {
      signedUrl: getSignedUrlForImage.data.signedUrl,
    },
    onFailure: (err) => console.log(`Failed newHomeSubmit ${err}`),
    onSuccess: (data) => console.log(`Success: ${JSON.stringify(data)}`),
  })
    
})();

At the bottom of this post you can see my setup for the homeImageUpload and getSignedUrlForImage components.

Question: Is there a more Retool-y way of going about this?

Happy to her any advice or thoughts!


homeImageUpload Setup

getSignUrlForImage Setup

Welcome to the forum @bentley

You are on the right track, but you found an old post with an old way of doing it.

Here is a query I just wrote today that used the correct pattern (with some comments added)

// Make the data I am going to pass to the query
let lineItems = []
tblInventory.selectedRows.forEach(item => {
  lineItems.push({
    po_id: selMakePO_POs.selectedItem.po_id,
    sku: item.sku,
    date_checkedin: moment().format("YYYY-MM-DD HH:mm:ss")
  })
})

console.log(lineItems)   // for debugging
await qryAddMultiplePOLineItems.trigger({additionalScope: {lineItems: lineItems}})
// The qryAddMultiplePOLineItems.data property will now be populated with the query results
await tblInventory.clearSelection()
await qryPOLineItems.trigger()
// The qryPOLineItems.data will now be populated and the table that uses it will be updated.
await qryProductsLowInventoryWithoutPOLineItems.trigger()

You can also do this (but make sure "Keep variable references in sync" option in the Advanced tab is checked if you are going to return results from a query.)

const newPO = await qryPONew.trigger() // Load the query return values into newPO
await qryActivePOs.trigger()
selMakePO_POs.setValue(newPO.result[0].po_id) // We can then use newPO later in the query
modNewPO.close()

You can also check to see if the query finished successfully:

const newPO = await qryPONew.trigger() // Load the query return values into newPO
if (newPO) {
 // It worked, do stuff
   return newPO.result[0].po_id  // If another query called this one you can return values
} else {
 // it failed. qryPONew.error will probably say why depending on the resource
}
2 Likes

:+1: Perfect. Returning results from a query was exactly what I was looking for. That matches the mental model I have for normally writing JS :sweat_smile:.

Thanks for sharing!