Thursday, October 24, 2019

Getting Started With Axios In Nuxt

In this tutorial, we will learn how to make a request in our Nuxt.js applications using the Axios module. We will also learn how to use the ayncData and fetch methods to fetch data on the server-side using Axios and the differences between the two methods. Finally, we will learn how to add authentication to our application using the Auth module.
Nuxt.js provides an Axios module for easy integration with your application. Axios is a promise-based HTTP client that works in the browser and Node.js environment or, in simpler terms, it is a tool for making requests (e.g API calls) in client-side applications and Node.js environment.
In this tutorial, we’re going to learn how to use the Axios module and how to make a request on the server-side using asyncData and fetch. These two methods make a request on the server-side but they have some differences which we’re also going to cover. Finally, we’ll learn how to perform authentication and secure pages/routes using the auth module and auth middleware.
This article requires basic knowledge of Nuxtjs and Vuejs as we’ll be building on top of that. For those without experience with Vuejs, I recommend you start from their official documentation and the Nuxt official page before continuing with this article.

What Is The Nuxt.js Axios Module?

According to the official Documentation,
“It is a Secure and easy Axios integration with Nuxt.js.”
Here are some of its features:
  1. Automatically set base URL for client-side & server-side.
  2. Proxy request headers in SSR (Useful for auth).
  3. Fetch Style requests.
  4. Integrated with Nuxt.js Progressbar while making requests.
To use the axios module in your application, you will have to first install it by using either npm or yarn.
YARN
yarn add @nuxtjs/axios
NPM
npm install @nuxtjs/axios
Add it into your nuxt.config.js file:
modules: [
    '@nuxtjs/axios',
  ],

  axios: {
    // extra config e.g
    // BaseURL: 'https://link-to-API'
  }
The modules array accepts a list of Nuxt.js modules such as dotenv, auth and in this case, Axios. What we’ve done is to inform our application that we would be using the Axios module, which we reference using @nuxtjs/axios. This is then followed by the axios property which is an object of configurations like the baseURL for both client-side and server-side.
Now, you can access Axios from anywhere in your application by calling this.$axios.method or this.$axios.$method. Where method can be get, post, or delete.


Making Your First Request Using Axios


After cloning the repo and opening the start folder, we would need to install all our packages in the package.json file so open your terminal and run the following command:
npm install
Once that is done, we can start our app using the npm run dev command. This is what you should see when you go to localhost:3000.

The next thing we have to do is to create a .env file in the root folder of our application and add our API URL to it. For this tutorial, we’ll be using a sample API built to collect reports from users.
API_URL=https://ireporter-endpoint.herokuapp.com/api/v2/
This way, we do not have to hard code our API into our app which is useful for working with two APIs (development and production).
The next step would be to open our nuxt.config.js file and add the environmental variable to our axios config that we added above.
/*
   ** Axios module configuration
   */
  axios: {
    // See https://github.com/nuxt-community/axios-module#options
    baseURL: process.env.API_URL,
  },
Here, we tell Nuxt.js to use this baseURL for both our client-side and server-side requests whenever we use this Axios module.
Now, to fetch a list of reports, let us open the index.vue file and add the following method to the script section.
async getIncidents() {
  let res = await this.$store.dispatch("getIncidents");
  this.incidents = res.data.data.incidents;
}
What we have done is to create an async function that we call getIncidents() and we can tell what it does from the name — it fetches a list of incidents using the Vuex store action method this.$store.dispatch. We assign the response from this action to our incidents property so we can be able to make use of it in the component.
We want to call the getIncidents() method whenever the component mounts. We can do that using the mounted hook.
mounted() {
    this.getIncidents()
  }
mounted() is a lifecycle hook that gets called when the component mounts. That will cause the call to the API to happen when the component mounts. Now, let us go into our index.js file in our store and create this action where we’ll be making our Axios request from.
export const actions = {
  async getIncidents() {
    let res = await this.$axios.get('/incidents')
    return res;
  }
}
Here, we created the action called getIncidents which is an async function, then we await a response from the server and return this response. The response from this action is sent back to our getIncidents() method in our index.vue file.
If we refresh our application, we should now be able to see a long list of incidents rendered on the page.

We have made our first request using Axios but we won’t stop there, we are going to be trying out asyncData and fetch to see the differences between them and using Axios.

AsyncData

