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.
Please make sure that the Bit binary is installed on your machine:
npx @teambit/bvm install
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:
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:
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:
If you don't feel like following the next steps, you can also use the fork command:
It's time to write some code! Here's the implementation of our to-date
util function:
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);
}
}
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.tsimport 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));
});
});
});
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.tsimport React from 'react';
import { toDate } from './to-date';
export function ReturnsCorrectValue() {
return <div>{toDate(new Date()).toString()}</div>;
}
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>
'''
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.
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
Let's publish our component so that it can be used by other developers:
We will create another workspace in a different directory to simulate collaboration between teammates:
In this Workspace, we are going to create another component called difference-in-milliseconds
:
Forking my component is fine if you're lazy:
Let's look at the implementation of this component:
difference-in-milliseconds.tsimport { 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();
}
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:
Now it's time to quickly construct our component tests documentation and compositions:
Tests:
difference-in-milliseconds.spec.tsimport 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);
});
});
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>
'''
And a composition:
difference-in-milliseconds.composition.tsimport 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>
);
}
The only thing left is to tag it:
There we have another independent utility function component :)
new components
(first version for components)
> date-fns/difference-in-milliseconds@0.0.1
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:
We are able to modify the toDate
function. We recently heard about that Internet Explorer is dead. So, let's remove its support:
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);
}
}
Your Workspace should now look like this:
Now we can tag the toDate
component, and Bit will auto-tag its dependents:
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
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.
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!