Sharing JavaScript Utility Functions Across Projects

ni
nitsan7702 years ago

So you've built that awesome utility function and put in a few hours on it. Some months later, you realized you needed the same function for another project.

What will you do then? Naive approach is to copy and paste the code from your previous project. However, will this solution scale? What if you weren't the one needing this function? Imagine that it was your teammate (think of all those functions that were remade over and over again)

What if I told you that this utility function can be created only once and can be reused across multiple projects? In addition, whenever you update this function, all the places where it appears will be updated accordingly. Furthermore, anyone in the world (or in your organization if you restrict permissions) can easily discover and use this function.

How cool is that? Let's find out how!

The util functions in this guide are taken from the incredible util function library - date-fns.

Let's build some date utilities!

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

npx @teambit/bvm install
CopiedCopy

This guide assumes that you have a bit.cloud account and know how to open a remote scope.

Our goal is to use Bit to build some date utilities that can be reused across different projects, as indicated by the title.

Below is an example of a date utility function we're going to build (check out the code tab to see how it works):

The first step is to create a Bit Workspace. A Workspace is a development and staging area for components. Here, we create new components, retrieve and modify existing ones, and compose them together.

Bit’s workspace is where you develop and compose independent components. Components are not coupled to a workspace; you can dynamically create, fetch, and export components from a workspace and only work on them without setup.

Each component is a standalone “mini-project” and package, with its own codebase and version. The workspace makes it easy to develop, compose, and manage many components with a great dev experience.

You can develop all types of components in the workspace, which is tech-agnostic. A workspace can be used as a temporary, disposable context for components, or as a long-term workshop.

Without further ado, let's create a Workspace. In the designated directory, run the following command:

$bit
Copiedcopy

In the root of your workspace, two files and one directory will be created:

  • workspace.jsonc: This file contains the configuration of the Workspace.
  • .bitmap: Here Bit keeps track of which components are in your workspace. You shouldn't edit this file directly.
  • .bit: This is where the component's objects are stored.

Here is how the Workspace looks like:

MY-WORKSPACE
.bit
.bitmap
workspace.jsonc

Please ensure you replace "defaultScope": "[your-bit-cloud-username].[scope-name]" with your Bit Cloud user name and the scope name.

We are ready to create our first utility function component!

In your Workspace folder, run the following command:

$bit
Copiedcopy

If you don't feel like following the next steps, you can also use the fork command:

$bit
Copiedcopy

It's time to write some code! Here's the implementation of our to-date util function:

to-date.ts
export function toDate(argument: Date | number): Date {
  const argStr = Object.prototype.toString.call(argument);

  // Clone the date
  if (
    argument instanceof Date ||
    (typeof argument === 'object' && argStr === '[object Date]')
  ) {
    // Prevent the date to lose the milliseconds when passed to new Date() in IE10
    return new Date(argument.getTime());
  } else if (typeof argument === 'number' || argStr === '[object Number]') {
    return new Date(argument);
  } else {
    if (
      (typeof argument === 'string' || argStr === '[object String]') &&
      typeof console !== 'undefined'
    ) {
      console.warn(
        "Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use `parseISO` to parse strings. See: https://github.com/date-fns/date-fns/blob/master/docs/upgradeGuide.md#string-arguments"
      );
      console.warn(new Error().stack);
    }
    return new Date(NaN);
  }
}
CopiedCopy

It's a very simple helper function that converts a date or a number to a date object (using the Date constructor).

The next step is to write some unit tests since a well-tested component is a good component:

to-date.spec.ts
import assert from 'assert';
import { toDate } from './to-date';

