Git Hooks are one of the most integral parts of development workflows today. Hooks assist in maintaining code quality across a large codebase with several contributors. With the introduction of linting, code formatting, and type checking, there are more Git Hook tasks than ever.
For frontend developers (or JavaScipt devs in general), Husky is the go-to choice for managing Git hooks. Husky, although popular, lacks parallelization, which can speed up the execution of hooks. It also adds loads of dependencies to node_modules
, which can be an issue.
In this article, we will look at Lefthook, a faster, simpler, and more powerful alternative to Husky for frontend developers. We will look at how Lefthook compares to Husky by adding it to a sample React and React Native application.
The JS-based frontend community has grown to the point where tools developed with other languages now support JS development. Lefthook is a product of this trend; developed using Go, it claims to be the fastest polyglot Git Hooks manager for Node.js and Ruby, but I believe it works well for other stacks, too. Since it works via a single binary, the node_modules
directory is polluted less.
Lefthook stands out among Git Hook managers because of the incredible features that can bolster development workflows without any management hassle. All of the listed features here are unavailable in Husky.
Lefthook is developed with Go, giving it more potential to utilize parallelization of machines; enabling parallel execution of tasks is a matter of just a single line.
Parallel execution is one of the most important requirements for large-scale apps. For larger commits, one can utilize spell checks, type checks, ESLint, and StyleLint in parallel, saving tons of time for developers. I will share an example of this in a React app later on.
Lefthook assists in executing commands on a limited set of files, with prebuilt shorthands, glob and RegEx filters, and the option to run in a sub-directory.
This can be super helpful if you are running JS linters and StyleLints in pre-commit tasks; ideally, type-checking tasks will go with a full list of files while ESLint and StyleLint will be applied to staged files. Here are examples of selecting files to execute tasks:
pre-commit: commands: lint: glob: "*.{js,ts,jsx,tsx}" run: yarn eslint
pre-commit: commands: frontend-linter: glob: "*.{js,ts,jsx,tsx}" # glob filter for list of files run: yarn eslint {staged_files} # {staged_files} or {all_files} - list of files
pre-push: commands: frontend-style: files: git diff --name-only master # files with, diff with master. glob: "*.js" run: yarn stylelint {files}
Although the purpose of using Git Hooks is to keep the codebase’s quality intact while several developers are working on it, one may need a way around it to execute specific hooks differently on their local machine. Lefthook supports such optimization cleanly by adding a separate file to manage Hooks differently in a local environment.
Let’s create a commit-msg
hook to understand local configs better. Use the following code to create a new hook:
lefthook add -d commit-msg
Adding -d
will create the .lefthook/commit-msg
and .lefthook-local/commit-msg
directories. The first one is for common project-level scripts; the second is for personal scripts, which will be used when tasks are run locally only on the creator’s machine. Ideally, .lefthook-local
should be part of .gitignore
.
With Lefthook, you can create custom tasks with any programming language. This gives us the freedom to optimize Git Hooks by making them faster.
Going beyond this, we can select runners for these scripts, like Docker VMs. The following is a simple example of handling custom script with a Bash runner.
Let’s create a Bash script to check commit templates in .lefthook/commit-msg/template_checker
:
INPUT_FILE=$1 START_LINE=`head -n1 $INPUT_FILE` PATTERN="^(ISSUE)-[[:digit:]]+: " if ! [[ "$START_LINE" =~ $PATTERN ]]; then echo "Bad commit message, see example: ISSUE-123: some text" exit 1 fi
Now we can ask Lefthook to run our Bash script by adding this code to the lefthook.yml
file:
commit-msg: scripts: "template_checker": runner: bash # "template_checker.js": # We can also use JS file and reference it here # runner: node # It will be executed using node as a runner # USING DOCKER AS RUNNER # "docker_hook.js": # Similar file reference # runner: docker run -it --rm <container_id_or_name> {cmd} # Here {cmd} => command
Although parallel execution is an outstanding feature, Lefthook also supports piped execution of commands. If task execution requires the piping of functions, and any command in the sequence fails, Lefthook will not execute others, ensuring smooth completion of tasks.
This can be super helpful for tasks that require database setup before execution. Here is a small example:
database: piped: true commandy 1_create: run: rake db:create 2_migrate: run: rake db:migrate 3_seed: run: rake db:seed
Configuration setup in Lefthook is not just limited to local and project levels; we can also extend configs.
Inheritance in configuration helps to ensure the product or company-wide synchronization of commit messages, security audits, and other checks. Here is how to extend any other configuration:
extends: - ~/unicorns/configs/lefthook-extend-2.yml
Don’t worry if the above examples are not intuitive for you as a React or React Native developer; the following is a demonstration of how to utilize Lefthook in your app.
We will begin with installing Lefthook:
npm install @arkweid/lefthook --save-dev # or yarn add @arkweid/lefthook -D
Once Lefthook is installed, you will see lefthook.yml
at the root of your project; this is the file that will hold all of our jobs and hooks. Remember, this is a dev dependency and a binding to a locally or globally installed Lefthook binary. It is freeing you from a nightmare of dependencies.
This section assumes your app has Husky integrated; if that is not the case for you, please move to the next section.
Start from uninstalling Husky from your project:
npm uninstall husky # or yarn remove husky
Once uninstalled, you can remove the .husky
directory from the project, or its declaration from package.json
depending upon your implementation.
Next up, let’s reset Git Hooks back to the default:
git config --unset core.hooksPath rm .git/hooks/pre-commit rm .git/hooks/pre-push
Now, move the Husky implementation to lefthook.yml
:
pre-commit: commands: sometest: run: npm lint pre-push: commands: anothertest: run: npm audit
Let’s add a pre-commit hook that runs quickly and in parallel to make sure our app’s code is always fit to be merged when the developer commits it. Make sure you have TypeScript enabled in your app.
Because we have already installed Lefthook (if you skipped to this section from above, review the previous code for help in installing), let’s start by adding a pre-commit hook for our app. We have divided our pre-commit hook into four tasks, all of which can run in parallel:
Here is our pre-commit hook, with all four tasks running in parallel:
pre-commit: parallel: true commands: type-check: glob: '*.{ts,tsx}' run: yarn typecheck eslint: glob: '*.{js,ts,jsx,tsx}' run: yarn lint:eslint:fix {staged_files} stylelint: glob: '*.{js,ts,jsx,tsx}' # For CSS-in-JS run: yarn lint:style {staged_files} spelling: glob: '*.{js,ts,jsx,tsx,md}' run: yarn cspell {staged_files} markdown-link-check: glob: '*.md' run: npx markdown-link-check {staged_files}
As you can see, we have leveraged Lefthook’s capability to select a chunk of files for job execution. Selection of files depends on the task, e.g., type check has to be performed across the app to ensure type safety while lint, spell checks, and link checks are good with staged files only.
Next up is the commit message hook. This hook saves us from the hassle of managing change logs and the readability of commit messages.
We are using commitlint here, but you can always use another library or your custom scripts with runners, as mentioned previously. Here is the code for our commit-msg
hook looks:
commit-msg: commands: parallel: true lint-commit-msg: run: npx commitlint --edit spell-check: run: yarn cspell --no-summary {1}
Now, it’s time to ensure that whenever code is pushed in our repository, it’s well tested for quality and security. Just like our pre-commit hook, these tasks can be performed in parallel. Here is our pre-push hook:
pre-push: parallel: true commands: test: run: yarn test packages-audit: run: yarn audit
In my opinion, Lefthook is more potent than other Git Hook management libraries while giving us more freedom and simplicity at the same time. With parallelization, tasks are executed faster, and with support of custom runners, Lefthook is a good choice for use in CI/CD environments. Inheritance of configs can be super helpful for enforcing common coding practices across teams.
If you’re impressed with Lefthook and looking to migrate from Husky, here is the migration guide.
I have also prepared a gist to add hooks discussed here to your React and React Native apps. Give it a try!
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]