AsyncData fetches data on the server-side and it’s called before loading the page component. It does not have access to this because it is called before your page component data is created. this is only available after the created hook has been called so Nuxt.js automatically merges the returned data into the component’s data.
Using asyncData is good for SEO because it fetches your site’s content on the server-side and also helps in loading content faster. Note that asyncData method can only be used in the pages folder of your application as it would not work in the components folder. This is because asyncData hook gets called before your component is created.

Let us add asyncData to our index.vue file and observe how fast our incidents data loads. Add the following code after our components property and let us get rid of our mounted hook.
async asyncData({ $axios }) {
    let { data } = await $axios.get("/incidents");
    return { incidents: data.data.incidents };
  },
  // mounted() {
  //   this.getIncidents();
  // },
Here, the asyncData method accepts a property from the context $axios. We use this property to fetch the list of incidents and the value is then returned. This value is automatically injected into our component. Now, you can notice how fast your content loads if you refresh the page and at no time is there no incident to render.

Fetch

The Fetch method is also used to make requests on the server-side. It is called after the created hook in the life cycle which means it has access to the component’s data. Unlike the asyncData method, the fetch method can be used in all .vue files and be used with the Vuex store. This means that if you have the following in your data function.
data() {
    return {
      incidents: [],
      id: 5,
      gender: 'male'
    };
}
You can easily modify id or gender by calling this.id or this.gender.

Using Axios As A Plugin

During the process of development with Axios, you might find that you need extra configuration like creating instances and interceptors for your request so your application can work as intended and thankfully, we can do that by extending our Axios into a plugin.
To extend axios, you have to create a plugin (e.g. axios.js) in your plugins folder.
export default function ({
  $axios,
  store,
  redirect
}) {
  $axios.onError(error => {
    if (error.response && error.response.status === 500) {
      redirect('/login')
    }
  })
  $axios.interceptors.response.use(
    response => {
      if (response.status === 200) {
        if (response.request.responseURL && response.request.responseURL.includes('login')) {
          store.dispatch("setUser", response);
        }
      }
      return response
    }
  )
}
This is an example of a plugin I wrote for a Nuxt application. Here, your function takes in a context object of $axios, store and redirect which we would use in configuring the plugin. The first thing we do is to listen for an error with a status of 500 using $axios.onError and redirect the user to the login page.
We also have an interceptor that intercepts every request response we make in our application checks if the status of the response we get is 200. If that is true we proceed and check that there is a response.request.responseURL and if it includes login. If this checks out to be true, we then send this response using our store’s dispatch method where it then mutated in our state.
Add this plugin to your nuxt.config.js file:
plugins: [
    '~/plugins/axios'
  ]
After doing this, your Axios plugin would intercept any request you make and check if you have defined a special case for it.

Introduction To The Auth Module

The auth module is used for performing authentication for your Nuxt application and can be accessed from anywhere in your application using $this.auth. It is also available in fetch, asyncData, middleware and NuxtInitServer from the context object as $auth.
The context provides additional objects/params from Nuxt to Vue components and is available in special nuxt lifecycle areas like those mentioned above.
To use the auth module in your application, you would have to install it using yarn or npm.
YARN
yarn add @nuxtjs/auth
NPM
npm install @nuxtjs/auth
Add it to your nuxt.config.js file.
modules: [
  '@nuxtjs/auth'
],
auth: {
  // Options
}
The auth property accepts a list of properties such as strategies and redirect. Here, strategies accepts your preferred authentication method which can be:
  • local
    For username/email and password-based flow.
  • facebook
    For using Facebook accounts as a means of authentication.
  • Github
    For authenticating users with Github accounts.
  • Google
    For authenticating users with Google accounts.
  • Auth0
  • Laravel Passport
The redirect property accepts an object of links for:
  • login
    Users would be redirected to this link if login is required.
  • logout
    Users would be redirected here if after logout current route is protected.
  • home
    Users would be redirected here after login.
Now, let us add the following to our nuxt.config.js file.
/*
 ** Auth module configuration
 */
