Build your way
with a fully extendable agile development tool.


Get early access
2021-05-17

How we enable hot-reloading in production for extension developers

Aha! Develop is our extendable agile development tool. You can completely customize the UI, workflow, and integrations through extensions to create your team's ideal workspace. We made extensions with the goal of creating a lovable development experience.

When you write an extension for Aha! Develop, we want the experience to be as smooth as possible, without needing to take manual steps every time the code is changed. Frontend developers are used to seeing their changes appear before their eyes as they work in development mode. Getting instant feedback as you edit is satisfying and speeds the development lifecycle.

As you write an extension for Aha! Develop, your extension is uploaded to our production system. This makes instant feedback challenging compared to local development. And yet, if you use the aha extension:watch command, you'll see extension views change before your eyes.

Watch command

The client side of hot reloading is watching the file system for changes to the extension source files and running the equivalent of aha extension:install. We use the chokidar package. File system changes often come in a flurry, so the code applies a short timeout and batches the changes before calling install.

chokidar
  .watch('.', { ignoreInitial: true, ignored: '.git' })
  .on('all', async (event, changedPath) => {
    if (this.timeoutHandle) {
      this.changedPaths.push(changedPath);
      clearTimeout(this.timeoutHandle);
    } else {
      this.changedPaths = [changedPath];
    }

    this.timeoutHandle = setTimeout(
      () => this.performInstall,
      WAIT_TIMEOUT
    );
  });

We then use the fantastic esbuild to bundle the code into a JS file for every contribution entrypoint specified in the package.json file. By bundling the code by contribution, we can have a smaller eventual bundle size by loading only the required code in each place.

esbuild makes it very easy to generate the bundle — plus it's super fast. With some custom plugins, we can perform the bundling and uploading, including loading modules from skypack.dev, in 1–2 seconds.

esbuild.bulid({
  entryPoints: [path],
  bundle: true,
  outfile: 'bundle.js',
  plugins: [
    httpPlugin({ cache })
  ],
  target: 'es2020',
  write: false,
  sourcemap: 'external',
  sourcesContent: false,
  loader: { '.js': 'jsx' },
})

Hot reloading

If you've used Aha! before, then you'll know that it has a reactive and collaborative interface. If you update a record, then your colleagues will see your change almost immediately. If you're editing rich text fields, then your changes are visible to your team as you type.

Aha! establishes a websocket connection when the page is loaded and sends information through that socket about all record changes within the account you're logged into. To maintain a balance between permissions and performance, Aha! will send information about every single change in the account. It will limit the information sent via the websocket to only the record identifiers. When you're looking at a record, Aha! will be receiving updates about all record changes in the background. When Aha! sees an update related to the record you're viewing, it will fetch the updated record and update the screen.

Extensions are able to leverage this mechanism to provide hot reloading. In fact, we had to build the structure for hot reloading to make extension views work like the rest of Aha! If you build an extension that stores data against a record, like the planning poker extension, then changes to that data are visible to other team members in real time.

const storeVote = async (estimate) => {
  const user = aha.user;
  const key = `${FIELD_BASE}:${user.id}`;
  const newVote = {
    id: String(user.id),
    name: user.name,
    avatar: user.avatarUrl,
    estimate
  };

  // Set the vote on the record
  await record.setExtensionField(EXTENSION_ID, key, newVote);

  // Update the state of the current view
  setVotes(votes.filter(vote => vote.id !== user.id).concat([payload]));
  setHasVoted(true);
}

In this snippet of the planning poker extension, the vote data is saved to the record. The extension itself doesn't need to do anything more to communicate this change to any other viewers of the record. Aha! will send an update to all connected browsers logged into this account with information that there is an update to the extension fields for this particular record. If any browser has that record open in either the drawer or the details view, Aha! will reload the field data and call the render function in each extension with the new fields. For React-based extensions, this looks like a prop update.

aha.on("planningPoker", ({ record, fields }) => {
  return <PlanningPoker votes={votesFromFields(fields)}/>;
})

As we have this mechanism in place already for extension fields, it is a small step to perform a similar change when the extension code is uploaded. As soon as the extension is uploaded, a new extension bundle is ready. If you refresh the page at this point, you will see the changes. So Aha! sees the extension record change and the browser does an async import to make the new code available.

await import(
  `/extension_contributions.js?ts=${new Date().getTime()}`
);

The new code is now ready to run. Each extension view on the page needs to be instructed to reload. Unfortunately though, Aha! cannot call the render function again as it does for field data changing. This is because, as an extension author, you may have added event handlers or other kinds of long-running code. When extensions render, they are passed a function prop that is a callback for cleaning up when the extension is unmounted:

const elementDetectionHandler = (event) => {
  // does something on mouse move
}

aha.on("page", ({ onUnmounted, isUpdate }) => {
  onUnmounted(() => {
    document.removeEventListener("mousemove", elementDetectionHandler);
  });

  if (!isUpdated) {
    document.addEventHandler("mousemove", elementDetectionHandler);
  }

  return <Page />;
})

In this example, the extension is adding an event handler for its page view to add a special effect. If Aha! rerenders the component now and the new code has a change to the handler code, like renaming elementDetectionHandler to mouseMoveHandler , then a memory leak will occur and there will be inconsistent behavior.

Instead, Aha! will unmount each extension view currently open in the browser, running the onUnmounted callback, and reload them. This only takes a moment. It means there is a slight difference in behavior between fields being rerendered by extensions and hot-reloading. The extension will start again with a fresh state. Extension authors can make this experience better by using extension fields to store data in a way that allows the extension to reload from where it last left off — a good practice for the extension user experience anyway.

Developer experience

When you save a file in your editor and start switching to Aha!

  1. The aha-cli tool detects the file has changed
  2. esbuild creates a single JavaScript file bundle
  3. aha-cli uploads the bundle to Aha!
  4. Aha! sends a message to every connected browser to indicate the extension has updated
  5. The extension host code in the browser reloads the JavaScript
  6. The extension host code remounts every extension
  7. You see your changes within seconds of saving

By connecting up several existing mechanisms in Aha! that have already been in use for a slightly different purpose, we can offer developers a compelling feedback cycle when working on extension code that is being developed and deployed to an account on our production system.

Get started with Aha! Develop today and write your own extensions to customize your workflow.

Jeremy Wells

Jeremy is a Principal Software Engineer at Aha! — the world's #1 roadmap software. He likes thinking about software architecture and problem solving at any level of the software stack.

Follow Jeremy

Follow Aha!

© 2021 Aha! Labs Inc.All rights reserved