You just tried making an API call into your first Atlassian Forge App, but now it won’t build and it’s displaying an error, or the data being returned isn’t what you expect.
You’re creating your first Forge app, but you’ve become stuck on async functions which you haven’t come across before.
Sound familiar? Then this blog is for you. This blog will help you learn enough to be able to successfully call APIs from your forge app. Plus, I’ll provide some suggestions on where you can learn more.
What the [object Promise] is this?!
You just tried making an API call into your first Atlassian Forge App. Now it won’t build and it’s displaying an error; or the data being returned isn’t what you expect.
There was an error invoking the function - Unexpected child type: object. Valid children are @forge/ui components, function components, and strings.
Error occurred in Text:
in Fragment
in App
in IssuePanel
in IssuePanel
Error: Unexpected child type: object. Valid children are @forge/ui components, function components, and strings.
Error occurred in Text:
in Fragment
in App
in IssuePanel
in IssuePanel
You were expecting some data, probably in JSON format – but what you’re seeing is [object Promise]
.
Why? When you make an API request in Forge, you’re dealing with an async function and it’s returning a promise.
What is a Promise?
A Promise is a stand-in for a value that is not yet known when the promise is created. It allows a developer to associate a handler with an asynchronous actions eventual value (or possibly failure). Promises let asynchronous methods return values in a similar way to synchronous methods.
So, when you call an async method, Instead of immediately returning the result, the method returns a promise to provide the result at some point in the future.
Promises were created to provide a more convenient way to do things asynchronously in Java. Before promises, callbacks were used, but callbacks were hard to read, and made handling errors complicated.
Learn more about Promises: Promise – JavaScript | MDN
What is an Async function?
Async functions allow developers to use await expressions within the function body. Together async and await allow asynchronous promise-based functions to be written in a cleaner style.
Await expressions make functions that would return promises behave like they’re synchronous. They do this by suspending execution until the result of the promise is provided (or rejected). This result (or error) is then treated as the return value of the await expression.
Learn more about await/async async function – JavaScript | MDN
This means that, to ensure that your app gets the result of the promise, you’ll need to add an await before your api call.
Now, if you’ve tried adding this directly into the App method, which is a constant function you’ll end up with an error. You can only use await expressions within async functions.
const App = () => {
const context = useProductContext();
console.log("context: " + JSON.stringify(context));
const response = await api.asUser().requestJira(route`/rest/api/3/search?jql=project%20%3D%20${context.platformContext.projectKey}`, {
headers: {
'Accept': 'application/json'
}
});
console.log(`Response: ${response.status} ${response.statusText}`);
const responseData = await response.json()
console.log(responseData);
return (
<Fragment>
<Text>Hello async world!</Text>
<Text>Project Key: {context.platformContext.projectKey}</Text>
<Text>Issue count: ??</Text>
</Fragment>
);
};
Error: 'await' expressions are only allowed within async functions and at the top levels of modules.
How do you call an async function from a non async method?
This is where UI kit hooks come in mighty handy.
A hook allows you to access state and lifecycle features of React.
In Forge UI kit hooks work similarly, allowing you to manage the data you need to render your app UI as variables change.
In this example, let’s look at the useState hook specifically, which can be used to solve the problem we’re running into above.
The useState hook
The useState hook allows you to define the state of a variable, as well as update it and render it.
In the example below, we declare a state variable called count
, and set it to 0
. Forge will remember its current value between re-renders, and provide the most recent one to our function. If we want to update the current count
, we can call setCount
.
const App = () => {
const [count, setCount] = useState(0);
return (
<Button
text={`Count is ${count}`}
onClick={() => {
setCount(count + 1);
}}
/>
);
};
Learn more about the Forge UI kit useState hook
So, why not use a regular variable?
When you use a useState hook in your app, it will detect when the useState variables state has changed, and trigger a re-render of the UI. This doesn’t happen for a regular variable. That means, when dealing with Async functions, when the result is eventually returned, it will trigger a re-render in the app – displaying the updated data as it becomes available (replacing your [object Promise] with the result you were expecting).
So, using the useState hook, we can now solve our problem as follows
import ForgeUI, {render, ProjectPage, Fragment, Text, useProductContext, useState} from '@forge/ui';
import api, {route} from '@forge/api';
const fetchIssues = async (projectKey) => {
const res = await api.asUser().requestJira(route`/rest/api/3/search?jql=project%20%3D%20${projectKey}`, {
headers: {
'Accept': 'application/json'
}});
const data = await res.json();
return data.issues;
};
const App = () => {
const context = useProductContext();
console.log("context: " + JSON.stringify(context));
const [issues] = useState(async () => await fetchIssues(context.platformContext.projectKey));
console.log(issues.length);
return (
<Fragment>
<Text>Hello async world!</Text>
<Text>Project Key: {context.platformContext.projectKey}</Text>
<Text>Issue Count: {issues.length} </Text>
</Fragment>
);
There are of course other hooks out there that can also be used, but they’re outside the scope of this article.
Stay tuned for a future blog where we dive deeper into Forge UI Kit Hooks.