Component Spotlight: Composable Search Bar

ni
nitsan7702 years ago
You can search this blog post headings anytime by hitting cmd/ctrl+shift+k._

The ability to search an app is a core feature of any application. It gets users to the resource they are looking for instantly without getting them frustrated.

Many companies build their own search functionality. If you have only one app, that might not sound like such a bad idea, but when your company scales and more apps are added, it is a terrible idea to repeat yourself so many times. Let's put it out simple - repeatedly rewrites of the same code will most likely lead to many bugs and ruin the consistency of your brand.

Inspired by the Spotlight search feature of macOS, we at Bit created a similar search bar (UI wise). Here it is:

Initially, it was created to search for components and commands in a Bit Workspace, but as we grew, we had to extend its ability to support more complex search queries. For instance, look at how the search functionality works in the following Bit component - the Bit.dev app. Click on the magnifier icon below:

As you can see, you can search for anything in the docs and navigate to it.

If you navigate to any Workspace UI either locally or on the cloud(such as the

you are currently reading) and hit cmd/ctrl+k, you'll be able to see the original purpose of the search functionality - to find the components you need.

While writing this post, we realized that it could also be great if we had the search functionality working on the blog you are currently browsing.

It only took a few minutes to create a

that would compose the original component. And trust me, we didn't copy and paste the code.

Write once, use anywhere

If you're not used to working with independent components, you might think that all of our apps exist in a Monorepo, and this is how we can reuse the same logic across all apps. While the Monorepo solution will work to some extent, it has a few disadvantages:

  • Monorepos are difficult to maintain and scale.
  • Monorepos require a lot of time to build and deploy.
  • Monorepos are not easy to test.

Instead, the

is just an independent component living in the explorer scope. It is versioned, built, and unit-tested independently with Bit.

The main benefit is that you can use the same search functionality across all your apps and keep a consistent brand and UX. The user browsing your apps may not even notice that they are switching between apps.

How does this magic happen?

The

is a headless search component that uses the Fuse.js library to Fuzzy Search anything.

You have to pass the searching logic to the search prop, and the rest is done for you. Occasionally, you want to control its visibility by passing a boolean to the visible prop.

<BaseCommandBar
  className={classnames(textColumn, styles.commandBar)}
  searcher={searcher.search}
  visible={visible}
  onVisibilityChange={(next) => setVisible(next)}
/>
CopiedCopy

Here, for example, is the logic that I implemented for searching headings in this blog post:

  1. The first step is to create the searcher.
export type Heading = {
  /** unique identifier for the heading */
  id: string;
  /** the searchable text */
  title: string;
  /**The DOM element */
  element: HTMLHeadingElement;
};

export class InPageSearcher extends Searcher<Heading, Heading> {
  constructor() {
    super({ searchKeys: ["title", "id"] });
  }

  override toSearchResult = ({ item }: FuzzySearchItem<Heading>) => {
    return {
      id: item.id,
      title: item.title,
      element: item.element,
      action: () => {
        item.element.scrollIntoView({ behavior: "smooth", block: "start" });
      },

      children: (
        <div className={styles.searchBoxItem}>
          <span className={classnames(ellipsis, styles.title)}>
            {item.title}
          </span>
        </div>
      ),
    };
  };

  override toSearchableItem(item: Heading) {
    return item;
  }
}


}
CopiedCopy

In the InPageSearcher class, we are overriding the toSearchResult and toSearchableItem methods. In the toSearchResult method, we determine what props we want on the search result, the action we want to perform when the user clicks on the search result, and the children we want to display as the search results.

  1. Next, we need to create the useInPageSearcher hook.
export function useInPageSearcher(Headings: Heading[]) {
  const searcherRef = useRef<InPageSearcher>();
  if (!searcherRef.current) searcherRef.current = new InPageSearcher();

  const searcher = searcherRef.current;
  searcher.update(Headings);

  return searcher;
CopiedCopy
  1. And finally, we can have to pass all heading on the page to the useInPageSearcher hook.
const [headings, setHeadings] = useState<Heading[]>();
const searcher = useInPageSearcher(headings || []);
const [visible, setVisible] = useState(false);

useHotkeys('command+shift+k, ctrl+shift+k', () => setVisible(true));

useEffect(() => {
  setHeadings(
    [...document.getElementsByTagName('h3')].map((element) => ({
      id: element.textContent,
      title: element.textContent,
      element: element,
    }))
  );
}, []);
CopiedCopy

The searcher returned from the useInPageSearcher hook will be passed to the BaseCommandBar component, as seen in the first snippet.

Can I make it work in my app?

Please make sure that the Bit binary is installed on your machine:

npx @teambit/bvm install
CopiedCopy

The process of composing a search bar in your app is as simple as:

  1. Fork the blogs'

    component and implement your own logic.

    $bit
    Copiedcopy
  2. Tag the component with its first version:

    $bit
    Copiedcopy
  3. Export the component.

    $bit
    Copiedcopy
  4. Enjoy the search bar in your app and/or share it with the entire world. 🥳

Conclusion

In this blog post, we have covered the basics of how to use the

but the main takeaway is that by using independent component, you can reuse the same logic across all of your apps.

You see, in the beginning, when Uri Kutner developed the

, he didn't think that one day this component would grow to serve all the applications in the world (ok, just at Bit. For now at least).

He just created the product that he was requested to build, and he was happy with it. But since every independent component is an asset, it was easy to refactor it into a more generic component that can be used in any app, and that is what he did.

This way, whenever anyone on our team builds a component, it becomes another piece of “Lego” in our box that we can all use to create new things while making it easy to collaborate and keep our users consistently superb.