auth: {
  redirect: {
    login: '/login',
    logout: '/',
    home: '/my-reports'
  },
  strategies: {
    local: {
      endpoints: {
        login: {
          url: "/user/login",
          method: "post",
          propertyName: "data.token",
        },
        logout: false,
        user: false,
      },
      tokenType: '',
      tokenName: 'x-auth',
      autoFetchUser: false
    },
  },
}
Please note that the auth method works best when there is a user endpoint provided in the option above.
Inside the auth config object, we have a redirect option in which we set our login route to /login, logout route to / and home route to /my-reports which would all behave as expected. We also have a tokenType property which represents the Authorization type in the header of our Axios request. It is set to Bearer by default and can be changed to work with your API.
For our API, there is no token type and this is why we’re going to leave it as an empty string. The tokenName represents the Authorization name (or the header property you want to attach your token to) inside your header in your Axios request.
By default, it is set to Authorization but for our API, the Authorization name is x-auth. The autoFetchUser property is used to enable user fetch object using the user endpoint property after login. It is true by default but our API does not have a user endpoint so we have set that to false.
For this tutorial, we would be using the local strategy. In our strategies, we have the local option with endpoints for login, user and logout but in our case, we would only use the *login* option because our demo API does not have a *logout* endpoint and our user object is being returned when *login* is successful.
Note: The auth module does not have a register endpoint option so that means we’re going to register the traditional way and redirect the user to the login page where we will perform the authentication using this.$auth.loginWith. This is the method used in authenticating your users. It accepts a ‘strategy’ (e.g local) as a first argument and then an object to perform this authentication with. Take a look at the following example.
let data {
          email: 'test@test.com',
          password: '123456'
}
this.$auth.loginWith('local', { data })

Using The Auth Module

Now that we have configured our auth module, we can proceed to our registration page. If you visit the /register page, you should see a registration form.

Let us make this form functional by adding the following code:
methods: {
  async registerUser() {
    this.loading = true;
    let data = this.register;
    try {
      await this.$axios.post("/user/create", data);
      this.$router.push("/login");
      this.loading = false;
      this.$notify({
        group: "success",
        title: "Success!",
        text: "Account created successfully"
      });
    } catch (error) {
      this.loading = false;
      this.$notify({
        group: "error",
        title: "Error!",
        text: error.response
          ? error.response.data.error
          : "Sorry an error occured, check your internet"
      });
    }
  }
}
Here, we have an async function called registerUser which is tied to a click event in our template and makes an Axios request wrapped in a try/catch block to an endpoint /user/create. This redirects to the /login page and notifies the user of a successful registration. We also have a catch block that alerts the user of any error if the request is not successful.
If the registration is successful, you would be redirected to the login page.

