Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Tuesday, May 31, 2022

Those HTML Attributes You Never Use

 

In January, Madison Kanna asked her Twitter followers:

My answer was easy: HTML. And I wasn’t being sarcastic or mocking in the least. Sure, I pretty much know which tags to use in which instances and how to keep my HTML mostly semantic and accessible.

But there is a whole bunch of lesser-used attributes that I was sure I’d forgotten about, and probably a whole bunch of attributes I didn’t even know existed. This post is the result of my research, and I hope you’ll find some of these useful to you, as you build HTML pages in the coming months.

The enterkeyhint Attribute For Virtual Keyboards

The enterkeyhint attribute is a global attribute that can be applied to form controls or elements that have contenteditable set to true. This attribute assists users on mobile devices that use a virtual on-screen keyboard.

<input type="text" enterkeyhint="done">

enterkeyhint accepts one of seven possible values that will determine what the user sees on his ‘enter’ key:

  • enter,
  • done,
  • go,
  • next,
  • previous,
  • search,
  • send.

You can see how these “hints” can be useful for the user. Is the user progressing through a series of actions? Are they submitting info? Are they saving a setting? Depending on what they’re doing, you can customize the hint to match your app’s needs.

You can try this one out by visiting the CodePen demo below on a mobile device.

On my iOS device, the text for the enter key changes along with the color of the key, depending on the value, as you can see in the screenshots below. This might vary, depending on the user’s device.

And just to emphasize, this attribute doesn’t accept custom values; the value needs to be one of the seven shown above. An unrecognized value will just default to whatever the device’s default text is for the enter key.

The title Attribute On Stylesheets

This one was brand new to me when doing research for this article and might be the most interesting one in this list. As a bit of background, in case you didn’t know, Firefox has an option that lets you select which style sheet you want to use when viewing a page. Normally, this feature displays two options: “Basic Page Style” and “No Style”, as shown in the image below on my Windows machine.

This lets you quickly test how a page will look when styles are disabled, and it also allows you to view the page with any alternate stylesheets.

The alternate stylesheet feature is enabled with two attributes: The title attribute and rel=alternate applied to a <link> element, as shown in the code below:

<link href="main.css" rel="stylesheet" title="Default">
<link href="contrast.css" rel="alternate stylesheet" title="High Contrast">
<link href="readable.css" rel="alternate stylesheet" title="Readable">

In this case, my “default” styles will apply automatically, but the alternate stylesheets will only apply if I select them using Firefox’s “Page Style” option. You can try out the above example by visiting the following CodePen, using Firefox or another compatible browser:

See the Pen Alternate Stylesheets Using rel title Attributes [forked] by Louis Lazaris.

The screenshot below shows the stylesheet options in Firefox:


As mentioned, this feature works in Firefox, but I wasn’t able to get it to work in any Chromium-based browser. MDN’s article on alternate stylesheets says it can be enabled in other browsers using an extension, but I couldn’t find an active extension that does this.

More after jump! Continue reading below ↓

The cite Attribute For The <blockquote> And <q> Elements

I’m sure you use the <blockquote> element pretty regularly. You can use it plainly without an attribute, but you also have the option to use the cite attribute. Here’s an example that quotes the MDN article that describes using cite on <blockquote>:

<blockquote cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote#attr-cite">
      A URL that designates a source document or message for the information quoted. This attribute is intended to point to information explaining the context or the reference for the quote.
</blockquote>

Since my blockquote above is from the MDN article that explains what cite does, I’ve set the URL that points to the page as the cite value.

You can see how this is useful, because it wraps up the quote and the source of the quote in one element. But note this further explanation in the HTML spec:

User agents may allow users to follow such citation links, but they are primarily intended for private use (e.g., by server-side scripts collecting statistics about a site’s use of quotations), not for readers.

And naturally, the same concepts apply to use of cite on the <q> element, which is used for inline quotations.

Attributes For Custom Ordered Lists

Ordered lists using the <ol> element are used often. Some lesser known features that allow you to customize the behaviour of the numbering that appears in such a list are:

  • the reversed attribute, to number the items in reverse order (high to low instead of the default low to high);
  • the start attribute, to define what number to start from;
  • the type attribute, to define whether to use numbers, letters, or Roman numerals;
  • the value attribute, to specify a custom number on a specific list item.

As you can see, ordered lists are a lot more flexible with plain HTML than you might normally be accustomed to.

The reversed attribute is an interesting one, because it doesn’t actually reverse the contents of the list itself; it only reverses the numbers next to each list item.

<ol reversed>
    <li>List item...</li>
    <li>List item...</li>
    <li>List item...</li>
</ol>

The CodePen demo below adds some JavaScript, so you can interactively toggle the reversed attribute.

Notice, that the list itself stays the same, but the numbers change. Keep that in mind if you’re looking for a way to reverse the contents. That’s something you would do with JavaScript, CSS, or directly in the HTML source.

Above, I also mentioned three other attributes. Let’s incorporate those into the list to see how they can be used:

<ol reversed start="20" type="1">
    <li>Typee: A Peep at Polynesian Life (1846)</li>
    <li>Omoo: A Narrative of Adventures in the South Seas (1847)</li>
    <li>Mardi: and a Voyage Thither (1849)</li>
    <li>Redburn: His First Voyage (1849)</li>
    <li value="100">White-Jacket; or, The World in a Man-of-War (1850)</li>
    <li>Moby-Dick; or, The Whale (1851)</li>
    <li>Pierre; or, The Ambiguities (1852)</li>
    <li>Isle of the Cross (1853 unpublished, and now lost)</li>
</ol>

