Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with mobile machine learning, React, React Native, and UI designing.

A deep dive into Lefthook for React and React Native

5 min read 1543

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.

What is Lefthook?

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.

Standout features

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.

Parallel execution

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.

Choose which files to check

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:

  1. Glob
    pre-commit:
      commands:
        lint:
          glob: "*.{js,ts,jsx,tsx}"
          run: yarn eslint
  2. Shorthand
    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
  3. Custom list
    pre-push:
      commands:
        frontend-style:
          files: git diff --name-only master # files with, diff with master.
          glob: "*.js"
          run: yarn stylelint {files}

Separate local configs

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.

We made a custom demo for .
No really. Click here to check it out.

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.

Scripts and runners

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 

Piped execution

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

Inheritance of configs

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

Implementing Lefthook in React/React Native

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.

Replacing Husky

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

Pre-commit hook

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:

  • Type checking
  • ESLint
  • Code spell checker
  • Markdown link check

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.

Commit message hook

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}

Pre-push hook

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 a nutshell

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!

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with mobile machine learning, React, React Native, and UI designing.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

Leave a Reply