describe('toDate', () => {
  describe('date argument', () => {
    it('returns a clone of the given date', () => {
      const date = new Date(2016, 0, 1);
      const dateClone = toDate(date);
      dateClone.setFullYear(2015);
      assert.deepStrictEqual(date, new Date(2016, 0, 1));
    });
  });
  describe('timestamp argument', () => {
    it('creates a date from the timestamp', () => {
      const timestamp = new Date(2016, 0, 1, 23, 30, 45, 123).getTime();
      const result = toDate(timestamp);
      assert.deepStrictEqual(result, new Date(2016, 0, 1, 23, 30, 45, 123));
    });
  });
});
CopiedCopy

Let's create a very basic composition. Compositions depict how a component is used in real life. We may also wish to compose a component with another component to simulate real-life use.

to-date.composition.ts
import React from 'react';
import { toDate } from './to-date';

export function ReturnsCorrectValue() {
  return <div>{toDate(new Date()).toString()}</div>;
}
CopiedCopy

Finally we would want to write some docs so that whoever consumes our component can understand what it does:

to-date.docs.ts
---
labels: ['to date', 'date convertor', 'date utils']
description: 'A util functgion that converts numbers(timestamp) to a date object'
---

import toDate from './to-date';

You can use this function to convert a timestamp to a date object.

'''tsx live

<div>{toDate(1656584694000).toString()}</div>
'''

This function also makes sure you don't get a `NaN` date.

'''tsx live

<div>{toDate('string').toString()}</div>
'''
CopiedCopy

Bit's documentation uses MDX, so you can use the tsx live syntax to render the code in the documentation, which can be very useful for developers.

The last step is to tag and export our component.

$bit
Copiedcopy

During the tag phase, Bit will run the components tests and build the dist folder, artifacts(including a consumable package) according to the environment that has been specified. At the end of this phase, Bit will release a new version of your component:

new components
(first version for components)
     > date-fns/to-date@0.0.1
CopiedCopy

Let's publish our component so that it can be used by other developers:

$bit
Copiedcopy

Let's build some more date utilities now!

We will create another workspace in a different directory to simulate collaboration between teammates:

$bit
Copiedcopy

In this Workspace, we are going to create another component called difference-in-milliseconds:

$bit
Copiedcopy

Forking my component is fine if you're lazy:

$bit
Copiedcopy

Let's look at the implementation of this component:

difference-in-milliseconds.ts
import { toDate } from '@nitsan770/shared-js-utils.date-fns.to-date';

export default function differenceInMilliseconds(
  dateLeft: Date | number,
  dateRight: Date | number
): number {
  return toDate(dateLeft).getTime() - toDate(dateRight).getTime();
}
CopiedCopy

A very simple function that returns the difference between two dates in milliseconds.

As you can see, this component has the to-date util function as a dependency. Let's install it:

$bit
Copiedcopy

Now it's time to quickly construct our component tests documentation and compositions:

Tests:

difference-in-milliseconds.spec.ts
import assert from 'assert';
import { differenceInMilliseconds } from './difference-in-milliseconds';

describe('differenceInMilliseconds', () => {
  it('returns the number of milliseconds between the given dates', () => {
    const result = differenceInMilliseconds(
      new Date(2014, 6 /* Jul */, 2, 12, 30, 20, 700),
      new Date(2014, 6 /* Jul */, 2, 12, 30, 20, 600)
    );
    assert(result === 100);
  });

  it('returns a negative number if the time value of the first date is smaller', () => {
    const result = differenceInMilliseconds(
      new Date(2014, 6 /* Jul */, 2, 12, 30, 20, 600),
      new Date(2014, 6 /* Jul */, 2, 12, 30, 20, 700)
    );
    assert(result === -100);
  });

  it('accepts timestamps', () => {
    const result = differenceInMilliseconds(
      new Date(2014, 8 /* Sep */, 5, 18, 30, 45, 500).getTime(),
      new Date(2014, 8 /* Sep */, 5, 18, 30, 45, 500).getTime()
    );
    assert(result === 0);
  });
});
CopiedCopy

Docs:

difference-in-milliseconds.docs.ts
---
labels: ['difference in miliseconds', 'time', 'date utils']
description: 'A util function that checks the difference between two dates and returns the resuls in miliseconds'
---