Here, we’re going to make use of auth authentication method this.$auth.loginWith('local', loginData) after which we would use the this.$auth.setUser(userObj) to set the user in our auth instance.
To get the login page working, let’s add the following code to our login.vue file.
methods: {
  async logIn() {
    let data = this.login;
    this.loading = true;
    try {
      let res = await this.$auth.loginWith("local", {
        data
      });
      this.loading = false;
      let user = res.data.data.user;
      this.$auth.setUser(user);
      this.$notify({
        group: "success",
        title: "Success!",
        text: "Welcome!"
      });
    } catch (error) {
      this.loading = false;
      this.$notify({
        group: "error",
        title: "Error!",
        text: error.response
          ? error.response.data.error
          : "Sorry an error occured, check your internet"
      });
    }
  }
}
We created an async function called logIn using the auth method this.$auth.loginWith('local, loginData). If this login attempt is successful, we then assign the user data to our auth instance using this.$auth.setUser(userInfo) and redirect the user to the /my-report page.
You can now get user data using this.$auth.user or with Vuex using this.$store.state.auth.user but that’s not all. The auth instance contains some other properties which you can see if you log in or check your state using your Vue dev tools.
If you log this.$store.state.auth to the console, you’ll see this:
{
  "auth": {
    "user": {
      "id": "d7a5efdf-0c29-48aa-9255-be818301d602",
      "email": "tmxo@test.com",
      "lastName": "Xo",
      "firstName": "Tm",
      "othernames": null,
      "isAdmin": false,
      "phoneNumber": null,
      "username": null
    },
    "loggedIn": true,
    "strategy": "local",
    "busy": false
  }
}
The auth instance contains a loggedIn property that is useful in switching between authenticated links in the nav/header section of your application. It also contains a strategy method that states the type of strategy the instance is running (e.g local).
Now, we will make use of this loggedIn property to arrange our nav links. Update your navBar component to the following:
<template>
  <header class="header">
    <div class="logo">
      <nuxt-link to="/">
        <Logo />
      </nuxt-link>
    </div>
    <nav class="nav">
      <div class="nav__user" v-if="auth.loggedIn">
        <p>{{ auth.user.email }}</p>
        <button class="nav__link nav__link--long">
          <nuxt-link to="/report-incident">Report incident</nuxt-link>
        </button>
        <button class="nav__link nav__link--long">
          <nuxt-link to="/my-reports">My Reports</nuxt-link>
        </button>
        <button class="nav__link" @click.prevent="logOut">Log out</button>
      </div>
      <button class="nav__link" v-if="!auth.loggedIn">
        <nuxt-link to="/login">Login</nuxt-link>
      </button>
      <button class="nav__link" v-if="!auth.loggedIn">
        <nuxt-link to="/register">Register</nuxt-link>
      </button>
    </nav>
  </header>
</template>
<script>
import { mapState } from "vuex";
import Logo from "@/components/Logo";
export default {
  name: "nav-bar",
  data() {
    return {};
  },
  computed: {
    ...mapState(["auth"])
  },
  methods: {
    logOut() {
      this.$store.dispatch("logOut");
      this.$router.push("/login");
    }
  },
  components: {
    Logo
  }
};
</script>
<style></style>
In our template section, we have several links to different parts of the application in which we are now using auth.loggedIn to display the appropriate links depending on the authentication status. We have a logout button that has a click event with a logOut() function attached to it. We also display the user’s email gotten from the auth property which is accessed from our Vuex store using the mapState method which maps our state auth to the computed property of the nav component. We also have a logout method that calls our Vuex action logOut and redirects the user to the login page.
Now, let us go ahead and update our store to have a logOut action.
export const actions = {
    // ....
  logOut() {
    this.$auth.logout();
  }
}
The logOut action calls the auth logout method which clears user data, deletes tokens from localStorage and sets loggedIn to false.
Routes like /my-reports and report-incident should not be visible to guests but at this point in our app, that is not the case. Nuxt does not have a navigation guard that can protect your routes, but it has is the auth middleware. It gives you the freedom to create your own middleware so you can configure it to work the way you want.
It can be set in two ways:
  1. Per route.
  2. Globally for the whole app in your nuxt.config.js file.
router: {
  middleware: ['auth']
}
This auth middleware works with your auth instance so you do not need to create an auth.js file in your middleware folder.
Let us now add this middleware to our my-reports.vue and report-incident.vue files. Add the following lines of code to the script section of each file.
middleware: 'auth'
Now, our application would check if the user trying to access these routes has an auth.loggedIn value of true. It’ll redirect them to the login page using our redirect option in our auth config file — if you’re not logged in and you try to visit either /my-report or report-incident, you would be redirected to /login.


This page is for adding incidents but that right now the form does not send incident to our server because we are not making the call to the server when the user attempts to submit the form. To solve this, we will add a reportIncident method which will be called when the user clicks on Report. We’ll have this in the script section of the component. This method will send the form data to the server. Update your report-incident.vue file with the following:
<template>
  <section class="report">
    <h1 class="report__heading">Report an Incident</h1>
    <form class="report__form">
      <div class="input__container">
        <label for="title" class="input__label">Title</label>
        <input
          type="text"
          name="title"
          id="title"
          v-model="incident.title"
          class="input__field"
          required
        />
      </div>
      <div class="input__container">
        <label for="location" class="input__label">Location</label>
        <input
          type="text"
          name="location"
          id="location"
          v-model="incident.location"
          required
          class="input__field"
        />
      </div>
      <div class="input__container">
        <label for="comment" class="input__label">Comment</label>
        <textarea
          name="comment"
          id="comment"
          v-model="incident.comment"
          class="input__area"
          cols="30"
          rows="10"
          required
        ></textarea>
      </div>
      <input type="submit" value="Report" class="input__button" @click.prevent="reportIncident" />
      <p class="loading__indicator" v-if="loading">Please wait....</p>
    </form>
  </section>
</template>
<script>
export default {
  name: "report-incident",
  middleware: "auth",
  data() {
    return {
      loading: false,
      incident: {
        type: "red-flag",
        title: "",
        location: "",
        comment: ""
      }
    };
  },
  methods: {
    async reportIncident() {
      let data = this.incident;
      let formData = new FormData();
      formData.append("title", data.title);
      formData.append("type", data.type);
      formData.append("location", data.location);
      formData.append("comment", data.comment);
      this.loading = true;
      try {
        let res = await this.$store.dispatch("reportIncident", formData);
        this.$notify({
          group: "success",
          title: "Success",
          text: "Incident reported successfully!"
        });
        this.loading = false;
        this.$router.push("/my-reports");
      } catch (error) {
        this.loading = false;
        this.$notify({
          group: "error",
          title: "Error!",
          text: error.response
            ? error.response.data.error
            : "Sorry an error occured, check your internet"
        });
      }
    }
  }
};
</script>
<style>
</style>
Here, we have a form with input fields for title, location, and comment with two-way data binding using v-model. We also have a submit button with a click event. In the script section, we have a reportIncident method that collects all the information provided in the form and is sent to our server using FormData because the API is designed to also accept images and videos.
This formData is attached to a Vuex action using the dispatch method, if the request is successful, you get redirected to /my-reports with a notification informing you that this request was successful otherwise, you would be notified of an error with the error message.
At this point, we don’t have reportIncident action in our store yet so in your browser console, you would see an error if you try to click submit on this page.

To fix this, add the reportIncident action your index.js file.
   
export const actions = {
  // ...
  async reportIncident({}, data) {
    let res = await this.$axios.post('/incident/create', data)
    return res;
  }
}
Here, we have a reportIncident function that takes in an empty context object and the data we’re sending from our form. This data is then attached to a post request that creates an incident and returns back to our report-incident.vue file.
At this point, you should be able to add a report using the form after which you would be redirected to /my-reports page.

This page should display a list of incidents created by the user but right now it only shows what we see above, let’s go ahead to fix that.
We’re going to be using the fetch method we learned about to get this list. Update your my-reports.vue file with the following:
<script>
import incidentCard from "@/components/incidentCard.vue";
export default {
  middleware: "auth",
  name: "my-reports",
  data() {
    return {
      incidents: []
    };
  },
  components: {
    incidentCard
  },
  async fetch() {
    let { data } = await this.$axios.get("/user/incidents");
    this.incidents = data.data;
  }
};
</script>
Here, we use fetch method to get user-specific incidents and assign the response to our incidents array.

At this point, we would notice a difference in how fetch method and asyncData loads our data.

Conclusion

So far, we have learned about the Axios module and all of its features. We have also learned more about asyncData, and how we can fetch both of them together despite their differences. We’ve also learned how to perform authentication in our application using the auth module and how to use the auth middleware to protect our routes. Here are some useful resources that talk more about all we’ve covered.

Resources

  1. Auth Module,” NuxtJS.org
  2. Axios Module: Introduction,” NuxtJS.org
  3. FormData, MDN web docs
  4. API: The asyncData Method,” NuxtJS.org
  5. The Vue Instance: Lifecycle Diagram,” VueJS.org
  6. Understanding How fetch Works In Nuxt 2.12,” NuxtJS.org







Wednesday, October 9, 2019

Global vs. Local Styling In Next.js

 Next.js has strong opinions about how to organize JavaScript but not CSS. How can we develop patterns that encourage best CSS practices while also following the framework’s logic? The answer is surprisingly simple — to write well-structured CSS that balances global and local styling concerns.

I have had a great experience using Next.js to manage complex front-end projects. Next.js is opinionated about how to organize JavaScript code, but it doesn’t have built-in opinions about how to organize CSS.

After working within the framework, I have found a series of organizational patterns that I believe both conform to the guiding philosophies of Next.js and exercise best CSS practices. In this article, we’ll build a website (a tea shop!) together to demonstrate these patterns.

Note: You probably will not need prior Next.js experience, although it would be good to have a basic understanding of React and to be open to learning some new CSS techniques.

Writing “Old-Fashioned” CSS

When first looking into Next.js, we may be tempted to consider using some kind of CSS-in-JS library. Though there may be benefits depending on the project, CSS-in-JS introduces many technical considerations. It requires using a new external library, which adds to the bundle size. CSS-in-JS can also have a performance impact by causing additional renders and dependencies on the global state.

Furthermore, the whole point of using a library like Next.js is to statically render assets whenever possible, so it doesn’t make so much sense to write JS that needs to be run in the browser to generate CSS.

There are a couple of questions we have to consider when organizing style within Next.js:

How can we fit within the conventions/best practices of the framework?

How can we balance “global” styling concerns (fonts, colors, main layouts, and so on) with “local” ones (styles regarding individual components)?

The answer I have come up with for the first question is to simply write good ol’ fashioned CSS. Not only does Next.js support doing so with no additional setup; it also yields results that are performant and static.

To solve the second problem, I take an approach that can be summarized in four pieces:

  1. Design tokens
  2. Global styles
  3. Utility classes
  4. Component styles

I’m indebted to Andy Bell’s idea of CUBE CSS (“Composition, Utility, Block, Exception”) here. If you haven’t heard of this organizational principle before, I recommended checking out its official site or feature on the Smashing Podcast. One of the principles we will take from CUBE CSS is the idea that we should embrace rather than fear the CSS cascade. Let’s learn these techniques by applying them to a website project.

Getting Started #

We’ll be building a tea store because, well, tea is tasty. We’ll start by running yarn create next-app to make a new Next.js project. Then, we’ll remove everything in the styles/ directory (it’s all sample code).

Note: If you want to follow along with the finished project, you can check it out here.

Design Tokens #

In pretty much any CSS setup, there’s a clear benefit to storing all globally shared values in variables. If a client asks for a color to change, implementing the change is a one-liner rather than a massive find-and-replace mess. Consequently, a key part of our Next.js CSS setup will be storing all site-wide values as design tokens.

We’ll use inbuilt CSS Custom Properties to store these tokens. (If you’re not familiar with this syntax, you can check out “A Strategy Guide To CSS Custom Properties”.) I should mention that (in some projects) I’ve opted to use SASS/SCSS variables for this purpose. I haven’t found any real advantage, so I usually only include SASS in a project if I find I need other SASS features (mix-ins, iteration, importing files, and so on). CSS custom properties, by contrast, also work with the cascade and can be changed over time rather than statically compiling. So, for today, let’s stick with plain CSS.

In our styles/ directory, let’s make a new design_tokens.css file:

:root {
  --green: #3FE79E;
  --dark: #0F0235;
  --off-white: #F5F5F3;

  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;

  --font-size-sm: 0.5rem;
  --font-size-md: 1rem;
  --font-size-lg: 2rem;
}

Of course, this list can and will grow over time. Once we add this file, we need to hop over to our pages/_app.jsx file, which is the main layout for all our pages, and add:

import '../styles/design_tokens.css'

I like to think of design tokens as the glue that maintains consistency across the project. We will reference these variables on a global scale, as well as within individual components, ensuring a unified design language.

Global Styles #

Next up, let’s add a page to our website! Let’s hop into the pages/index.jsx file (this is our homepage). We’ll delete all the boilerplate and add something like:

export default function Home() {
  return <main>
    <h1>Soothing Teas</h1>

    <p>Welcome to our wonderful tea shop.</p>

    <p>We have been open since 1987 and serve customers with hand-picked oolong teas.</p>
  </main>
}

Unfortunately, it will look quite plain, so let’s set some global styles for basic elements, e.g. <h1> tags. (I like to think of these styles as “reasonable global defaults”.) We may override them in specific cases, but they’re a good guess as to what we will want if we don’t.

I’ll put this in the styles/globals.css file (which comes by default from Next.js):

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  color: var(--off-white);
  background-color: var(--dark);
}