Note, the type and start attributes that have been added as well as the value attribute on an individual list item. The type attribute accepts one of the five single-character values (a, A, i, I, 1) representing the numbering type.

Try it using the following interactive demo:

Use the radio buttons to select one of the five values for the type attribute. Then try reversing the list using the Toggle Reversed button. As you can see, there is a ton of possibilities beyond the default behaviour of ordered lists!

The download Attribute For The <a> Element

As ubiquitous as links are on the web, it’s always nice to have an attribute that makes links even more powerful. The download attribute was added to the spec a number of years ago, and it allows you to specify that when a link is clicked, it should be downloaded rather than visited.

<a href="/example.pdf" download>Download File</a>

With no value, the download attribute forces the linked page to be downloaded. Alternatively, you can provide a value which the browser uses as the suggested file name for the downloaded resource.

<a href="/example.pdf" download="my-download.pdf">Download File</a>

As a bonus trick involving this attribute, you can combine this feature with some JavaScript to create a way for the user to download content they create themselves. I covered it a little bit previously in this post, and you can try it out by using the demo below.


The decoding Attribute For The <img> Element

This is another one that was brand new to me when researching this article — and it seems to be fairly new in the spec. Adding the decoding attribute to an image element provides an image decoding hint to the browser.

<img src="/images/example.png" alt="Example" decoding="async">

This attribute is similar to using the async attribute on scripts. The time it takes to load the image doesn’t change, but the manner in which its “decoded” (and thus its content becomes visible in the viewport) is determined by the decoding attribute.

Values are:

  • sync
    Decode the image synchronously, which is generally how browsers do it.
  • async
    Decode the image asynchronously to avoid delaying presentation of other content.
  • auto
    The default which allows the browser to use its own built-in method of decoding.

If you’re curious about the concept of decoding images, the spec has a good explanation that’s not too difficult to follow.

The loading Attribute For The <iframe> Element

As you probably already know, image elements can now include a loading attribute that puts lazy loading into the browser as a feature, something we’ve been doing for years with JavaScript solutions. But don’t forget that the loading attribute can also be used on <iframe> elements:

<iframe src="/page.html" width="300" height="250" loading="lazy">

As with images, the loading attribute accepts either a value of eager (the default browser behaviour) or lazy, which defers loading of the iframe’s contents until the iframe is about to enter the viewport. The only down-side to this attribute is the fact that its use on iframes is not supported in Firefox (though, Firefox does support loading on images).

The form Attribute for Form Fields

In most cases, you’re going to nest your form inputs and controls inside a <form> element. But if your app or layout requires something a little different, you have the option to put a form input anywhere you want and associate it with any <form> element — even one that’s not the element’s parent.

<form id="myForm" action="/form.php">
  <input id="name">
  <button type="submit">
</form>

<input type="email" form="myForm">

As you can see above, the email <input> that’s outside the form has the form attribute set to myForm, which is set to the same value as the id of the form. You can associate a form control (including the submit button) with any form in a document by using this attribute and the form’s id.

You can try this out using this demo page. The form submits using a GET request, so you can see the values submitted in the URL’s query string. On that page, the “comments” box is outside the <form> element.

The only complaint I have with this attribute is that it probably should have been given a more unique name, maybe something like “formowner”. Nonetheless, it’s a useful one to remember, should your design or layout require a parent-less form field.

The cite And datetime Attributes For Deletions/Insertions

I’ve already mentioned cite when dealing with blockquotes, but this attribute can also be used with deletions and insertions marked up with the <del> and <ins> elements. Additionally, both elements can include a datetime attribute.

<del
  cite="https://bugzilla.mozilla.org/show_bug.cgi?id=1620467"
  datetime="2020-07-23"
>Firefox doesn't support CSS's standard <code>appearance</code> property, so you can only use it prefixed.</del>

<ins          
  cite="https://bugzilla.mozilla.org/show_bug.cgi?id=1620467"
  datetime="2020-07-23"
>The <code>appearance</code> property, previously only available prefixed in Firefox, can now be used in all modern browers unprefixed.</ins>

For each element, here’s what the two attributes represent:

  • cite
    A URL pointing to a resource that explains why the content was deleted or inserted.
  • datetime
    Date that the deletion or insertion was made.

In my case, I’m using the example of some text, describing a CSS property that required a vendor prefix in Firefox. This might be an old blog post. Once the prefixes were removed, I could delete the old text and insert the new text using the <del> and <ins> elements. I can then use the cite attribute to reference the bug report where the problem was resolved.

The label Attribute for the <optgroup> Element

Finally, this last one is a bit of a golden oldie, but because it doesn’t get used too often, maybe you didn’t even know it existed. This one is a combination of an element along with an attribute.

If you have a long list of items included in the options for a <select> drop-down, you can group the options into visible categories using the <optgroup> element along with its associated label attribute:

<select>
  <option>--Your Favourite Animal--</option>
  <optgroup label="Birds">
    <option>Blue Jay</option>
    <option>Cardinal</option>
    <option>Hummingbird</option>
  </optgroup>
  <optgroup label="Sea Creatures">
    <option>Shark</option>
    <option>Clownfish</option>
    <option>Whale</option>
  </optgroup>
  <optgroup label="Mammals">
    <option>Lion</option>
    <option>Squirrel</option>
    <option>Quokka</option>
  </optgroup>
</select>

You can try out an example by using the following CodePen:

Notice, each <optgroup> has a label attribute that defines a heading for each group — but the headings can’t be selected. As a bonus tip, you can also use the disabled attribute on an <optgroup> to disable all the options in that section of the <select> drop-down.

The imagesizes and imagesrcset Attributes for Preloading Responsive Images

