Quickstart
Hooks is designed to be easily installed and implemented within both browser and server side projects
It is built on top of @poppinss/hooks, but removes any environment-specific requirements allowing it to be used in both client and browser environments.
Installation
Install @nhtio/hooks directly from your preferred package manager using one of the following commands:
npm i @nhtio/hookspnpm add @nhtio/hooksyarn add @nhtio/hooksSetup
Import the Hooks class as follows:
import { Hooks } from '@nhtio/hooks'
const hooks = new Hooks()
hooks.add('saving', function () {
console.log('called')
})
// Execute hooks using runner
await hooks.runner('saving').run()Defining hooks
The hooks are defined using the hooks.add method. The method accepts the event name and a callback function to execute.
const hooks = new Hooks()
hooks.add('saving', function () {
console.log('called')
})You can also define hook as an object with the name and the handle method property. This is usually helpful when you want to specify a custom name for the hook, or re-use the same handle method multiple times.
const hooks = new Hooks()
function handleSave() {}
hooks.add('saving', { name: 'beforeSave', handle: handleSave })
hooks.add('creating', { name: 'beforeCreate', handle: handleSave })The handle method receives the first argument as the event name, followed by the rest of the arguments supplied during runtime.
Running hooks
You can execute hooks using the Hooks Runner. You can create a new runner instance by calling the hooks.runner method and passing the event name for which you want to execute hooks.
const hooks = new Hooks()
const runner = hooks.runner('saving')
await runner.run()To run hooks in the reverse order, you can use the runner.runReverse method.
const hooks = new Hooks()
const runner = hooks.runner('saving')
await runner.runReverse()Passing data to hooks
You can pass one or more arguments to the runner.run method, which the runner will share with the hook callbacks. For example:
const hooks = new Hooks()
hooks.add('saving', function (model, transaction) {})
const runner = hooks.runner('saving')
await runner.run(model, transaction)Cleanup functions
Cleanup functions allow hooks to clean up after themselves after the main action finishes successfully or with an error. Let's consider a real-world example of saving a model to the database.
- You will first run the
savinghooks. - Assume one of the
savinghooks writes some files to the disk. - Next, you issue the insert query to the database, and the query fails.
- The hook that has written files to the disk would want to remove those files as the main operation got canceled with an error.
Following is how you can express that with cleanup functions.
hooks.add('saving', function () {
await fs.writeFile()
// Return the cleanup function
return (error) => {
// In case of an error, remove the file
if (error) {
await fs.unlink()
}
}
})The code responsible for issuing the insert query should run hooks as follows.
const runner = hooks.runner('saving')
try {
await runner.run(model)
await model.save()
} catch (error) {
// Perform cleanup and pass error
await runner.cleanup(error)
throw error
}
// Perform cleanup in case of success as well
await runner.cleanup()Note: The
runner.cleanupmethod is idempotent. Therefore you can call it multiple times, yet it will run the underlying cleanup methods only once.
Exclude certain hook handlers
You can exclude certain hook handlers from executing using the without method.
In the following example, we run hooks without executing the generateDefaultAvatar hook handler. As you can notice, you can specify the function name as a string.
hooks.add('saving', function hashPassword() {})
hooks.add('saving', function generateDefaultAvatar() {})
await hooks.runner('saving').without(['generateDefaultAvatar']).run()Event types
You can also specify the types of supported events and their arguments well in advance as follows.
The first step is to define a type for all the events.
type Events = {
saving: [
[BaseModel], // for hook handler
[error: Error | null, BaseModel], // for cleanup function
]
finding: [
[QueryBuilder], // for hook handler
[error: Error | null, QueryBuilder], // for cleanup function
]
}And then pass it as a generic to the Hooks class.
const hooks = new Hooks<Events>()Additional API Methods
Check if hooks exist
You can check if any hooks are registered for a specific event using the has method:
const hooks = new Hooks()
hooks.add('saving', function hashPassword() {})
// Check if any hooks exist for an event
if (hooks.has('saving')) {
console.log('Saving hooks are registered')
}
// Check if a specific handler exists
if (hooks.has('saving', hashPasswordHandler)) {
console.log('Hash password handler is registered')
}Remove hook handlers
Remove a specific hook handler using the remove method:
const hooks = new Hooks()
const handler = function hashPassword() {}
hooks.add('saving', handler)
// Remove the handler
const removed = hooks.remove('saving', handler)
console.log(removed) // true if handler was found and removedGet all registered hooks
Access all registered hooks using the all method:
const hooks = new Hooks()
const allHooks = hooks.all()
console.log(allHooks.size) // Number of events with handlersMerge hooks from another instance
Combine hooks from multiple instances using the merge method:
const hooks1 = new Hooks()
const hooks2 = new Hooks()
hooks2.add('saving', handler1)
hooks2.add('deleting', handler2)
// Merge hooks2 into hooks1
hooks1.merge(hooks2)Check cleanup state
You can check if cleanup is pending on a runner:
const runner = hooks.runner('saving')
await runner.run()
if (runner.isCleanupPending) {
await runner.cleanup()
}