h1 {
  color: var(--green);
  font-size: var(--font-size-lg);
}

p {
  font-size: var(--font-size-md);
}

p, article, section {
  line-height: 1.5;
}

:focus {
  outline: 0.15rem dashed var(--off-white);
  outline-offset: 0.25rem;
}
main:focus {
  outline: none;
}

img {
  max-width: 100%;
}

Of course, this version is fairly basic, but my globals.css file doesn’t usually end up actually needing to get too large. Here, I style basic HTML elements (headings, body, links, and so on). There is no need to wrap these elements in React components or to constantly add classes just to provide basic style.

I also include any resets of default browser styles. Occasionally, I will have some site-wide layout style to provide a “sticky footer”, for example, but they only belong here if all pages share the same layout. Otherwise, it will need to be scoped inside individual components.

I always include some kind of :focus styling to clearly indicate interactive elements for keyboard users when focused. It’s best to make it an integral part of the site’s design DNA!

Utility Classes #

One area where our homepage could certainly improve is that the text currently always extends to the sides of the screen, so let’s limit its width. We need this layout on this page, but I imagine that we might need it on other pages, too. This is a great use case for a utility class!

I try to use utility classes sparingly rather than as a replacement for just writing CSS. My personal criteria for when it makes sense to add one to a project are:

  1. I need it repeatedly;
  2. It does one thing well;
  3. It applies across a range of different components or pages.