import { differenceInMilliseconds } from './difference-in-milliseconds';

A util function that checks the difference between two dates and returns the resuls in miliseconds

Pass in two dates and it will return the difference in miliseconds

'''tsx live

<div>
  The difference between the dates in milliseconds is:
  {differenceInMilliseconds(
    new Date(2014, 8 /* Sep */, 5, 18, 30, 45, 500).getTime(),
    new Date(2014, 8 /* Sep */, 5, 18, 30, 45, 500).getTime()
  ).toString()}
</div>
'''
CopiedCopy

And a composition:

difference-in-milliseconds.composition.ts
import React from 'react';
import { differenceInMilliseconds } from './difference-in-milliseconds';

export function ReturnsCorrectValue() {
  return (
    <div>
      {differenceInMilliseconds(
        new Date(2015, 8 /* Sep */, 5, 18, 30, 45, 500).getTime(),
        new Date(2014, 8 /* Sep */, 5, 18, 30, 45, 500).getTime()
      ).toString()}
    </div>
  );
}
CopiedCopy

The only thing left is to tag it:

$bit
Copiedcopy

There we have another independent utility function component :)

new components
(first version for components)
     > date-fns/difference-in-milliseconds@0.0.1
CopiedCopy

Modifying dependencies

Assume you had to refactor the dependency of the component - toDate. What will you do if you cannot access your friend's Workspace?

Here's one of my favorite Bit commands - bit import.

The bit import command adds a component from a remote scope to your workspace (as opposed to installing a component which only adds it as a package to your node_modules). This allows you to live edit the component while seeing how all dependents change as a result.

Let's import the to-date component:

$bit
Copiedcopy

We are able to modify the toDate function. We recently heard about that Internet Explorer is dead. So, let's remove its support:

to-date.ts
export function toDate(argument: Date | number): Date {
  const argStr = Object.prototype.toString.call(argument);

  // Clone the date
  if (
    argument instanceof Date ||
    (typeof argument === 'object' && argStr === '[object Date]')
  ) {
    return new Date(argument);
  } else if (typeof argument === 'number' || argStr === '[object Number]') {
    return new Date(argument);
  } else {
    if (
      (typeof argument === 'string' || argStr === '[object String]') &&
      typeof console !== 'undefined'
    ) {
      console.warn(
        "Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use `parseISO` to parse strings. See: https://github.com/date-fns/date-fns/blob/master/docs/upgradeGuide.md#string-arguments"
      );
      console.warn(new Error().stack);
    }
    return new Date(NaN);
  }
}
CopiedCopy

Your Workspace should now look like this:

MY-WORKSPACE
.bit
shared-js-utils
.bitmap
workspace.jsonc

Now we can tag the toDate component, and Bit will auto-tag its dependents:

$bit
Copiedcopy
changed components
(components that got a version bump) > nitsan770.shared-js-utils/date-fns/to-date@0.0.2
auto-tagged dependents:
date-fns/difference-in-milliseconds@0.0.2
CopiedCopy

No words can describe the joy I feel when I am able to quickly refactor my node_modules dependencies.

Both components are now independent and can be reused in any project (either the backend or the frontend!). Any package manager can be used to install components, but we recommend keeping on building components with Bit so the dependency graph is always intact.

Write once, use anywhere

In this blog post, we learned how to use Bit to create reusable utility function components. Its strength is that it lets you create components that can be incorporated into any project.

This way you don't write redundant code and since there is only one source of truth, you can easily update the code and see the changes in the dependents.

Additionally, we had a taste of collaboration and learned how to use Bit to share code with other developers.

As we saw, bit import lets us easily manipulate our dependencies and send the changes to the dependents, but this is just the tip of the iceberg when it comes to sharing and collaborating on components.

For a deeper understanding of Bit, please read Bit's official documentation. Feel free to join us on the Slack channel if you have any questions. Best of luck!