This is another pair of attributes that were new to me when researching this article, and both of them are relatively new in the spec as well.

Both of these attributes can be defined along with rel=preload and as on the <link> element, as follows:

<link rel="preload"
      as="image"
      imagesrcset="images/example-480.png 480w,
             images/example-800.png 800w,
             images/example.png 2000w"
     imagesizes="(max-width: 600px) 480px,
            (max-width: 1000px) 800px,
            1000px"
     src="images/example.png"
     alt="Example Image">

Use of rel=preload here informs the browser that we want the indicated resources to load in priority, so they’re not blocked by things like scripts and stylesheets. The as attribute specifies the type of content requested.

You can preload regular images by using the href attribute along with preload and as. But on top of that, you can use the imagesrcset and imagesizes attributes, as I’ve done in the code above.

This allows you to preload the correct image, depending on the size of the viewport or other media features you’ve specified in the imagesizes attribute.

Honorable Mentions

In addition to the attributes I’ve already described and demonstrated in detail, there are some others you might want to look into that I’ll just mention briefly here:

  • The crossorigin attribute which can apply to multiple elements, including <audio>, <img>, <link>, <script>, and <video>, providing support for cross-origin resource sharing (CORS);
  • The title attribute for <dfn> and <abbr>;
  • The new disablepictureinpicture attribute for the <video> element;
  • The integrity attribute for scripts, which helps the browser verify that the resource hasn’t been improperly manipulated;
  • The disabled attribute for the <fieldset> element, to easily disable multiple form elements simultaneously;
  • The multiple attribute for email and file inputs.

If you’ve used any of the attributes mentioned in this article, or if you know of another HTML feature that you’ve personally benefited from using in one of your projects, feel free to let me know in the comments.

Saturday, May 28, 2022

Remix Routes Demystified

 

Around six months ago, Remix became open source. It brings a lovely developer experience and approximates web development to the web platform in a refreshing way. It’s a known tale that naming is the hardest thing in programming, but the team nailed this one. Remix drinks from the community experience, puts the platform and browser behavior in a front seat; sprinkles the learning the authors got from React-Router, Unpkg, and from teaching React. Like a remixed record, its content mixes the old needs to novel solutions in order to deliver a flawless experience.

Writing a Remix app is fun, it gets developers scratching their heads about, “How did Forms actually work before?”, “Can Cache really do that?”, and (my personal favorite), “The docs just pointed me to Mozilla Dev Network!”

In this article, we will dig deeper and look beyond the hype, though. Let’s pick inside (one of) Remix’s secret sauces and see one of the features that powers most of its features and fuels many of its conventions: routes. Buckle up!

Anatomy Of A Remix Repository

If pasting npx create-remix@latest, following the prompt, and opening the scanning the bare bones project file-tree, a developer will be faced with a structure similar to the one bellow:

├───/.cache
├───/public
├───/app
│   ├───/routes
│   ├───entry.client.jsx
│   ├───entry.server.jsx
│   └───root.tsx
├───remix.config.js
└───package.json
  • .cache will show up, once there’s a build output;
  • public is for static assets;
  • app is where the fun will happen, think of it as a /src for now;
  • the files on the root are the configuration ones, for Remix, and for NPM.

Remix can be deployed to any JavaScript environment (even without Node.js). Depending on which platform you choose, the starter may output more files to make sure it all works as intended.

We have all seen similar repositories on other apps. But things already don’t feel the same with those entry.server.jsx and entry.client.jsx: every route has a client and a server runtime. Remix embraces the server-side from the very beginning, making it a truly isomorphic app.

While entry.client.jsx is pretty much a regular DOM renderer with Remix built-in sauce, entry.server.jsx already shows a powerful strategy of Remix routing. It is possible and clear to determine an app-wide configuration for headers, response, and metadata straight from there. The foundation for a multi-page app is set from the very beginning.

Routes Directory

Out of the 3 folders inside /app on the snippet above, routes is definitely the most important. Remix brings a few conventions (one can opt-out with some configuration tweaks) that power the Developer Experience within the framework. The first convention, which has somewhat raised to a standard among such frameworks, is File System based routing. Within the /routes directory, a regular file will create a new route from the root of your app. If one wants myapp.com and myapp.com/about, for example, the following structure can achieve it:

├───/apps
│   ├───/routes
│   │   ├───index.jsx
│   │   └───about.jsx

Inside those files, there are regular React components as the default export, while special Remix methods can be named exports to provide additional functionalities like data-fetching with the loader and action methods or route configuration like the headers and meta methods.

And here is where things start to get interesting: Remix doesn’t separate your data by runtime. There’s no “server data”, or “build-time data”. It has a loader for loading data, it has an action for mutations, headers and meta for extending/overriding response headers and metatags on a per-route basis.

More after jump! Continue reading below ↓

Route Matching And Layouts

Composability is an order of business within the React ecosystem. A componentized program excels when we allow it to wrap one component on another, decorating them and empowering them with each other. With that in mind, the Layout Pattern has surfaced, it consists of creating a component to wrap a route (a.k.a another component) and decorate it in order to enforce UI consistency or make important data available.

Remix puts the Layout Pattern front and center it’s possible to determine a Layout to render all routes which match its name.

├───/apps
│   ├───/routes
│   │   ├───/posts    // actual posts inside
│   │   └───posts.jsx // this is the layout

The posts.jsx component uses a built-in Remix component (<Outlet />) which will work in a similar way that React developers are used to have {children} for. This component will use the content within a /posts/my-post.jsx and render it within the layout. For example:

import { Outlet } from 'remix'

export default PostsLayout = () => (
  <main>
     <Navigation />
     <article>
       <Outlet />
     </article>
     <Footer />
  </main>
)

