npm and Advanced Modules
Advanced modules and library usage
One of the wonderful things about the modern JavaScript ecosystem is that there are literally thousands of libraries for us to use, solving any number of complex problems we might encounter in the realm of web development. Learning how to install, manage, and import
these third-party dependencies is a key part of being an effective web developer.
More Modules
You'll recall that we've been working with our own library of components through import
-ing and export
-ing default
chunks of JavaScript. But there are a few variations on this these syntaxes that will help us keep our code organized as our applications begin to pull in third-party dependencies. Here's a quick overview:
// import the default export from a module
import toolbox from 'toolbox';
// import a single export (of many) from a module
import { tool } from 'toolbox';
// import many exports at once from a module
import * from 'toolbox';
// import many exports as a named Object from a module
import * as tools from 'toolbox';
// export a single, default chunk from a module
export default 'toolbox';
// export a single chunk (of many) from a module
export 'tool';
Let's see how these import
/export
variations might help us organize our large state and content trees in our portfolio projects.
Portfolio Project 1
Modular State and Content
When last we left our state tree, it was beginning to look something like this:
const states = {
Home: {
links: ["Blog", "Contact", "Projects"],
title: "Welcome to my Portfolio"
},
Blog: {
links: ["Home", "Contact", "Projects"],
title: "Welcome to my Blog"
},
Contact: {
links: ["Home", "Blog", "Projects"],
title: "Contact Me"
},
Projects: {
links: [
"Home",
"Blog",
"Contact",
"Choose-Your-Own-Adventure",
"Rock-Paper-Scissors"
],
title: "Check out some of my projects"
}
};
This is pretty ungainly. Let's see if we can clean this up by extracting all of these disparate states into their own modules.
To start, let's create a directory called
store
that will contain all of ourstate
s. Inside thatstore
directory, create a singleindex.js
file. This file will act similarly to the way theindex.html
does for HTML documents: our bundler will, by default, look for thatindex
if no explicit file path is provided.Inside of
store
, let's create a separate JavaScript file for each part of our state tree, That means newHome
,Blog
,Contact
, andProjects
files. Each of these files should have a singledefault
export of a single Object. That Object should represent that single piece of the original state tree. So, for example, theBlog
module would look like this:export default { links: ["Home", "Contact", "Projects"], title: "Welcome to my Blog" };
Once every state module has been created, we can re-export each of those modules with a name in
store/index.js
. The syntax for one of those re-exports would beexport { default as SomeName } from './some-location';
. See if you can re-export all of these pieces of our state tree with this syntax! HINT:export { default as Blog } from "./Blog"; export { default as Contact } from "./Contact"; export { default as Home } from "./Home"; export { default as Projects } from "./Projects";
Now that this base module has been set up we should be able to
import
our states through the module system. In this case, we would like toimport
all of the states in one big glob using the* as name
syntax. Try the following in your root-levelindex.js
file, and see if you can figure out what the data type ofstates
will be:import * as states from "./store"; console.log(states); // what's the data type here?
We should now be able to access any piece of our
states
tree, just as before! Except that this time, all of the complexity of our application state is hidden away behind our module system (which is a good thing).Now that we've ironed out navigation, let's see if we can finally get our content to change in response to our user input! You should still have HTML files that represent content for your
Blog
,Content
, andProjects
landing pages that we haven't yet incorporated into our new Single-Page architecture. Let's convert those pieces of HTML into their owncomponent
s. To start, let's create aPages
directory inside ourcomponents
directory. This is a common pattern when dealing with a group of similar components. In this case, we're grouping all of the different page-level (or content-level) templates.Let's repeat the export pattern from our
states
. That means creating anindex.js
file next to aBlog.js
,Contact.js
,Home.js
, andProjects.js
file inside ofcomponents/Pages
. Each Page component shouldexport
some HTML as a template literal, e.g.:export default ` <form> <input type="text" name="test"> <input type="submit"> </form> `;
...and re-
export
thosedefault
s with a name fromcomponents/Pages/index.js
, e.g.:export { default as Contact } from './Contact';
Now each
state
component can point to a component from thecomponents/Pages
directory. Let's use a String to connect that content to each state component as abody
property. For example, ourBlog
state might become:export default { body: "Blog", links: ["Home", "Contact", "Projects"], title: "Welcome to my Blog" };
Once you've associated some content with each piece of the state tree, it's time to change our
Content
component to allow for variation in thebody
property of astate
parameter. That means ourContent
component becomes:import * as pages from "./Pages"; export default function Content(state) { return ` <div id="content"> ${pages[state.body]} // why do we need square brackets? </div> `; }
Now you should have a Single-Page application that behaves almost exactly like our old HTML page-based site, but with a lot more flexibility and some real performance wins for our users.
Dependencies
We've already worked with "dependencies" during our time with CSS. A dependency is any piece of code (regardless of language) provided by a third-party upon which our project depends. Previous dependencies were included through <link>
tags in our HTML files, and were retrieved from a Content Delivery Network (e.g. CDNJS). These dependencies included things like CSS reset libraries (normalize.css), fonts, and icons.
In the realm of JavaScript, the number of possible dependencies is much larger than we've encountered in CSS-land. While we could include many third-party libraries from a CDN using a <script>
tag, there are a couple of good reasons not to:
It's easier to update third-party dependencies through a dedicated dependency management system instead of keeping track of those dependencies by hand. JavaScript's dependency management program is called
npm
.As your application gets more complex, it's not uncommon to have more than 10 JavaScript dependencies, many of which will need to be loaded in a certain order to work correctly. That's a lot of
<script>
tags to keep track of! Instead, we caninstall
dependencies withnpm
andimport
those dependencies into a single bundle.Unlike with CSS, it's rare to use every piece of a JavaScript library. With our module bundler, we can use a process called tree-shaking to get rid of un-unsed parts of our dependencies. This means that our users don't need to download nearly as many bytes of data as they would if the entire dependency were included.
Let's go through two practical examples of using third-party dependencies to improve navigation for our users!
lodash
lodash
lodash
is a library of useful utility functions that make working with collections of data much easier. On top of the collections helpers, though, there are a number of functions that make working with Strings just a bit easier.
Portfolio Project 2
lowercased routes
Install
lodash
by typing the following into your terminal:npm install --save lodash
And just like that, we should be able to
import
individual helper functions in any of our own modules. Try adding this to the top ofNavigation.js
:import { lowerCase } from "lodash";
lowerCase
is a function that turns any String into its lower-cased variant. Since lower-case routes are much more common than the upper-case routes that we've been using so far, let's turn ourbuildLinks
function into something that looks like this instead:import { lowerCase } from "lodash"; function buildLinks(linkArray) { let i = 0; let links = ""; let link = ""; while (i < linkArray.length) { link = lowerCase(linkArray[i]); links += ` <li> <a href='/${link}'>${linkArray[i]}</a> </li> `; i++; } return links; }
Along with the changes to
buildLinks
, we'll also need to capitalize our component names inhandleNavigation
in ourindex.js
file. That's as simple as addingimport { capitalize } from 'lodash';
to the top of the file before modifyinghandleNavigation
to be:function handleNavigation(event) { const component = event.target.textContent; event.preventDefault(); startApp(state[capitalize(component)]); }
Not a bad way to offload some of that complexity to a library maintainer, right?
Routing and navigo
navigo
While our SPA has some nice performance benefits for users (once our JavaScript has loaded) and some nice features during development (from automatic re-bundling and hot-module reloading), there's a major drawback for those visiting our application for the first time: as written, there's no way to navigate directly to any "page" other than the landing page/Home
component. That's because our JavaScript application no longer differentiates between any URL paths; localhost:1234
is treated the same as localhost:1234/blog
and localhost:1234/whatever
. To capture these URLs and treat them correctly, we need a client-side router that listens for changes to the URL and responds with our startApp
function called with the correct state
.
It's very possible (and a fun bonus exercise) to use window.location.pathname
to create a very basic client-side router, but we can make things easier on ourselves by leveraging the work of others. For this project, we'll use navigo.
Portfolio Project 3
Routing with navigo
navigo
Let's start by installing the
navigo
library withnpm install --save navigo
We should now be able to import the default
Navigo
constructor at the top of ourindex.js
file with:import Navigo from "navigo";
Navigo
is a special type of function that can be used to create a new Object (more on these later). To create therouter
Object that we'll use to route requests, create arouter
variable like so:// origin is required to help our router handle localhost addresses const router = new Navigo(window.location.origin);
router
works by chaining a number of different functions together (more on this idea of chaining functions later, too). The two that we'll use areon
andresolve
.on
uses a callback structure: whenever a URL matches the pattern given toon
as its first argument, the function provided as the second argument is called. We useresolve
at the end of the chain to kick off the client-side routing process. Try this onindex.js
:router.on("/", () => console.log("hello home page!")).resolve();
You should see
hello home page!
whenever you visit the landing page of your application.on
also allows us to capture routes as something called params (short for parameters) using a special syntax (but one that's common among routers of all types). Try experimenting with this setup:router .on(":page", params => console.log(params.page)) .on("/", () => console.log("hello home page!")) .resolve();
Now we have the ability to execute JavaScript in response to the URL. But what is it we actually want to do? Previously, we were using
handleNavigation
to navigate around through click events targets. Now, we can handle routes a bit more fluidly. Let's refactorhandleNavigation
into ahandleRoute
function that looks something like this:function handleRoute(params) { const page = capitalize(params.page); startApp(states[page]); }
...which we can apply to the
:pages
route as a callback, like so:router .on("/:path", handleRoute) .on("/", () => console.log("hello home page!")) .resolve();
We should now be able to use the URL to navigate between application states... pretty cool! But we're not done yet. What if we want to be able to navigate to different URLs using the links in our
Navigation
component? Right now, we're hijacking that process with custom event listeners, so our URL isn't changing at all. Luckily,navigo
gives us a helper function to let us handle those anchor tags without reloading the page (and without the custom event listeners). Let's start by refactoring yourstartApp
function to something a bit more simple:function startApp(state) { rooter.innerHTML = ` ${Navigation(state)} ${Header(state)} ${Content(state)} ${Footer(state)} `; router.updatePageLinks(); // much simpler! }
Now we can add a special attribute to our generated links called
data-navigo
to allow ourrouter
to "hijack" these anchor tags. That would mean that ourbuildLinks
function inNavigation
is as simple as:function buildLinks(linkArray) { let i = 0; let links = ""; let link = ""; while (i < linkArray.length) { link = lowerCase(link); links += ` <li> <a href='/${link}' data-navigo> // new attribute ${linkArray[i]} </a> </li> `; i++; } return links; }
Now most of our links should be navigable using client-side routing!
You may have noticed that "most" from above. What's the bug? You'll notice that we're currently handling our landing page (the
/
route) with aconsole.log
statement. What we'd really like to do is start our application with theHome
branch of thestate
tree. So let's modify our routing to account for users navigating to our landing page:router .on("/:page", handleRoute) .on("/", () => startApp(states["Home"])) .resolve();
Almost done now! The last bug: you'll notice that any
Home
link actually links to/home
on click. Instead of doing that, let's route allHome
links to/
, which is our actual home page. We'll do that over inNavigation.js
'sbuildLinks
function by doing a quick refactor to:function buildLinks(linkArray) { let i = 0; let links = ""; let link = ""; while (i < linkArray.length) { if (linkArray[i] !== "Home") { link = linkArray[i]; } // what's the value of link here? links += ` <li> <a href='/${lowerCase(link)}' data-navigo> ${linkArray[i]} </a> </li> `; i++; } return links; }
And there you have it! Now you have a fully-operational SPA, ready for your users with lightning-fast navigation.
Last updated
Was this helpful?