I think this case meets all three criteria, so let’s make a new CSS file styles/utilities.css and add:

.lockup {
  max-width: 90ch;
  margin: 0 auto;
}

Then let’s add import '../styles/utilities.css' to our pages/_app.jsx. Finally, let’s change the <main> tag in our pages/index.jsx to <main className="lockup">.

Now, our page is coming together even more. Because we used the max-width property, we don’t need any media queries to make our layout mobile responsive. And, because we used the ch measurement unit — which equates to about the width of one character — our sizing is dynamic to the user’s browser font size.

As our website grows, we can continue adding more utility classes. I take a fairly utilitarian approach here: If I’m working and find I need another class for a color or something, I add it. I don’t add every possible class under the sun — it would bloat the CSS file size and make my code confusing. Sometimes, in larger projects, I like to break things up into a styles/utilities/ directory with a few different files; it’s up to the needs of the project.

We can think of utility classes as our toolkit of common, repeated styling commands that are shared globally. They help prevent us from constantly rewriting the same CSS between different components.

Component Styles #

We’ve finished our homepage for the moment, but we still need to build a piece of our website: the online store. Our goal here will be to display a card grid of all the teas we want to sell, so we’ll need to add some components to our site.

Let’s start off by adding a new page at pages/shop.jsx:

export default function Shop() {
  return <main>
    <div className="lockup">
      <h1>Shop Our Teas</h1>
    </div>

  </main>
}