But not always the UI will walk in sync with the URL structure. There is a chance that developers will need to create layouts without nesting routes. Take for example the /about page and the /, they are often completely different, and this convention ties down the URL structure with UI look and feel. Unless there is an escape hatch.

Skipping Inheritance #

When nesting route components like above, they become child components of another component with the same name as their directory, like posts.jsx is the parent component to everything inside /posts through <Outlet />. But eventually, it may be necessary to skip such inheritance while still having the URL segment. For example:

├───/apps
│   ├───/routes
│   │   ├───/posts                       // post
│   │   ├───posts.different-layout.jsx   // post
│   │   └───posts.jsx                    // posts layout

In the example above, posts.different-layout.tsx will be served in /posts/different-layout, but it won’t be a child component of posts.jsx layout.

Dynamic Routes

Creating routes for a complex multi-page app is almost impossible without some Dynamic Routing shenanigans. Of course, Remix has its covered. It is possible to declare the parameters by prefixing them with a $ in the file name, for example:

├───/apps
│   ├───/routes
│   |   └───/users
│   │         └───$userId.jsx

Now, your page component for $userId.jsx can look something like:

import { useParams } from 'remix'

export default function PostRoute() {
  const { userId } = useParams()

  return (
    <ul>
      <li>user: {userId}</li>
    </ul>
  )
}

Also there’s an additional twist: we can combine this with the Dot Limiters mentioned a few sections prior, and we can easily have:

├───/apps
│   ├───/routes
│   |   └───/users
│   |         ├───$userId.edit.jsx
│   │         └───$userId.jsx

Now the following path segment will not only be matched, but also carry out the parameter: /users/{{user-id}}/edit. Needless to say, the same structure can be combined to also carry additional parameters, for example: $appRegion.$userId.jsx will carry out the 2 parameters to your functions and page component: const { appRegion, userId } = useParams().

Catch-all With Splats

Eventually, developers may find themselves in situations where the number of parameters, or keys for each, a route is receiving is unclear. For these edge-cases Remix offers a way of catching everything. Splats will match everything which was not matched before by any of its siblings. For example, take this route structure:

├───/apps
│   ├───/routes
│   │   ├───about.jsx
│   │   ├───index.jsx
│   │   └───$.jsx      // Splat Route
  • mydomain.com/about will render about.jsx;
  • mydomain.com will render index.jsx;
  • anything that’s not the root nor /about will render $.jsx.

And Remix will pass a params object to both of its data handling methods (loader and action), and it has a useParams hook (exactly the same from React-Router) to use such parameters straight on the client-side. So, our $.jsx could look something like:

import { useParams } from 'remix'
import type { LoaderFunction, ActionFunction } from 'remix'

export const loader: LoaderFunction = async ({
  params
}) => {
  return (params['*'] || '').split('/')
};

export const action: ActionFunction = async ({
  params
}) => {
  return (params['*'] || '').split('/')
};

export default function SplatRoute() {
  const params = useParams()
  console.log(return (params['*'] || '').split('/'))

  return (<div>Wow. Much dynamic!</div>)
}

Check the Load data and the Mutating data sections for an in-depth explanation of loader and action methods respectively.

The params[''] will be a string with the all params. For example: mydomain.com/this/is/my/route will yield “this/is/my/route”. So, in this case we can just use .split('/') to turn into an array like ['this', 'is', 'my', 'route'].

Load Data

Each route is able to specify a method that will provide and handle its data on the server right before rendering. This method is the loader function, it must return a serializable piece of data which can then be accessed on the main component via the special useLoaderData hook from Remix.

import type { LoaderFunction } from 'remix'
import type { ProjectProps } from '~/types'
import { useLoaderData } from 'remix'

export const loader: LoaderFunction = async () => {
  const repositoriesResp = await fetch(
    'https://api.github.com/users/atilafassina/repos'
  )
  return repositoriesResp.json()
}

