Add interactivity to your CLIs with React
Tags: cli, react, javascript, tutorial
Published: 2019-10-30 13:59:39

---
title: Add interactivity to your CLIs with React
published: true
date: 2019-10-30 13:00:47 UTC
tags: cli,react,javascript,tutorial
canonical_url: https://blog.logrocket.com/add-interactivity-to-your-clis-with-react/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/64qwmfw5haoeqdfqivxu.png
---
**Written by [Vadim Demedes](https://blog.logrocket.com/author/vadim-demedes/)**✏️

Command-line interfaces (or CLIs) are becoming more and more interactive lately. They accept user input, render many variations of user interfaces, have improved designs, and are generally more pleasant to work with these days.

Thanks to these advancements, CLIs are no longer reserved for hardcore programmers. More people have started using CLI over graphical user interfaces.

Take Jest for example: it offers a user-friendly interactive watch mode that eliminates the need for any GUI. However, creating such a CLI is not an easy task.

Rendering something to terminal involves basically joining multiple strings together and writing them to output stream.

This code can be hard to test, because it doesn’t usually decouple from the rest of the codebase well.

Interactivity is another headache, since tools mostly solve a specific problem, such as asking a user a question and letting them select an answer from a list of questions.

What if we had a platform for building interactive command-line applications that would provide us with the building blocks for creating any user interface imaginable?

Better yet, what if those building blocks were components, too?

That’s where [React](https://reactjs.org/) and [Ink](https://github.com/vadimdemedes/ink) come in.

React lets you structure terminal output in components in the same way you’d build an app for a browser.

Ink takes these components and renders them to terminal, relieving you from the burden of assembling terminal output manually.

Here’s how easy it is to create a simple counter with Ink in terminal:

```jsx
const React = require('react');
const {render, Text, Color} = require('ink');
const useAutoCounter = require('@use-it/auto-counter');

const Counter = () => {
  // Count from zero to infinity every 100ms
  const count = useAutoCounter(0, 100);

  return (
    <Text>
      Count: <Color green>{count}</Color>
    </Text>
  );
};

render(<Counter/>);
```

[Here’s an example of the auto-counter in action.](https://blog.logrocket.com/wp-content/uploads/2019/10/Adding-interactivity-to-React-1.mov)

It’s important to note that Ink not only renders components like `Text`, `Color` and `Box`, but also allows you to use Flexbox to lay out these components almost as you would in the browser with CSS.

Thanks to React and Ink, you can import 3rd-party components and mix and match them in your terminal application.

This was not easily achievable before. There are all kinds of components you can reuse, from [text inputs](https://github.com/vadimdemedes/ink-text-input), [selectable lists](https://github.com/vadimdemedes/ink-select-input), [spinners](https://github.com/vadimdemedes/ink-spinner) and [progress bars](https://github.com/brigand/ink-progress-bar) to [links](https://github.com/sindresorhus/ink-link) (yes, links in the terminal), and [images](https://github.com/kevva/ink-image) (yes, images in the terminal!)

Companies like [Gatsby](https://www.gatsbyjs.org/) and [Shopify](https://www.shopify.com/)), as well as popular projects such as [Parcel](https://parceljs.org/) and [Tap](https://node-tap.org/), are already using Ink for building and rendering the user interface of their CLIs.

Let’s find out why.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

### Create a CLI with React and Ink

Now, let’s experience the magic first-hand and create our own command-line application with React and Ink.

We’re going to build a tiny utility that searches for files in the current directory and then opens it in the user’s favorite code editor.

[At the end of this tutorial we’ll have an interactive CLI that looks like this.](https://blog.logrocket.com/wp-content/uploads/2019/10/Completed-CLI.mov)

Here’s how it’s going to work:

1. User enters a search query to compare file names to those in the current directory
2. Search results are listed as user types, allowing them to select which file they want to open in their code editor

### Getting Started

Let’s skip the boilerplate and use [create-ink-app](https://github.com/vadimdemedes/create-ink-app), which scaffolds a new CLI with React and Ink pre-configured so we can start building our tool right away. You may have heard of `create-react-app`, which does the same job but for React apps in the browser.

Create a new directory called `search` (this will be the name of our CLI) and run `create-ink-app` inside it:

```jsx
$ mkdir search
$ cd search
$ npx create-ink-app
```

It may take `create-ink-app` a few minutes to install all dependencies for you. Once it’s finished, you should see the following output:

![An example to illustrate how to add interactivity to your CLIs in React](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/10/Projects-logrocket-search.png?resize=730%2C486&ssl=1)

At this point, `search` CLI is already available in your environment and you can try running it:

```jsx
$ search
```

If all is well, you should see a “Hello, Stranger” message, where the word “Stranger” is green.

If you take a look at the `search` directory, you’ll find `cli.js` and `ui.js` files. For the sake of simplifying this tutorial, we’re going to be working only with `ui.js`, since that’s where the user interface of our CLI is implemented.

Replace the source code of `ui.js` that `create-ink-app` scaffolded for us with this so that we can start from scratch:

```jsx
'use strict';
    const React = require('react');
    const {Box} = require('ink');
    
    const App = () => {
      return (
        <Box/>
      );
    };
    
    module.exports = App;
```

If you run it, nothing will be rendered, because `App` returns an empty `Box` component. The `Box` component is used as a container for other components like `Text` or `Color`.

Every `Box` component aligns its children horizontally, similar to a `div` element with `display: flex` applied in a browser.

### Add a search input

The first thing that our CLI needs to do is accept and handle user input.

We need to install `ink-text-input` component to implement that easily, which already handles things like cursor management and recording of user input.

```jsx
$ npm install ink-text-input
```

Now, we’ll add a `TextInput` component to our CLI.

`TextInput` component doesn’t handle input state, so we need to pass the `value` property with a current input value and an `onChange` handler to update it when the user enters more characters.

Text that you pass via the `placeholder` property will be displayed when the user hasn’t entered any text yet.

```jsx
'use strict';
const React = require('react');
const {Box} = require('ink');
const TextInput = require('ink-text-input').default;

const App = () => {
  const [value, setValue] = React.useState('');

  return (
    <Box>
      <TextInput
        placeholder="Enter your search query..."
        value={value}
        onChange={setValue}
      />
    </Box>
  );
};

module.exports = App;
```

If you try running the `search` command in the terminal now, you should see an “Enter your search query…” text and be able to type any text you want.

You should also be able to use a cursor to move left and right inside the text value you’ve entered.

Let’s rename `value` and `setValue` to `searchQuery` and `setSearchQuery` respectively to better reflect what this variable is for.

We’ll also use the `Color` component to add a nice green arrow before text input to highlight it better:

```jsx
'use strict';
const React = require('react');
const {Box, Color} = require('ink');
const TextInput = require('ink-text-input').default;

const App = () => {
  const [searchQuery, setSearchQuery] = React.useState('');

  return (
    <Box>
      <Color green>❯ </Color>
      <TextInput
        placeholder="Enter your search query..."
        value={searchQuery}
        onChange={setSearchQuery}
      />
    </Box>
  );
};

module.exports = App;
```

If you run the `search` command again, you should see a nice green arrow before the text you’re typing.

### Search current directory for matching files

Now, let’s add an actual searching functionality.

As the user types their query, we’re going to search the entire directory for files that match that query.

To do this, we’re going to install two new modules – [globby](https://github.com/sindresorhus/globby) and [fuzzaldrin](https://github.com/atom/fuzzaldrin). Globby returns a list of all files in the current directory while fuzzaldrin searches this list to see if any file names match the search query.

```jsx
$ npm install globby fuzzaldrin
```

To speed up the search, we only need to scan the directory once and then just keep filtering it with different search queries.

To do that, we’re going to use a `useEffect` hook with an empty list of dependencies, which makes it execute only once after the component is first rendered.

We’re also going to create a new state variable via `useState` called `files`, where we’ll store the list of all files.

```jsx
'use strict';
const React = require('react');
const {Box, Color} = require('ink');
const TextInput = require('ink-text-input').default;
const globby = require('globby');

const App = () => {
  const [searchQuery, setSearchQuery] = React.useState('');
  const [files, setFiles] = React.useState([]);

  React.useEffect(() => {
    globby(['**/*', '!node_modules']).then(files => {
      setFiles(files);
    });
  });

  return (
    <Box>
      <Color green>❯ </Color>
      <TextInput
        placeholder="Enter your search query..."
        value={searchQuery}
        onChange={setSearchQuery}
      />
    </Box>
  );
};

module.exports = App;
```

There are two patterns passed to `globby` and each of them does the following:

- `**/*` – signals to search all directories recursively (find all nested files)
- `!node_modules` -signals to ignore `node_modules` directory, since you wouldn’t normally want to search inside dependencies

Next, since we already store the current search query in `searchQuery`, we can immediately filter the `files` array with `fuzzaldrin` to get our search results.

Fuzzaldrin is fast enough that we can execute it on every render.

```jsx
'use strict';
const React = require('react');
const {Box, Color} = require('ink');
const TextInput = require('ink-text-input').default;
const globby = require('globby');
const fuzzaldrin = require('fuzzaldrin');

const App = () => {
  const [searchQuery, setSearchQuery] = React.useState('');
  const [files, setFiles] = React.useState([]);

  React.useEffect(() => {
    globby(['**/*', '!node_modules']).then(files => {
      setFiles(files);
    });
  });

  const searchResults = fuzzaldrin.filter(files, searchQuery);

  return (
    <Box>
      <Color green>❯ </Color>
      <TextInput
        placeholder="Enter your search query..."
        value={searchQuery}
        onChange={setSearchQuery}
      />
    </Box>
  );
};

module.exports = App;
```

### Display a list of results

Now, we need to display the list of results and allow the user of our CLI to select the file they want.

We can use the `ink-select-input` module, which provides a `SelectInput` component that implements a list of choices. To install this component, run the following command:

```jsx
$ npm install ink-select-input
```

Next, we’ll modify our `ui.js` file by adding a `SelectInput` component and a list of choices.

There’s one step in-between, though.

In our current code, `searchResults` is an array of strings where each item is a file path. However, `SelectInput` expects an array of objects where each object looks like this:

```jsx
{
  label: "Label of item, displayed to the user",
  value: "Internal value, not displayed to the user"
}
```

To pass along a list of items that `SelectInput` understands, we’ll need to update the `searchResults` array to contain objects like these:

```jsx
'use strict';
const React = require('react');
const {Box, Color} = require('ink');
const TextInput = require('ink-text-input').default;
const SelectInput = require('ink-select-input').default;
const globby = require('globby');
const fuzzaldrin = require('fuzzaldrin');

const App = () => {
  const [searchQuery, setSearchQuery] = React.useState('');
  const [files, setFiles] = React.useState([]);

  React.useEffect(() => {
    globby(['**/*', '!node_modules']).then(files => {
      setFiles(files);
    });
  });

  const searchResults = fuzzaldrin.filter(files, searchQuery).map(file => ({
    label: file,
    value: file
  }));

  return (
    <Box flexDirection="column">
      <Box>
        <Color green>❯ </Color>
        <TextInput
          placeholder="Enter your search query..."
          value={searchQuery}
          onChange={setSearchQuery}
        />
      </Box>
      
      <SelectInput
        limit={5}
        items={searchResults}
      />
    </Box>
  );
};

module.exports = App;
```

Note that I’ve also added a `limit={5}` property so the list only displays a maximum of 5 items at a time.

If the number of items is actually bigger than 5, the list will become scrollable. How cool is that?

You may have also noticed that I wrapped the entire output in `<Box flexDirection="column">`.

I did this to position search results below search input. Otherwise they would all be displayed horizontally, side by side.

Let’s see how our CLI looks! Run the `search` command and you should see something like this: ![LogRocket search Master](https://blog.logrocket.com/wp-content/uploads/2019/10/logrocket-search-master-nocdn.png)

By default, it displays all files in the current directory. You can move up and down the list with your arrow keys.

If you enter a search query, it should filter the list with files that only match the search pattern.

### Open selected file in a code editor

Now let’s add the last missing piece — selecting files and opening them in the user’s preferred code editor. We will need to install an `open-editor` dependency, which does this for us.

```jsx
$ npm install open-editor
```

Before we start, it’s important to check if you have a preferred code editor set in the first place.

You can verify this by running the following command in the terminal:

```jsx
$ echo $EDITOR
```

If the command runs successfully, feel free to skip the next paragraph.

If it doesn’t output anything, that means you need to set a preferred code editor.

For the purposes of this tutorial, we’re going to set it only for the current terminal session.

However, it’s best to set it in your configuration file, like `.bashrc` or `.zshrc`. Here are few examples of how to select different code editors as preferred ones:

```jsx
# Set Atom as preferred editor
export EDITOR=atom

# Set Sublime Text as preferred editor
export EDITOR=subl

# Set Vim as preferred editor
export EDITOR=vim
```

Next, let’s add an `onSelect` handler to the `SelectInput` component. This handler will receive an item from the `searchResults` array, which we’ll use to open a file in the user’s editor.

```jsx
'use strict';
const React = require('react');
const {Box, Color} = require('ink');
const TextInput = require('ink-text-input').default;
const SelectInput = require('ink-select-input').default;
const globby = require('globby');
const fuzzaldrin = require('fuzzaldrin');
const openEditor = require('open-editor');

const App = () => {
  const [searchQuery, setSearchQuery] = React.useState('');
  const [files, setFiles] = React.useState([]);

  React.useEffect(() => {
    globby(['**/*', '!node_modules']).then(files => {
      setFiles(files);
    });
  });

  const searchResults = fuzzaldrin.filter(files, searchQuery).map(file => ({
    label: file,
    value: file
  }));

  const handleSelect = searchResult => {
    openEditor([
      {
        file: searchResult.value,
        line: 1,
        column: 1
      }
    ]);
  };

  return (
    <Box flexDirection="column">
      <Box>
        <Color green>❯ </Color>
        <TextInput
          placeholder="Enter your search query..."
          value={searchQuery}
          onChange={setSearchQuery}
        />
      </Box>

      <SelectInput
        limit={5}
        items={searchResults}
        onSelect={handleSelect}
      />
    </Box>
  );
};

module.exports = App;
```

Voila! Our very own command-line app with React and Ink is ready.

### Next steps

Try evolving this CLI further to strengthen your knowledge of Ink.

You can add a [loading spinner](https://github.com/vadimdemedes/ink-spinner) while the file list is loading, let users select [multiple files](https://github.com/karaggeorge/ink-multi-select), add a [big fancy logo](https://github.com/sindresorhus/ink-big-text), or do just about anything that comes to mind.

### Conclusion

As you can see, building interactive CLIs isn’t that scary when you have the right tools on hand.

With the superior capabilities of React, it’s easier than ever to split your user interface into components.

It’s important to remember that React’s scope is far beyond the applications that live in the browser.

People use React to build mobile, VR, and now terminal applications. I’m sure we’ll see even more environments where React’s methodology shines.

Don’t stop your curiosity train.
* * * 
**Editor's note:** Seeing something wrong with this post? You can find the correct version [here](https://blog.logrocket.com/add-interactivity-to-your-clis-with-react/).

## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *
The post [Add interactivity to your CLIs with React](https://blog.logrocket.com/add-interactivity-to-your-clis-with-react/) appeared first on [LogRocket Blog](https://blog.logrocket.com).