Skip to main content
Big news! Interval has been acquired by Meter. Learn more →

Composability

Since Interval actions and pages are just async functions, sharing logic between actions is as simple as writing shared functions.

import { Action } from "@interval/sdk";
export default new Action(async (io, ctx) => {
//
});

To extract and share logic from an action that uses io and/or ctx, your shared functions can use the io and ctx objects exported from the Interval SDK:

import { Action, io, ctx } from "@interval/sdk";
export default new Action(async () => {
//
});

Both objects are identical whether you access them as exports from the SDK or as arguments passed to your action handler functions. Which form you use is matter of stylistic preference.

warning

Trying to access io or ctx outside of an action will cause an error to be thrown. If you write shared functions that reference io or ctx, only call those functions within an action.

Example: lookup user function

Imagine you have multiple actions that require the ability for person running the action to lookup a user from your database.

Our app might look something like this:

src/routes/refund_user.ts
import { Action } from "@interval/sdk";
import db from "../db";

export default new Action(async (io, ctx) => {
let user: User | null = null;

if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}

if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to refund a user
});
src/routes/edit_user.ts
import { Action } from "@interval/sdk";
import db from "../db";

export default new Action(async (io, ctx) => {
let user: User | null = null;

if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}

if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to edit a user
});

The above code is clearly very repetitive. We can extract our user lookup logic into its own function:

src/utils/users.ts
import { io, ctx } from "@interval/sdk";
import db from "../db";

export async function lookupOrSearchForUser() {
let user: User | null = null;

if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}

if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}

return user;
}

This lookupOrSearchForUser function can now be used in multiple actions:

src/routes/refund_user.ts
import { Action } from "@interval/sdk";
import { lookupOrSearchForUser } from "../utils/user";

export default new Action(async () => {
const user = lookupOrSearchForUser();
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to refund a user
});
src/routes/edit_user.ts
import { Action } from "@interval/sdk";
import { lookupOrSearchForUser } from "../utils/user";

export default new Action(async () => {
const user = lookupOrSearchForUser();
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to edit a user
});

Returning an I/O Promise from a helper function

info

In short, I/O Promises cannot be returned from async functions.

This currently only applies to TypeScript and JavaScript.

Returning an I/O method call directly from a helper function is a great way to share custom behavior for commonly used methods, allowing them to be used both on their own and in groups.

import { Action, io } from "@interval/sdk";

function customNumberInput(label) {
return io.input.number(label, {
decimals: 3,
min: 0,
max: 10,
});
}

export default new Action(() => {
const num1 = await customNumberInput("First");

const [num2, num3] = await io.group([
customNumberInput("Second"),
customNumberInput("Third"),
]);
});

However, because I/O Promises are lazy and are executed immediately when awaited, returning an I/O method call from an async function will not work as expected in an io.group. JavaScript will automatically resolve the promise at the end of the function body and return the result, which will cause the I/O method call to be rendered individually.

See MDN for more information about async functions.

There are two ways to work around this. The simplest is to remove the async keyword from the helper function, and pass in any data that requires an asynchronous function call.

Another option is to wrap the return value in a data structure that prevents JavaScript from immediately resolving the I/O Promise. This can be as simple as an array, an object, or a custom class wrapper.

Note that the I/O Promise will need to be unwrapped at the call site before being used.


import { Action, io } "@interval/sdk";


async function nameHelper() {
return {inner: io.input.text('Enter a name')};
}

export default new Action(async () => {
const name = await (await nameHelper()).inner;

const [nameFromGroup] = await io.group([
(await nameHelper()).inner
]);
});

Was this section useful?