Then, we’ll need some teas to display. We’ll include a name, description, and image (in the public/ directory) for each tea:

const teas = [
  { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" },
  // ...
]

Note: This isn’t an article about data fetching, so we took the easy route and defined an array at the beginning of the file.

Next, we’ll need to define a component to display our teas. Let’s start by making a components/ directory (Next.js doesn’t make this by default). Then, let’s add a components/TeaList directory. For any component that ends up needing more than one file, I usually put all the related files inside a folder. Doing so prevents our components/ folder from getting unnavigable.

Now, let’s add our components/TeaList/TeaList.jsx file:

import TeaListItem from './TeaListItem'

const TeaList = (props) => {
  const { teas } = props

  return <ul role="list">
    {teas.map(tea =>
      <TeaListItem tea={tea} key={tea.name} />)}
  </ul>
}

export default TeaList

The purpose of this component is to iterate over our teas and to show a list item for each one, so now let’s define our components/TeaList/TeaListItem.jsx component:

import Image from 'next/image'

const TeaListItem = (props) => {
  const { tea } = props

  return <li>
    <div>
      <Image src={tea.image} alt="" objectFit="cover" objectPosition="center" layout="fill" />
    </div>

  <div>
      <h2>{tea.name}</h2>
      <p>{tea.description}</p>
    </div>
  </li>
}

export default TeaListItem

Note that we’re using Next.js’s built-in image component. I set the alt attribute to an empty string because the images are purely decorative in this case; we want to avoid bogging screen reader users down with long image descriptions here.

Finally, let’s make a components/TeaList/index.js file, so that our components are easy to import externally:

import TeaList from './TeaList'
import TeaListItem from './TeaListItem'

export { TeaListItem }

export default TeaList

And then, let’s plug it all together by adding import TeaList from ../components/TeaList and a <TeaList teas={teas} /> element to our Shop page. Now, our teas will show up in a list, but it won’t be so pretty.

Colocating Style With Components Through CSS Modules #

Let’s start off by styling our cards (the TeaListLitem component). Now, for the first time in our project, we’re going to want to add style that is specific to just one component. Let’s create a new file components/TeaList/TeaListItem.module.css.

You may be wondering about the module in the file extension. This is a CSS Module. Next.js supports CSS modules and includes some good documentation on them. When we write a class name from a CSS module such as .TeaListItem, it will automatically get transformed into something more like . TeaListItem_TeaListItem__TFOk_ with a bunch of extra characters tacked on. Consequently, we can use any class name we want without being concerned that it will conflict with other class names elsewhere in our site.

Another advantage to CSS modules is performance. Next.js includes a dynamic import feature. next/dynamic lets us lazy load components so that their code only gets loaded when needed, rather than adding to the whole bundle size. If we import the necessary local styles into individual components, then users can also lazy load the CSS for dynamically imported components. For large projects, we may choose to lazy load significant chunks of our code and only to load the most necessary JS/CSS upfront. As a result, I usually end up making a new CSS Module file for every new component that needs local styling.

Let’s start by adding some initial styles to our file:

.TeaListItem {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  background-color: var(--color, var(--off-white));
  color: var(--dark);
  border-radius: 3px;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
}

Then, we can import style from ./TeaListItem.module.css in our TeaListitem component. The style variable comes in like a JavaScript object, so we can access this class-like style.TeaListItem.

Note: Our class name doesn’t need to be capitalized. I’ve found that a convention of capitalized class names inside of modules (and lowercase ones outside) differentiates local vs. global class names visually.

So, let’s take our new local class and assign it to the <li> in our TeaListItem component:

<li className={style.TeaListComponent}>

You may be wondering about the background color line (i.e. var(--color, var(--off-white));). What this snippet means is that by default the background will be our --off-white value. But, if we set a --color custom property on a card, it will override and choose that value instead.

At first, we’ll want all our cards to be --off-white, but we may want to change the value for individual cards later. This works very similarly to props in React. We can set a default value but create a slot where we can choose other values in specific circumstances. So, I encourage us to think of CSS custom properties like CSS’s version of props.

The style still won’t look great because we want to make sure that the images stay within their containers. Next.js’s Image component with the layout="fill" prop gets position: absolute; from the framework, so we can limit the size by putting in a container with position: relative;.

Let’s add a new class to our TeaListItem.module.css:

.ImageContainer {
  position: relative;
  width: 100%;
  height: 10em;
  overflow: hidden;
}

And then let’s add className={styles.ImageContainer} on the <div> that contains our <Image>. I use relatively “simple” names such as ImageContainer because we’re inside a CSS module, so we don’t have to worry about conflicting with the outside style.

Finally, we want to add a bit of padding on the sides of the text, so let’s add one last class and rely on the spacing variables we set up as design tokens:

.Title {
  padding-left: var(--space-sm);
  padding-right: var(--space-sm);
}

We can add this class to the <div> that contains our name and description. Now, our cards don’t look so bad.

Combining Global And Local Style #

Next, we want to have our cards show in a grid layout. In this case, we’re just at the border between local and global styles. We could certainly code our layout directly on the TeaList component. But, I could also imagine that having a utility class that turns a list into a grid layout could be useful in several other places.

Let’s take the global approach here and add a new utility class in our styles/utilities.css:

.grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--min-item-width, 30ch), 1fr));
  gap: var(--space-md);
}

