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.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:
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.
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)}
/>
Here, for example, is the logic that I implemented for searching headings in this blog post:
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;
}
}
}
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.
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;
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,
}))
);
}, []);
The searcher returned from the useInPageSearcher
hook will be passed to
the BaseCommandBar
component, as seen in the first snippet.
Please make sure that the Bit binary is installed on your machine:
npx @teambit/bvm install
The process of composing a search bar in your app is as simple as:
Fork the blogs'
component and implement your own logic.Tag the component with its first version:
Export the component.
Enjoy the search bar in your app and/or share it with the entire world. 🥳
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.