Follow

Follow
Strapi lifecycle hooks

Strapi lifecycle hooks

A practical use case to learn Strapi lifecycle hooks "by doing"

Jib's photo
Jib
ยทDec 17, 2022ยท

6 min read

Table of contents

  • The issue
  • The solution with lifecycle hooks
  • Alternative lifecycle hooks strategies
  • Subscribing to database layer events
  • Content type lifecycles
  • Conclusions and acknowledgments
  • Source code and Strapi course

In this post, I want to show a practical implementation of Strapi lifecycle hooks. These are functions that are automatically triggered by Strapi after or before a certain event occurs that involves content types, for example creating or deleting a content type item. I will do this while solving a real-world issue I faced while working on the "developer blog" project for my Complete Strapi Course. If you want to follow along, here I link the repo at the commit stage before adding the lifecycle hooks code: link

If you prefer learning by videos, here you have it!

The issue

In my blog Strapi app, I wanted to create a simple Post content type, that was meant to have an authors relation field to keep track of the admin user that has created each post, plus additional admin users that may be co-authors. You can check this implementation in the repo link reported above. Now the problem is that, for security reasons, admin users are not returned by the Strapi API, so it was impossible to retrieve the authors field from the client.

The solution with lifecycle hooks

Using lifecycle hooks we can put in place a workaround for this issue. We can create a new dedicated collection type for Authors to associate with the post, which will be free from any security concerns and so free to be returned as an API response. Spoiler: the Author is just a convenience duplicate of the Admin User, so we'll replicate the fields accordingly as follows:

image.png

As you can see I also added a 1-to-1 relation with the Admin User, which is necessary to keep the link between the 2 collections and to retrieve the Author related to a certain Admin User. But again, we still want instances of this new Author type to be really in sync with Admin Users, that are the only ones who should be able to create posts.

With that said, here's what we're going to do using lifecycle hooks to cut manual creation/update of entities:

  1. listen to the events of creating and updating instances of Admin User, to automatically create or update an instance of Author; optionally this may be done also for the delete event (but I don't feel that necessary)

  2. add a content-type lifecycle hook for Posts to automatically assign the Admin User who's creating a new Post (or, better, the corresponding Author instance) to the newly created Post as an author

Alternative lifecycle hooks strategies

The hooks mentioned above that we're going to create allow me to introduce the 2 strategies that Strapi offers to subscribe to events, as mentioned in the docs:

  • the first one is to create a lifecycle file specific to a content type: that's the obvious way to go if you're dealing with API you (or your plugins) have introduced in the app, so we'll do this for the Post hook

  • the alternative way, which seems simpler for the Admin User content type, is to directly subscribe to events at the database layer: we'll apply this for the Admin User hooks

Subscribing to database layer events

Let's start creating the hooks for our Admin User type. For this, we edit the bootstrap function inside the src/index.js file. Let's first add the afterCreate hook: see the comments in the code below.


// ./src/index.js

module.exports = {
  // Omitted
  bootstrap({ strapi }) {
    // we listen to lifecycle events...
    strapi.db.lifecycles.subscribe({ 
      // only listen to events for type with this UID
      models: ["admin::user"],
      // after creating a new Admin
      afterCreate: async ({ result }) => { 
        // take all attributes of the created instance...
        const {
          id,
          firstname,
          lastname,
          email,
          username,
          createdAt,
          updatedAt,
        } = result;  
        await strapi.service("api::author.author").create({ 
          // and use those to create a corresponding Author
          data: {
            firstname,
            lastname,
            email,
            username,
            createdAt,
            updatedAt,
            // note how I assign the new Admin User to the Author
            admin_user: [id],           
},
        });
      },
    });
  },
};

As you can see we have subscribed to the event triggered after the creation of a new entity of admin::user type (what I've called Admin User until now) and we perform a simple operation of taking the information about the newly created instance and using that to create a corresponding Author instance, also with a relation with the Admin User.

Now let's do a very similar operation for updates:

// ./src/index.js

module.exports = {
  // Omitted
  bootstrap({ strapi }) {
    strapi.db.lifecycles.subscribe({ 
      models: ["admin::user"],
      afterCreate: async ({ result }) => {
        // omitted
      },
      afterUpdate: async ({ result }) => {
        // we firstly get the ID of the Author that corresponds 
        // to the Admin User that's been just updated
        const correspondingAuthor = (
          await strapi.service("api::author.author").find({
            admin_user: [result.id],
          })
        ).results[0];

        // and we update accordingly the corresponding Author 
        // with the updated properties
        const { firstname, lastname, email, 
username, createdAt, updatedAt } = result;
        await strapi
          .service("api::author.author")
          .update(correspondingAuthor.id, {
            data: {
              firstname,
              lastname,
              email,
              username,
              updatedAt,
            },
          });
      },
    });
  },
};

Now you can test that new Authors are created/updated upon creating/updating Admin Users. Again, we could extend this to the deletion case, but I don't think this is necessary for my use case.

Content type lifecycles

Now to the last step: ensuring that each Post that gets created gets the Author corresponding to the Admin User who's creating that post in the authors relation field. For this, we have to create a lifecycles.js file inside the ./src/api/post/content-types/post/ folder, as follows:

// ./src/api/post/content-types/post/lifecycles.js
module.exports = {
  beforeCreate: async ({ params }) => {
    // find the Admin User who created the post
    const adminUserId = params.data.createdBy;
    // find the corresponding Author
    const author = (
      await strapi.entityService.findMany("api::author.author", {
        filters: {
          admin_user: [adminUserId],
        },
      })
    )[0];
    // update the data payload of the request for creating the new post
    // by adding the proper Author to the `authors` relationship
    params.data.authors.connect = 
      [...params.data.authors.connect, author.id];
  },
};

Please note that:

  • you don't have to specify the content type (or model) you're referring to, because we are inside a content type specific lifecycle file

  • in this case, we didn't access the result object, that clearly exists only in after* methods: here we still don't have a result, but we can handle the payload of the incoming post creation request, and in fact, we alter it in the last line of this code (that's params.data)

  • in order to find the correct Author for the new post, we had to use a relational field as a filtering method: that's why I used the lower Entity Service API instead of the usual Service API I used in previous steps

  • when editing the params.data.authors field of the payload, consider that the array of data is expected to be nested inside a connect subfield

Conclusions and acknowledgments

That's it; we have successfully re-implemented the Post-Author relationship more robustly and took advantage of Strapi's lifecycle hooks for not having to perform any manual tasks on our content types (besides writing the post of course ๐Ÿ˜„). Please let me know in the comments about any feedback.

I want to thank the following users from the Strapi Discord Server who suggested such a solution (in record time!): KevinRI and Derrick Mehaffy from the Strapi team.

Source code and Strapi course

The updated source code is available here. As I mentioned, this is the repo of the project for my Complete Strapi Course hosted on Udemy. In case you're interested, that's a 14+ hrs course to learn everything about Strapi, from the basics to very advanced topics such as plugin development and deployment.

Did you find this article valuable?

Support Jib by becoming a sponsor. Any amount is appreciated!

See recent sponsors |ย Learn more about Hashnode Sponsors
ย 
Share this