Now, we can add the .grid class on any list, and we’ll get an automatically responsive grid layout. We can also change the --min-item-width custom property (by default 30ch) to change the minimum width of each element.

Note: Remember to think of custom properties like props! If this syntax looks unfamiliar, you can check out “Intrinsically Responsive CSS Grid With minmax() And min()” by Chris Coyier.

As we’ve written this style globally, it doesn’t require any fanciness to add className="grid" onto our TeaList component. But, let’s say we want to couple this global style with some additional local store. For example, we want to bring a bit more of the “tea aesthetic” in and to make every other card have a green background. All we’d need to do is make a new components/TeaList/TeaList.module.css file:

.TeaList > :nth-child(even) {
  --color: var(--green);
}

Remember how we made a --color custom property on our TeaListItem component? Well, now we can set it under specific circumstances. Note that we can still use child selectors within CSS modules, and it doesn’t matter that we’re selecting an element that is styled inside a different module. So, we can also use our local component styles to affect child components. This is a feature rather than a bug, as it allows us to take advantage of the CSS cascade! If we tried to replicate this effect some other way, we’d likely end up with some kind of JavaScript soup rather than three lines of CSS.

Then, how can we keep the global .grid class on our TeaList component while also adding the local .TeaList class? This is where the syntax can get a bit funky because we have to access our .TeaList class out of the CSS module by doing something like style.TeaList.

One option would be to use string interpolation to get something like:

<ul role="list" className={`${style.TeaList} grid`}>

In this small case, this might be good enough. If we’re mixing-and-matching more classes, I find that this syntax makes my brain explode a bit, so I will sometimes opt to use the classnames library. In this case, we end up with a more sensible-looking list:

<ul role="list" className={classnames(style.TeaList, "grid")}>

Now, we’ve finished up our Shop page, and we’ve made our TeaList component take advantage of both global and local styles.

A Balancing Act

We’ve now built our tea shop using only plain CSS to handle the styling. You may have noticed that we did not have to spend ages dealing with custom Webpack setups, installing external libraries, and so on. That’s because of the patterns that we’ve used work with Next.js out of the box. Furthermore, they encourage best CSS practices and fit naturally into the Next.js framework architecture.

Our CSS organization consisted of four key pieces:

  1. Design tokens,
  2. Global styles,
  3. Utility classes,
  4. Component styles.

As we continue building our site, our list of design tokens and utility classes will grow. Any styling that doesn’t make sense to add as a utility class, we can add into component styles using CSS modules. As a result, we can find a continuous balance between local and global styling concerns. We can also generate performant, intuitive CSS code that grows naturally alongside our Next.js site.