Introduction
In an earlier blog, I briefly discussed UI Kit Hooks, and the useState hook in relation to calling, and displaying the results of an async function from a Forge App.
This blog will dig in deeper to uncover what hooks are, how they are used and some of the differences between how hooks work in UI Kit, UI Kit 2 and Custom UI.
As you read this blog, you’ll learn that UI kit and UI kit 2 use hooks quite differently. Going forward, to make the difference between UI kit and UI kit 2 really clear, I’ll refer to UI kit as UI kit 1. If you see me mention UI kit 1 below, please keep in mind this is referencing what is known in our developer docs as UI kit. |
What are Hooks?
A hook allows you to access state and lifecycle features of React easily.
The useState and useEffect hooks are the two fundamental hooks in React.
UI Kit 1 Hooks
In Forge UI kit 1, hooks have been implemented to work similarly to React hooks, allowing you to manage the data you need to render your app UI as variables and state change. However these are actually implemented and run on a FaaS platform and are executed server side.
In Forge UI Kit 1, there are 8 different hooks. These include the useEffect and useState hooks which resemble React Hooks, along with others are more clearly specific to Atlassian Products:
- useState [Resembles React useState]
- useEffect [Resembles React useEffect]
- useAction
- useProductContext
- … and so on
When you create a UI Kit 1 App, you’re using the Atlassian implementation of the useState and useEffect hooks, and these differ a little to the React implementation.
In the discussion below, I’m going to focus on the functionality of the React hooks. I’ll share how the UI kit 1 implementations differ from the React implementations so you don’t get caught out when building a UI kit 1 app.
Why do UI kit 1 hooks work differently? While UI kit 1 components may look similar to React components, there are some important differences. React apps are rendered and executed on the client side, whereas Forge apps run on a FaaS (function as a service) platform and are executed on the server side. Because the code is run in AWS lambda it does not contain state, instead the state is stored in the frontend and passed back and forth within the invocation request/response. Furthermore, because the code is run in an AWS lambda, you need to ensure you await asynchronous code – otherwise the lambda might return before the async code has had a chance to finish executing. |
UI Kit 2 and Custom UI Hooks
When you use UI Kit 2 or Custom UI, you will use the React Implementation of the useState
and useEffect
hooks, rather than the Atlassian implemented hooks.
While this means you don’t have access to the Atlassian UI Kit hooks like useProductContext
via @forge/ui
you can still access this data via @forge/bridge.
The useState Hook
What is State?
The basic building block of a React app is a component. Components may take in properties – which might be used when rendering you app. In addition a component can also have state variables – the state keeps track of all the information within a component.
So, the state variables represent a components underlying data model. But, state is also important because whenever the data within the components state changes, the component is automatically re-rendered to reflect the change.
The useState
hook allows you to add a state variable to your component, and provides a way to update the variable should it need to change. React will then automatically detect when the variable changes and re-render the component.
The useState Function
const [state, setState] = useState(initialState);
Parameters
initialState
is the value you want the state to be initially. It can be a value of any type (though if it is a function, there are some special requirements)state
is the current state – use this only to read the current state, not change it.setState
is a function which allows you to update the state to a different value and importantly, trigger a re-render.
Set Functions
You can use the setState
function returned by useState to update the state to a different value. You can either pass the new state directly, or you can pass a function to calculate the new state based on the previous state.
function handleClick() { setState('new state'); …
or
function handleClick() { setState(a => a + 1); …
Some important things to note
- When passing a function in the set function, it must be pure. The only argument should be the pending state and it should only return the new state.
- React batches state updates – it only updates the screen once all the event handlers have run and called their set functions
- The set function only updates the state for the next render. If you read the state variable after calling set, you’ll get the old value
Differences when using UI kit 1
While the back end for the useState
hook is implemented differently, at this stage I don’t have any examples of how your Forge UI Kit 1 app would differ to UI kit 2 or Custom UI.
If you’ve found one however, I’d love to hear about it. Be sure to reach out to me over on the Developer Community Forums.
Further Reading
The useEffect Hook
What is an Effect?
But actually, before that we need a quick refresher on Pure functions.
A function is pure if, when given the same input it will always return the same output.
Most react components are intended to be pure functions.
Now, then lets go back to effects. In the case of React, an Effect refers to a functional programming term known as a side effect.
Side effects are problematic, because their results may not be predictable. Virtually all applications rely on some kind of interaction with the outside world, and this may not always give a predictable result. So, we need a way to handle them.
So, useEffect
provides a way to handle side effects while keeping your components otherwise pure, in response to a change in data.
The useEffect Function
useEffect(setup, dependencies)
Parameters
setup
is the function with your effects logic. This might optionally return a cleanup function.dependencies
are a list of all values referenced from within the setup function. While dependencies are optional, if omitted the effect will re-run after every component re-render.
Calling Async functions in useEffect
Lets begin with a quick explanation of what not to do
// this won't work
useEffect(async () => {
const result = await api.asUser().requestJira({route});
}, [route])
The above function won’t work. The reason for this is that the first argument of the useEffect
function should return either nothing, or (in the case of React but not UI Kit) a function for cleanup purposes, but in the example above the async function returns a Promise.
In a UI kit 2 app, your useEffect
call might look like this:
import React, { useEffect, useState } from 'react';
import ForgeReconciler, { Text } from '@forge/react';
import {requestConfluence } from '@forge/bridge';
const App = () => {
// create a variable that the result from useEffect can be stored in.
const [user, setUser] = useState(null);
useEffect(() => {
// create a function within useEffect to fetch the data from the API and store it using the
// useState variable created earlier
const fetchData = async () => {
const response = await requestConfluence('/wiki/rest/api/user/current');
// await the response, rather than working with the Promise
const currentUser = await response.json();
console.log(currentUser);
setUser(currentUser.publicName);
}
fetchData()
}, []);
return (
<>
<Text>This example app uses client-side UI Kit with useState and useEffect from React</Text>
<Text>Hello {user ? user : 'Loading User...'}</Text>
</>
);
If you’re using UI Kit 1 then the above approach will work similarly with a few small tweaks:
On line 7 you’ll want to specify that useEffect is async:
useEffect ( async () => {
And on line 19 you’ll want to await the fetchData function:
await fetchData()
This is because you need to ensure you await asynchronous code. If not, the lambda might return before the async code has had a chance to finish executing.
When is the useEffect function run?
When useEffect runs is actually a little complicated, and is based on what you set as your functions dependencies.
- If your useEffect function has no second argument (eg
useEffect( someFunction)
) , it will be called on the initial component render, and then after every subsequent render. - If your useEffect function has an empty array as the second argument (eg
useEffect (someFunction, [])
) then it will be called on the initial component render, and then never again. - If your useEffect function has an array with one or more dependencies, it will be called after each render but only if one or more of the arguments in the dependencies changes.
What can you return from a useEffect function?
The only thing you can return from useEffect
is a function. The function returned is known as a cleanup function, and it’s important to understand when the cleanup function is run.
The cleanup function is called just before the useEffect function is called (except on the initial render).
If you set your second argument to an empty array, the cleanup function won’t ever be called.
Differences when using UI kit 1
- The useEffect function implemented by UI Kit 1 does not support a cleanup function
- The useEffect function implemented by UI Kit 1 requires a second argument, which can be an empty array.
- You need to include await / async keywords to your useEffect function in UI Kit 1. If you forget them, the lambda may return before your async code has had a chance to finish executing.
Further Reading
- Learn more about the useEffect Hook
- How to use async functions in useEffect (with examples) – Devtrium
Special Mention: the useAction Hook (Forge UI Kit 1)
In Forge UI Kit 1, you have the alternative option to use useAction
rather than combining useEffect
with useState
.
Here’s an example:
import ForgeUI, { useAction, render, Fragment, Macro, Text } from "@forge/ui";
import api, {route} from "@forge/api";
const fetchUser = async () => {
const response = await api.asUser().requestConfluence(route'/wiki/rest/api/user/current')
const currentuser = await response.json();
return currentuser;
}
const App = () => {
const [user] = useAction(value => value, async () => await fetchUser().catch(console.error));
return (
<Fragment>
<Text>Hello {user.publicName}</Text>
</Fragment>
);
};
export const run = render(
<Macro
app={<App />}
/>
);
Summary
In my earlier blog, I briefly discussed UI Kit Hooks, but didn’t go into a lot of detail about what they were or why they were useful.
I hope this blog has provided a deeper explanation about how and why Hooks are so useful, and how to use them within your Forge Apps.