Plugin Lifecycle Documentation and Questions

Describe the issue:

  • We have a single plugin that shares some core functionality (stores) across some discrete plugin hooks (form outlet, field extensions, sidebar, etc). We used this setup to avoid having many instances of plugins living at all times, based on how the plugin system works.
  • This has worked for us in testing 95% of the time, but occasionally we see the hooks called multiple times when opening the editor for a model record. We log when the entrypoint of a hook is called and most of the time that is a single time. However, sometimes a distinct form outlet hook is called up to 8 times when opening the editor.
  • This led me to the documentation to just understand the lifecycle of the plugin hook calls and just wasn’t able to find anything. Is what i’m describing something that could happen? If so, what would cause it to happen only in some cases?
  • By enabling field extensions associated with fields within a model while also having a form outlet defined, will the form outlet hook get called for each time we have a field extension enabled? We assumed the form outlet hook would only get called once.
  • Overall, it just isn’t well documented how the lifecycle is managed. What explicitly happens when you navigate away from a record editor to the record list, then back to the editor?

Do you have any sample code you can provide?

  • Our form outlet can be enabled from the plugin config at the model-level
  • It lives hidden in the form outlet area and handles derived/computed fields
itemFormOutlets(itemType: ItemType, ctx: IntentCtx) {
    // Get the list of model api keys configured on the plugin extension page
    const { modelApiKeys } = ctx.plugin.attributes.parameters as PluginConfig;
    const modelApiKeysArray = Array.isArray(modelApiKeys) ? modelApiKeys : [];

    // Check if this model is enabled for this plugin
    const modelIsEnabled = modelApiKeysArray
      .map((model) => model.value)
      .includes(itemType.attributes.api_key);

    // Debug
    logger.debug(
      `Checking ${itemType.attributes.api_key} for Item Form Outlet - Enabled: ${modelIsEnabled}`
    );

    if (modelIsEnabled) {
      logger.info(
        `Rendering Item Form Outlet for ${itemType.attributes.api_key}`
      ); // This is logged one time
      return [
        {
          id: "global_dynamic_computed_fields",
          initialHeight: 0,
        },
      ];
    } else {
      return [];
    }
  },
  renderItemFormOutlet(outletId: string, ctx: RenderItemFormOutletCtx) {
    switch (outletId) {
      case "global_dynamic_computed_fields":
        logger.debug(`Rendering Form Outlet: ${outletId}`); // This is logged multiple times, but not each time the form loads
        return render(.... the component ...) 
     }
  },```

Hey @nroth,

Thank you for this detailed report, and I’m sorry about the vague documentation.

I think by this point you are definitely pushing our plugin system to its limits (in a good way! thank you for using them so robustly), and you probably know them better than many of our staff… especially myself :sweat_smile:

Once the current fire with the Heroku situation is dealt with, I’ll look into this more deeply for you. It will probably require a deeper dive with the devs to properly understand this (for myself too). Once I have good answers, I will post back here and also write new documentation to make it clearer.

Appreciate you bringing this up, and please bear with us while they deal with the bigger crisis… this is important too, and I hope to have clarity for you soon!

@nroth,

Starting to look into this now, but have a preliminary question for you. When you said:

Does that mean there is some code (not in your snippet) that also does some field manipulation in that record? Are there any async or network calls (either ones that you made yourself, e.g. to the CMA, or ones you are using straight from the ctx methods)? Just wondering if maybe some of the redraws are tied to data or state changes resulting from record edits.

I’m going to try to replicate this behavior locally and see if I can isolate it at all. I also pinged the main plugin SDK developer to see if he has any thoughts upfront (though he’s also busy with the Heroku situation right now, so it might be a few days before he can answer this one). I’ll do my best to investigate for you in the meantime.

Hey @nroth,

Is this an open source plugin you would be able to share the code with us?
If not, can you send us the version of the SDK that you are using so I can give you an example repo?

Thank you!

Oops: While @m.finamor was posting that, I was working on my own reproduction.

@nroth If you don’t already have a reproducible example made, we can use this plugin to try to reproduce: https://github.com/arcataroger/plugin-lifecycle-forum-7715/blob/main/src/main.tsx

Which is tied to this project (emailed you an invite):
https://forum-7715-plugin-lifecycle.admin.datocms.com/editor/item_types/KNMkh-adQg2A_oYHZN0ecg/items/PPmUC5B6QJmJ9tSR4jIkWA

So far I haven’t been able to make the message ever render more than once per record.

@nroth Could you please share more of the actual code of what it’s doing, especially any potential async or network calls?

OK, if I try to use ctx.setFieldValue(), I’m able to get the re-renders to happen:

I think that causes the entire plugin iframe to redraw. Hmm… still looking into it, but is that maybe what you’re seeing because of the computed fields?


EDIT: Made it reproducible (click into any record and wait 3 seconds): https://forum-7715-plugin-lifecycle.admin.datocms.com/editor/item_types/KNMkh-adQg2A_oYHZN0ecg/items/ZSDcqlzHS_Gq0mnbm75kgQ/edit

Also got a better answer from a dev:

The lifecycle (“when are hooks called by the CMS”?) depends on the type of hook itself.

In general, however, they function somewhat like a React component: every time its arguments change, the hook is called again, allowing it to produce a new, updated rendering/result. Just like with React, we use referential equality checks to see if the arguments change, so it may sometimes seem like unnecessary calls to the hooks are being made.

This does not apply to specific hooks, such as event hooks for example. These are called only once when certain conditions are triggered.

Does all of the above provide enough information? Was there anything you were trying to solve, specifically, or did you just want to know how it works?