A Deeper Look at Hooks in Forge

Reading Time: 7 minutes

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:

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

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.