JavaScript Best Practices
JavaScript Transformers
Transformers can read and manipulate data from queries and components, but they cannot control components or trigger queries within a Retool application.
Within transformers, you can leverage the {{ }}
notation to reference components and queries in your application.
References within the {{ }}
are considered inputs to a transformer, and any update to an input will rerun the transformer.
Transformers will automatically run and be recomputed when any objects referenced by them change. Consequently, having many transformers in an application can impose a performance penalty (since they will all get re-evaluated on each input’s state change).
Having many transformers in an application can incur a performance penalty because of how Retool eagerly re-evaluates transformers. For example, a transformer that references some objects, and is itself referenced by some components, will re-evaluate when any of the objects it references change, with each re-evaluation causing all the components to be re-rendered. The number of re-evaluations and re-renders can compound quickly in a complex application with multiple transformers, which results in slow/unresponsive apps.
We recommend only using transformers to compute values that must be kept up to date in your application.
When to use JavaScript transformers
-
Data manipulation: Change the results of one or multiple queries into a different format that you can reference through
{{ transformer.value }}
. Note, transformers will not allow you to control components or trigger queries, but instead should only be used for manipulation of data values. -
Helper functions: Reference the output of complex, multi-line JS logic in other places without writing transformations in-line.
For example, current_ui
will stay up-to-date and return the currently selected UI. In other components, simply reference current_ui.value
rather than recomputing its logic.
JavaScript Queries
You can write JavaScript queries within Retool to create rich interactivity between components and queries. These queries can compose arbitrary pieces of logic to trigger queries and manipulate components in a single function call, creating greater customizability for your applications.
JavaScript queries differ from transformers in the following aspects:
-
Unlike transformers, JavaScript queries can be manually triggered. This gives you control over when and where a query will be running. When a query is set to run manually, you can use the “watched inputs” setting to have it behave the same as running automatically for those specific inputs.
-
JavaScript queries can mutate application state by controlling components and triggering queries within an application. Consequently, JavaScript queries can effectively encapsulate data manipulation and application control flow (e.g. a single JavaScript query can reformat some user input data, pass it to a REST endpoint and then close a modal).
-
Objects in Retool’s application state are referenceable within JavaScript queries without use of the
{{ }}
syntax.
When to use JavaScript queries
-
Centralize complex logic: Instead of scattering complex logic throughout an app, you can consolidate critical app business logic within a single JavaScript query. This, in turn, will make the logic flow cleaner and make it easier to quality control. Check out the note in the JS queries sub-section.
-
Conditionally trigger queries based on application state: A common use case is to build out decision logic within your app. You can leverage JavaScript queries to programmatically trigger based on certain input conditions (e.g. triggering notifications based on success of a query). Check out the “Creating Critical Business Logic with Bulk Update” sub-section.
-
Create a custom API-like functionality: JavaScript queries allow you to compose an API of sorts that combines multiple queries together along with the ability to use additionalScope to pass down params for each query. If you have multiple functionalities to be achieved, you can roll them into a single JS query that takes in different parameters and passes them down to the underlying queries. The JS query can be called using “Run Script” with additionalScope used to specify inputs.
Explore Examples
Check out this app to interact with the below examples and download the code seamlessly to connect to the same resource endpoints!
Note We are using the Retool API generator to create API endpoints, with example endpoints below you can add as resources on your Retool instance to interact with the app.
pending_approvals
example endpointverified
example endpoint
JavaScript Transformers Examples
Data manipulation with JS Transformers and Plotly Charts
A common UI/UX pattern is to slice and transform query data into a more visual format. In order to reshape our data, we’ll leverage JavaScript transformers.
For a CX agent in this example it would be helpful to have high level metrics of the requests they need to go through. So we created a chart that visualizes the status of all the pending requests a CX agent has to go through.
We’ll need to query the pending_approvals
endpoint to fetch data for the graph visualization. However, the response won’t be in the format we need it to be, so that’s where Transformers come into play to manipulate the data to match the expected input for our charting library.
Now that we have a data format with labels and datasets we just need to connect this to our charting component.
Our Chart component uses Plotly, and can build a wide variety of different charts (check out Plotly’s documentation), for this example since we used a transformer to do the hard work utilizing the chart component is seamless. Now we can easily see pending requests by approval status.
JavaScript Queries Examples
Trigger side effects with event handlers
Event handlers provide a great method to extend interactivity between a component and the rest of the Retool ecosystem. So far, we have explored one common use case of utilizing event handlers to control a query; now we’ll take a look at another common use case while understanding best practices with event handlers.
In the last tutorial, we built out the ability to view a specific pending request’s metadata based on the Table’s selected row. We’ll extend this functionality with decision logic that will display a list of recommended actions a CX agent can take based on the approval status of a pending request.
We’ll need to use the Row Click
event handler that performs the Control Component
action. This will make the row click on the table to take the user to another component, in this example we’ll take the user to a separate tab in a tabbed container which highlights status specific actions a CX agent can take advantage of.
Additionally, we see again how we can leverage JS to control which tab of the tabbed container to show depending on the Approval Status.
Note This logic can also be abstracted to be within a JS query as well. As a rule of thumb if the complexity of the application increases it can be easier to consolidate the query logic and dependencies in a JS query instead of through components and chained queries.
Here’s a simple example below that demonstrates how to centralize complex logic.
if (!pending_table.selectedRow.data) return;
const issue = pending_table.selectedRow.data.approval_status;
switch(issue) {
case 'Late Request':
tabsRecommendedActions.setCurrentViewIndex(1)
break;
case 'In Review':
tabsRecommendedActions.setCurrentViewIndex(0)
break;
}
Create critical business logic with bulk updates
In certain cases, event handlers aren’t robust enough for a greater level of complexity when queries begin to get chained. This is where scripting in Retool can come into play in order to consolidate queries and actions into one central location through JS queries.
For a CX agent, a common scenario we want to account for is accidentally approving several users that now need to be bulk removed from our approval table.
In order to do this, we can chain and programmatically trigger multiple queries within a single JS query to bulk update a resource.
const promises = approved_table.selectedRow.data.map((row) => {
return delete_approved_users.trigger({
additionalScope: {
id: row.id,
},
});
});
return Promise.all(promises);
In the image above we are removing all the selected users in the Approved users table by triggering the delete_approved_users
query. This example is just the first step in really adding complexity with scripting as users could combine multiple queries, JS functions like exporting as a PDF, and branching to achieve a complicated workflow all in a JS function if necessary.