export default function Projects() {
  const repositoryList: ProjectProps[] = useLoaderData()

  return (<div>{repositoryList.length}</div>
}

It’s important to point out, that the loader will always run on the server. Every logic there will not arrive in the client-side bundle, which means that any dependency used only there will not be sent to the user either. The loader function can run in 2 different scenarios:

  1. Hard navigation:
    When the user navigates via the browser window (arrives directly to that page).
  2. Client-side navigation:
    When the user was in another page in your Remix app and navigates via a <Link /> component to this route.

When hard navigation happens, the loader method runs, provides the renderer with data, and the route is Server-Side Rendered to finally be sent to the user. On the client-side navigation, Remix fires a fetch request by itself and uses the loader function as an API endpoint to fuel fresh data to this route.

Mutating Data

Remix carries multiple ways of firing a data mutation from the client-side: HTML form tag, and extremely configurable <Form /> component, and the useFetcher and useFetchers hooks. Each of them has its own intended use-cases, and they are there to power the whole concept of an Optimistic UI that made Remix famous. We will park those concepts for now and address them in a future article because all these methods unfailingly communicate with a single server-side method: the action function.

Action and Loader are fundamentally the same method, the only thing which differentiates them is the trigger. Actions will be triggered on any non-GET request and will run before the loader is called by the re-rendering of the route. So, post a user interaction, the following cascade will happen on Remix’s side:

  1. Client-side triggers Action function,
  2. Action function connects to the data source (database, API, …),
  3. Re-render is triggered, calls Loader function,
  4. Loader function fetches data and feeds Remix rendering,
  5. Response is sent back to the client.

Headers And Meta

As previously mentioned, there are other specific methods for each route that aren’t necessarily involved with fetching and handling data. They are responsible for your document headers and metatags.

Exporting meta function allows the developer to override the metatag values defined in the root.jsx and tailor it to that specific route. If a value isn’t changed, it will seamlessly inherit. The same logic will apply to the headers function, but with a twist.

Data usually is what determines how long a page can be cached, so, naturally, the document inherits the headers from its data. If headers function doesn’t explicitly declare otherwise, the loader function headers will dictate the headers of your whole document, not only data. And once declared, the headers function will receive both: the parent headers and the loader headers as parameters.

import type { HeadersFunction } from 'remix'

export const headers: HeadersFunction = ({ loaderHeaders, parentHeaders }) => ({
  ...parentHeaders,
  ...loaderHeaders,
  "x-magazine": "smashing",
  "Cache-Control": "max-age: 60, stale-while-revalidate=3600",
})

Resource Routes

These routes are essentially one which doesn’t exist naturally in the website’s navigation pattern. Usually, a resource route does not return a React component. Besides this, they behave exactly the same as others: for GET requests, the loader function will run, for any other request method, the action function will return the response.

Resource routes can be used in a number of use cases when you need to return a different file type: a pdf or csv document, a sitemap, or other. For example, here we are creating a PDF file and returning it as a resource to the user.

export const loader: LoaderFunction = async () => {
  const pdf = somethingToPdf()

  return new Response(pdf, {
    headers: {
      'Content-Disposition': 'attachment;',
      'Content-Type': 'application/pdf',
    },
  })
}

Remix makes it straightforward to adjust the response headers, so we can even use Content-Disposition to instruct the browser that this specific resource should be saved to the file system instead of displaying inline to the browser.

Remix Secret Sauce: Nested Routes

Here is where a multi-page app meets single-page apps. Since Remix’s routing is powered by React-Router, it brings its partial routing capabilities to the architecture. Each route is responsible for its own piece of logic and presentation, and this all can be declared used by the File-System heuristics again. Check this:

├───/apps
│   ├───/routes
│   │   ├───/dashboard
│   │   |    ├───profile.jsx
│   │   |    ├───settings.jsx
│   │   |    └───posts.jsx
│   │   └───dashboard.jsx      // Parent route

And just like we did implicitly on our Layout paradigm before, and how Remix handles the root//routes relationship, we will determine a parent route which will render all its children routes inside the <Outlet /> component. So, our dashboard.jsx looks something like this:

import { Outlet } from 'remix'

export default function Dashboard () {
  return (
   <div>
     some content that will show at every route
     <Outlet />
   </div>
  )
}

This way, Remix can infer which resources to pre-fetch before the user asks for the page. because it allows the framework to identify relationships between each route and more intelligently infer what will be needed. Fetching all of your page’s data dependencies in parallel drastically boosts the performance of your app by eliminating those render and fetch waterfalls we dread so much seeing in (too) many web apps today.

So, thanks to Nested Routes, Remix is able to preload data for each URL segment, it knows what the app needs before it renders. On top of that, the only things that actually need re-rendering are the components inside the specific URL segment.

For example, take our above app , once users navigate to /dashboard/activity and then to /dashboard/friends the components it will render and data it will fetch are only the ones inside /friends route. The components and resources matching the /dashboard segment are already there.

So now Remix is preventing the browser from re-rendering the entire UI and only doing it for the sections that actually changed. It can also prefetch resources for the next page so once actual navigation occurs the transition is instant because data will be waiting at the browser cache. Remix is able to optimize it all out of the box with fine-grained precision, thanks to Nested Routes and powered by partial routing.

Wrapping Up

Routing is arguably the most important structure of a web app because it dictates the foundation where every component will relate to each other, and how the whole app will be able to scale going forward. Looking closely through Remix’s decisions for handling routes was a fun and refreshing ride, and this is only the scratch on the surface of what this framework has under its hood. If you want to dig deeper into more resources, be sure to check this amazing interactive guide for Remix routes by Dilum Sanjaya.

Though an extremely powerful feature and a backbone of Remix, we have just scratched the surface with routes and these examples. Remix shows its true potential on highly interactive apps, and it’s a very powerful set of features: data mutation with forms and special hooks, authentication and cookie management, and more.

Designing A Better Infinite Scroll

 

We’ve all been there. You might have a long-winded list of search results, products, orders or data entries. Of course, you have all kinds of filters and sorting and search already in place. However, you also need to help customers explore relevant entries, and to do so, you need to support and speed up browsing through entries.

Your natural design instinct might tell you to stay true to good old-fashioned pagination at first. Yet before you know it, you might start wondering if infinite scroll might be a good option to consider, given the very unique use case that you have. So is infinite scroll actually a good idea? Well, we all have strong opinions about infinite scroll, and usually not very good ones. This has a number of good reasons.

Problems With Infinite Scroll

The issues with infinite scroll are well-known. The most obvious one is the sheer amount of options on the page which is often too overwhelming and too difficult to manage. It really feels like drowning in an information abyss with no end in sight. No wonder that once the number of displayed options is beyond the comfortable range, a good number of users abandon the page altogether as a reaction to that.

Also, we don’t have any control over when and how many items appear on scroll. Just like there is no easy way to navigate between the “old” and “new” segments of the infinite scroll as they all fall into the same stream of items. Once you scroll up and down a few items, it’s hard to see immediately what we have seen already and what we haven’t seen yet — unless we meticulously scan through the last few items a couple of times.

Sometimes the URL in the address bar changes on scrolling, but more often it does not, leaving us out there starting all over again if we want to continue browsing later. And if we want to send the URL to ourselves or to our loved ones to explore a particular set of items at once, that’s usually painful as we can’t really bookmark a location in the list.

Should we want to reach the footer, every time we scroll, we need to scroll just a little bit faster to get a miraculous chance to reach the footer before the new stream of items comes in. Sometimes users find themselves taking on a scrolling challenge while hitting Esc at the same time — to cancel the infinite scroll just in time. (Usually unsuccessfully.)

On top of that, infinite scroll breaks the scroll bar as the user’s expectations of the page length have to be recalibrated with every scroll. The scrollbar is a promise of how lengthy the page actually is, but with newly loaded items, the promise is always faulty. Not to mention accessibility issues of announcing the newly loaded items properly to screen readers as well as performance issues on choppy connections.

All of the issues listed above are just poor usability. So it’s not surprising that we often disregard infinite scroll as a fashionable technique that produces more problems than solutions. And it’s not surprising that as designers, we tend to use other options instead: pagination and “load more” buttons.

Pagination and “Load More”

We can avoid all infinite scroll issues by falling back to the usual suspect: pagination. And it has plenty of benefits. With it, the user always has a clear beginning and a clear end. As users finish a page and move to the next one, there is a very clear “cut” of what has and what hasn’t been seen yet, along with a sense of completion during that navigation.

Plus, there is a sense of control how much data is being displayed on a given page (usually with controls to change the number of items per page), the URL is different for every page, the footer is easy to reach and the options appearing on pages are easier to manage.

Unfortunately, in usability tests, sometimes pagination doesn’t perform well at all. It’s much more predictable and easier to manage, but also much slower compared to infinite scroll, so users often browse significantly less and often feel “slowed down”. There seems to be more time spent on the first few pages, and filters and sorting are used more often in that time, but compared to infinite scroll, they view fewer items in total and are often less engaged.

If we want to keep the benefits of pagination but avoid overwhelming users with infinite scroll, we can use a “Load more” pattern instead. With it in place, as users start scrolling, eventually they can choose to load more items on tap or on click. In some implementations, as users start scrolling down, more items appear automatically at first, but then the “Load more” button appears, once a certain threshold is reached.

For example, we could display 10–30 products on the initial page load (10 on mobile, 30 on desktop). When the user reaches the end of the listing, we can use automatically fetch the next 10–30 products. When the user reaches 30–70 items, we then switch to “Load more”. We also provide an option to go back with the “Back” button, so users always have a sense of control about where they navigate.

We could also provide an option to continue browsing later by allowing users to type in their email and get a link that later would bring them to the position in the list where they currently are. This solves the problem of not being able to continue browsing later, perhaps on another device, or at a different time.

“Load more” works very well in eCommerce — users have control of what they see as all items appear on a single page and the footer is always in reach. Plus, if the URL is changed every time a user hits the “Load more” button, we bring together the speed of infinite scroll with the comfort and security of pagination. Users do seem to be browsing more and are more engaged. This makes this pattern a good option to consider for lengthy lists.

Does it mean that we can abandon infinite scroll altogether? Not necessarily. An important benefit of infinite scroll is the speed of displaying results — displaying new items just when users want to see more. As it turns out, there are a few techniques and strategies to make infinite scroll better. But it requires solving all of the issues we’ve outlined earlier.

Bookmarking The Position In The List

The easiest way to improve infinite scroll is by marking the breaks between “new” and “old” items in the list. When a new batch comes in, we separate the items visually and allow users to mark the position in the list from which they want to continue browsing later. We could also allow them to see all products they’ve seen on a separate page, so they can separate viewed options from the infinite stream of all options. An example of this interaction is displayed below.

Once a user clicks on the “Continue here later”, we could either show a checkmark and store the position in the browser, or display a modal asking users for their email address.

Alternatively, we could display the newsletter box directly, allowing users to copy a link to the current position on the page as well. As a positive side effect, this also gives us an option to collect users’ emails to send them a reminder about new items later.

The solution above might solve the problem with the lack of understanding of where users are, but since items will be loaded in automatically, we still have some other issues — e.g. reaching the footer. This is quite easy to solve though.

Just like we are used to sticky headers, we could integrate a footer reveal: a little helper that would stay persistent at the bottom right bar, and display the footer if needed while the rest of the page uses infinite scroll. A good example of it in action is Dahmakan, a food delivery service from Kuala-Lumpur in Malaysia (no infinite scroll in use at the moment). It’s worth emphasizing that the footer should be accessible via keyboard navigation as well, rather than just opening on click or on tap.

Combine Pagination and Infinite Scroll

As users scroll down a page and items are loaded in, we can present it as dynamic pagination to users (see Pepper.pl). On scroll, the URL of the page changes, and the page number is updated in the sticky bottom bar. Users can also navigate to a specific page in a pagination drop-down. And of course, an accordion opens up a footer on tap or click as well. Check the video example.

What do we do with the “Back” button though? For example, once a user has browsed through “pages” 1, 2 and 3 and now has landed on “page” 4, should a click on a “Back” button bring them from page 4 to page 3, or to the previous page that they visited before getting to the page 1 in the first place? In general, we probably don’t want to pollute users’ browsers’ history by adding every step of the infinite scroll in there. So selecting a specific page with a drop-down is indeed a good idea.

A great example of combining pagination and infinite scroll in one place; the only refinement could be slightly better focus styles and better navigation jumps for accessibility. Additionally, it would be fantastic to add some sort of a drop-down chevron next to the current page to make it obvious that one can actually jump to a specific page. The “Back” button, then, would bring the user back to the page from which they came to the list they have in front of them at the moment.

Scrollbar Range Intervals

Another useful technique is suggested by Baymard Institute, a research company for testing eCommerce sites. The idea is to make scrollbars more helpful by adding dynamic labels that are spaced out vertically. These would indicate to users where they currently are and where they can jump to. As users keep scrolling down, the labels would be shifting as the scrollbar is growing. Could be used by any criteria a user has chosen to sort the items by.

Pinning Sections on a Mini-Map

We could make it even more useful by allowing users to pin, or bookmark, areas of interest in the list, so they can return to favorites faster. An interesting prototype of such an experience is Minimap experiment (currently works only in Firefox), created by Rauno Freiberg, along with many other wonderful experiments.

Wrapping Up

With all of these techniques in place, we solved many problems that infinite scroll is known for. We now have better control of how many items appear on scroll, and can always stop browsing and continue later. We can easily spot “old” and “new” segments. The URL is being updated as the user scrolls down the page, and we allow them to copy the URL to the current position in the list as well.

Users can always reach the footer, and the scrollbar indicates where they currently are and where they can jump to. They can also jump to any specific page since we provide pagination as well. Additionally, we still need to implement infinite scroll in a way to ensure accessibility for the keyboard and announce new items. But: we make use of all the benefits that infinite scroll provides: especially the speed of browsing.

Now, all of this seems like a lot of work just to make infinite scroll better. The ultimate question of whether all the work is worth it has to be answered by the goals your users are supposed to achieve. Infinite scroll is not for every website, and an endless list of options needs to be complemented with proper filtering, sorting and search. In general, if your users are more likely to compare options or find very specific content, infinite scroll probably won’t be very useful.

However, if your users often explore many options, and browsing is a very typical attribute on your site, especially when customers add multiple items in a shopping cart or operate on a large set of data entries at once, infinite scroll might be very well worth it — but only if accessibility and performance considerations are the very heart of its design.

The ideas highlighted in this article are just that — ideas. Some of them might fail miserably in your usability tests, while others might perform fairly well. But: if you absolutely need to make infinite scroll work, there are ways and workarounds to do so — it’s just not as straightforward as it might appear at first.

Infinite Scroll Checklist

As usual, here’s a general checklist of a few important guidelines to consider when designing a better infinite scroll:

  • If in doubt, always prefer pagination.
  • With infinite scroll, always integrate a footer reveal.
  • Consider separating “old” and “new” items visually.
  • Provide an option to continue browsing later.
  • Consider using “load more” + infinite scroll together.
  • Consider using pagination + infinite scroll together.
  • Change the URL as new items are loaded in and expose it to users.
  • Allow users to jump to any page with a pagination drop-down.
  • Consider using scrollbar range intervals.
  • Consider allowing users to pin or bookmark items/areas of interest.
  • Make sure accessibility and performance are major considerations in the implementation.

Meet Smart Interface Design Patterns

If you are interested in similar insights around UX, take a look at Smart Interface Design Patterns, our shiny new 6h-video course with 100s of practical examples from real-life projects. Plenty of design patterns and guidelines on everything from accordions and dropdowns to complex tables and intricate web forms — with 5 new segments added every year. Just sayin’! Check a free preview.


Useful Resources

Thursday, May 26, 2022

The What, When, Why And How Of Next.js’ New Middleware Feature

 

“Middleware” isn’t a new term in computing. It is often used as a term to describe a piece of software that holds two systems together. You could call it “glue” for software, and essentially, that’s how Next.js’ middleware works.

Next.js’ middleware allows you to create functions that execute after a user’s request is made and before the request is completed — in the middle of the two processes. This enables you to process a user’s request and then modify the response by rewriting, redirecting, modifying headers, or even streaming HTML.

Within Next.js, middleware operates in a limited runtime described as the “Edge Runtime”. The code which ran through the runtime has access to a set of standard Web APIs, that’ll be discussed later in the article. For Vercel customers, middleware functions will be executed as Vercel Edge Functions.

What About API Routes?

As you read this article, you may be thinking about how middleware sounds awfully like Next.js’ API routes that have been around for a while. The key difference is how they are used: the more restricted runtime of middleware functions, individual requests are made to API routes, whilst Middleware functions operate in between a user’s request to a page, and that page’s being rendered.

This also means that Middleware can be scoped to multiple pages allowing you to avoid repeating code. For example, if you need to change each page in the app directory based on whether a user is logged in, you could create a Middleware function within that directory to process users’ cookies to see if they’re logged in, and then pass that information onto the page. In comparison, achieving a similar effect would require extra code within an API route.

The primary technical difference between the two is that Next.js’ API routes were designed to be hosted on a single node server hosted in one place, whilst Middleware functions are designed to be deployed on the “edge”, which is essentially a marketing term for deploying code in multiple locations around the world. Alongside the difference in physical distance, the “edge” is commonly associated with aggressive caching and efficient cache invalidation which reduces unnecessary computation.

The goal of this is speed. A server’s response generally arrives faster when the user is closer to the server, so when you have only one server, those speeds are only accessible to a subset of your users. However, with your code being deployed in multiple locations, more users will have access to fast responses.

Lastly, Middleware is designed to have no cold boot time. An API route’s boot time is a significant cause of slow responses. On Vercel, Serverless Functions (which are used to deploy API routes) normally take around 250 milliseconds to boot. Middleware is also designed to startup in much less time than API routes, Vercel claims that their Edge Functions (which are used to deploy Next.js Middleware) have a “100x faster startup” than their Serverless Functions.

When Should I Use Middleware?

Middleware should be used in cases where a small amount of processing is required, this is because Middleware needs to return a response in less than 1.5 seconds, otherwise the request will time out.

Geolocation #

The NextRequest object which is available within Middleware has geographic information available in the geo key. Using this information, you could then rewrite your user to pages with localized information. For example, if you were creating a site for a global restaurant chain, you could show a different menu depending on the user’s location. Vercel’s example here uses this geolocation to provide Power Parity Pricing.

This can work alongside Next.js’ i8n / localization feature, like this.

Security

Through the NextRequest object, the cookie information is available (on the cookies key), and by using NextResponse you can set cookies. These cookies can be used to authenticate users on your site.

You may also want to block access to your sites from certain users, such as bots or users in a certain country. To achieve this you can conditionally return a 404 or rewrite the request to a “blocked” page. Vercel has an example of blocking based on location here.

A/B Testing

Previously, to show a different page to a user on a static site as part of A/B testing (or a similar exercise) you would have had to process the user’s request on the client-side which can cause cumulative layout shifts or a flash. However, if we process it on a server this can be avoided.

To achieve this you can place users in “buckets” through cookies, and then redirect them based on the bucket their cookie places them in. View Vercel’s example to see how that can work.

The Limitations of Middleware

Middleware is starting to sound pretty wonderful, isn’t it? Whilst it is wonderful, there are some drawbacks which means that you’ll probably still be needing API routes for certain use cases.

Some of these limitations are specific to Vercel deployments of Next.js sites, however, similar limitations exist on other platforms.

Execution Time (Vercel specific)

A middleware function can execute for a maximum of thirty seconds, however, as I mentioned above, it must return a response within one and a half seconds. This means that your function should return a response as soon as possible, and you can then continue any other workloads in the background if you need to. For example, if you were looking to do server-side analytics, you could extract the information you need, return a response, and then make a call to your database to log the information after returning the response.

Function Size (Vercel specific)

A Middleware function can be at most 1MB, this includes all other code bundled with the function. Most use cases won’t require such a large code bundle, but it’s certainly something to keep an eye on.

Native Node.js APIs Aren’t Supported

Middleware functions don’t run through Node.js like the rest of Next.js’ server-side code does (such as API Routes). One of the key things that limit Middleware functions from performing is reading and writing to the filesystem.

This also means that JavaScript modules that rely on native Node.js APIs also can’t be used.

ES Modules Only

Node Modules can be used within middleware, however, they must be ES Modules. Whilst there is a growing shift within the ecosystem to switch to ES Modules, there are still many packages that use CommonJS or rely on other packages through CommonJS.

No String Evaluation

Neither JavaScript’s eval or new Function(evalString) are allowed within the runtime.

Implementing Middleware

To explore how Middleware works we’ll be creating a link shortener that’ll be much faster than those that use API routes.

To get started, clone the starter for the app:

yarn create next-app -e https://github.com/sampoder/middleware-demo/tree/starter

The starter has two key files: routes.js & pages/index.js. routes.js will contain all the routes for our link shortener. Normally, you’d use a database, but for the purpose of this exercise, we’ll keep it simple with a hardcoded key/value object. pages/index.js will serve as our link shortener’s homepage with a list of all the available routes.

Then we’ll create our Middleware function by creating a new file named _middleware.js in the pages directory. A middleware function is scoped to the directory, affecting sibling and children routes. For example, as the /pages directory is linked to the / routes, thus if the middleware is placed in the /pages directory, it will apply to routes, such as /about or /about/team/john. Meanwhile, if the middleware was placed in the /pages/blog directory, it would apply to routes, such as /blog/middleware or /blog/about/submit, but not /info.

We then will need to import NextResponse from next/server:

import { NextResponse } from 'next/server'

As the NextResponse object is an extension of the Node.js’ Response interface, it’ll allow us to modify the response.

We’ll also need to import the routes file:

import routes from "../routes"

Each Middleware file needs to export a function named middleware. This will be what Next.js runs on request:

export function middleware(req) {
  
}

The middleware function will be passed through a request object. Similar to the NextResponse object, this request object is an extension of the Node.js’ Request interface. It’ll give us information about the client’s request.

Through this request object we can then access the path name of the current request via the nextUrl key:

let { pathname } = req.nextUrl;

For our link shortener we will need to check whether our routes object contains a key with the same value as the pathname:

if (routes[pathname]) {

}

Then we can use the NextResponse object to modify the response. The NextResponse object enables us to both redirect() and rewrite() responses to different locations. As we’re building a URL shortener, we’ll use the redirect() method to transport users to their intended destination:

if (routes[pathname]) {
  return NextResponse.redirect(routes[req.nextUrl.pathname])
}

We’ve created a new NextResponse object, applied the redirect method, and then returned that object.

We also need to handle cases in which the pathname doesn’t have a matching destination. In these cases, we’ll redirect the users to our homepage:

else{
  const url = request.nextUrl.clone()
  url.pathname = '/'
  return NextResponse.redirect(url)
}

We can’t redirect to / directly, because support for relative URLs within Middleware will be deprecated soon. Instead, we make a clone of the request’s URL and change the pathname, before passing that URL object to the redirect() function.

And just like that we’ve got a functioning link shortener! For those curious, our entire middleware function ended up as:

import { NextResponse } from "next/server";
import routes from "../routes";

export function middleware(req) {
  let { pathname } = req.nextUrl
  if (routes[pathname]) {
    return NextResponse.redirect(routes[req.nextUrl.pathname])
  }
  else{
    const url = request.nextUrl.clone()
    url.pathname = '/'
    return NextResponse.redirect(url)
  }
}

And the entire codebase is available at https://github.com/sampoder/middleware-demo.

Whilst this example is short, it shows how handy middleware can be in building things. When you run the web app, you’ll also see how fast it can be.

Last but not least, middleware has a lot of promise, and I hope you enjoyed exploring the feature with me!