Tag: React

A Complete Walkthrough of GraphQL APIs with React and FaunaDB

As a web developer, there is an interesting bit of back and forth that always comes along with setting up a new application. Even using a full stack web framework like Ruby on Rails can be non-trivial to set up and deploy, especially if it’s your first time doing so in a while.

Personally I have always enjoyed being able to dig in and write the actual bit of application logic more so than setting up the apps themselves. Lately I have become a big fan of React applications together with a GraphQL API and storing state with the Apollo library.

Setting up a React application has become very easy in the past few years, but setting up a backend with a GraphQL API? Not so much. So when I was working on a project recently, I decided to look for an easier way to integrate a GraphQL API and was delighted to find FaunaDB.

FaunaDB is a NoSQL database as a service that makes provisioning a GraphQL API an incredibly simple process, and even comes with a free tier. Frankly I was surprised and really impressed with how quickly I was able to go from zero to a working API.

The service also touts its production readiness, with a focus on making scalability much easier than if you were managing your own backend. Although I haven’t explored its more advanced features yet, if it’s anything like my first impression then the prospects and implications of using FaunaDB are quite exciting. For now, I can confirm that for many of my projects, it provides an excellent solution for managing state together with a React application.

While working on my project, I did run into a few configuration issues when making all of the frameworks work together which I think could’ve been addressed with a guide that focuses on walking through standing up an application in its entirety. So in this article, I’m going to do a thorough walkthrough of setting up a small to-do React application on Heroku, then persisting data to that application with FaunaDB using the Apollo library. You can find the full source code here.

Our Application

For this walkthrough, we’re building a to-do list where a user can take the following actions:

  • Add a new item
  • Mark an item as complete
  • Remove an item

From a technical perspective, we’re going to accomplish this by doing the following:

  • Creating a React application
  • Deploying the application to Heroku
  • Provisioning a new FaunaDB database
  • Declaring a GraphQL API schema
  • Provisioning a new database key
  • Configuring Apollo in our React application to interact with our API
  • Writing application logic and consume our API to persist information

Here’s a preview of what the final result will look like:

Creating the React Application

First we’ll create a boilerplate React application and make sure it runs. Assuming you have create-react-app installed, the commands to create a new application are:

create-react-app fauna-todo cd fauna-todo yarn start

After which you should be able to head to http://localhost:3000 and see the generated homepage for the application.

Deploying to Heroku

As I mentioned above, deploying React applications has become awesomely easy over the last few years. I’m using Heroku here since it’s been my go-to platform as a service for a while now, but you could just as easily use another service like Netlify (though of course the configuration will be slightly different). Assuming you have a Heroku account and the Heroku CLI installed, then this article shows that you only need a few lines of code to create and deploy a React application.

git init heroku create -b https://github.com/mars/create-react-app-buildpack.git git push heroku master

And your app is deployed! To view it you can run:

heroku open

Provisioning a FaunaDB Database

Now that we have a React app up and running, let’s add persistence to the application using FaunaDB. Head to fauna.com to create a free account. After you have an account, click “New Database” on the dashboard, and enter in a name of your choosing:

Creating an API via GraphQL Schema in FaunaDB

In this example, we’re going to declare a GraphQL schema then use that file to automatically generate our API within FaunaDB. As a first stab at the schema for our to-do application, let’s suppose that there is simply a collection of “Items” with “name” as its sole field. Since I plan to build upon this schema later and like being able to see the schema itself at a glance, I’m going to create a schema.graphql file and add it to the top level of my React application. Here is the content for this file:

type Item {  name: String } type Query {  allItems: [Item!] }

If you’re unfamiliar with the concept of defining a GraphQL schema, think of it as a manifest for declaring what kinds of objects and queries are available within your API. In this case, we’re saying that there is going to be an Item type with a name string field and that we are going to have an explicit query allItems to look up all item records. You can read more about schemas in this Apollo article and types in this graphql.org article. FaunaDB also provides a reference document for declaring and importing a schema file.

We can now upload this schema.graphql file and use it to generate a GraphQL API. Head to the FaunaDB dashboard again and click on “GraphQL” then upload your newly created schema file here:

Congratulations! You have created a fully functional GraphQL API. This page turns into a “GraphQL Playground” which lets you interact with your API. Click on the “Docs” tab in the sidebar to see the available queries and mutations.

Note that in addition to our allItems query, FaunaDB has generated the following queries/mutations automatically on our behalf:

  • findItemByID
  • createItem
  • updateItem
  • deleteItem

All of these were derived by declaring the Item type in our schema file. Pretty cool right? Let’s give these queries and mutations a spin to familiarize ourselves with them. We can execute queries and mutations directly in the “GraphQL Playground.” Let’s first run a query for items. Enter this query into the left pane of the playground:

query MyItemQuery {  allItems {    data {     name    }  } }

Then click on the play button to run it:

The result is listed on the right pane, and unsurprisingly returns no results since we haven’t created any items yet. Fortunately createItem was one of the mutations that was automatically generated from the schema and we can use that to populate a sample item. Let’s run this mutation:

mutation MyItemCreation {  createItem(data: { name: "My first todo item" }) {    name  } }

You can see the result of the mutation in the right pane. It seems like our item was created successfully, but just to double check we can re-run our first query and see the result:

You can see that if we add our first query back to the left pane in the playground that the play button gives you a choice as to which operation you’d like to perform. Finally, note in step 3 of the screenshot above that our item was indeed created successfully.

In addition to running the query above, we can also look in the “Collections” tab of FaunaDB to view the collection directly:

Provisioning a New Database Key

Now that we have the database itself configured, we need a way for our React application to access it.

For the sake of simplicity in this application, this will be done with a secret key that we can add as an environment variable to our React application. We aren’t going to have authentication for individual users. Instead we’ll generate an application key which has permission to create, read, update, and delete items.

Authentication and authorization are substantial topics on their own — if you would like to learn more on how FaunaDB handles them as a follow up exercise to this guide, you can read all about the topic here.

The application key we generate has an associate set of permissions that are grouped together in a “role.” Let’s begin by first defining a role that has permission to perform CRUD operations on items, as well as perform the allItems query. Start by going to the “Security” tab, then clicking on “Manage Roles”:

There are 2 built in roles, admin and server. We could in theory use these roles for our key, but this is a bad idea as those keys would allow whoever has access to this key to perform database level operations such as creating new collections or even destroy the database itself. So instead, let’s create a new role by clicking on “New Custom Role” button:

You can name the role whatever you’d like, here we’re using the name ItemEditor and giving the role permission to read, write, create, and delete items — as well as permission to read the allItems index.

Save this role then, head to the “Security” tab and create a new key:

When creating a key, make sure to select “ItemEditor” for the role and whatever name you please:

Next you’ll be presented with your secret key which you’ll need to copy:

In order for React to load the key’s value as an environment variable, create a new file called .env.local which lives at the root level of your React application. In this file, add an entry for the generated key:

REACT_APP_FAUNA_SECRET=fnADzT7kXcACAFHdiKG-lIUWq-hfWIVxqFi4OtTv

Important: Since it’s not good practice to store secrets directly in source control in plain text, make sure that you also have a .gitignore file in your project’s root directory that contains .env.local so that your secrets won’t be added to your git repo and shared with others.

It’s critical that this variable’s name starts with “REACT_APP_” otherwise it won’t be recognized when the application is started. By adding the value to the .env.local file, it will still be loaded for the application when running locally. You’ll have to explicitly stop and restart your application with yarn start in order to see these changes take.

If you’re interested in reading more about how environment variables are loaded in apps created via create-react-app, there is a full explanation here. We’ll cover adding this secret as an environment variable in Heroku later on in this article.

Connecting to FaunaDB in React with Apollo

In order for our React application to interact with our GraphQL API, we need some sort of GraphQL client library. Fortunately for us, the Apollo client provides an elegant interface for making API requests as well as caching and interacting with the results.

To install the relevant Apollo packages we’ll need, run:

yarn add @apollo/client graphql @apollo/react-hooks

Now in your src directory of your application, add a new file named client.js with the following content:

import { ApolloClient, InMemoryCache } from "@apollo/client"; export const client = new ApolloClient({  uri: "https://graphql.fauna.com/graphql",  headers: {    authorization: `Bearer $ {process.env.REACT_APP_FAUNA_SECRET}`,  },  cache: new InMemoryCache(), });

What we’re doing here is configuring Apollo to make requests to our FaunaDB database. Specifically, the uri makes the request to Fauna itself, then the authorization header indicates that we’re connecting to the specific database instance for the provided key that we generated earlier.

There are 2 important implications from this snippet of code:

  1. The authorization header contains the key with the “ItemEditor” role, and is currently hard coded to use the same header regardless of which user is looking at our application. If you were to update this application to show a different to-do list for each user, you would need to login for each user and generate a token which could instead be passed in this header. Again, the FaunaDB documentation covers this concept if you care to learn more about it.
  2. As with any time you add a layer of caching to a system (as we are doing here with Apollo), you introduce the potential to have stale data. FaunaDB’s operations are strongly consistent, and you can configure Apollo’s fetchPolicy to minimize the potential for stale data. In order to prevent stale reads to our cache, we’ll use a combination of refetch queries and specifying response fields in our mutations.

Next we’ll replace the contents of the home page’s component. Head to App.js and replace its content with:

import React from "react"; import { ApolloProvider } from "@apollo/client"; import { client } from "./client"; function App() {  return (    <ApolloProvider client={client}>      <div style={{ padding: "5px" }}>        <h3>My Todo Items</h3>        <div>items to get loaded here</div>      </div>    </ApolloProvider>  ); }

Note: For this sample application I’m focusing on functionality over presentation, so you’ll see some inline styles. While I certainly wouldn’t recommend this for a production-grade application, I think it does at least demonstrate any added styling in the most straightforward manner within a small demo.

Visit http://localhost:3000 again and you’ll see:

Which contains the hard coded values we’ve set in our jsx above. What we would really like to see however is the to-do item we created in our database. In the src directory, let’s create a component called ItemList which lists out any items in our database:

import React from "react"; import gql from "graphql-tag"; import { useQuery } from "@apollo/react-hooks"; const ITEMS_QUERY = gql`  {    allItems {      data {        _id        name      }    }  } `; export function ItemList() {  const { data, loading } = useQuery(ITEMS_QUERY); if (loading) {    return "Loading...";  } return (    <ul>      {data.allItems.data.map((item) => {        return <li key={item._id}>{item.name}</li>;      })}    </ul>  ); }

Then update App.js to render this new component  —  see the full commit in this example’s source code to see this step in its entirety. Previewing your app in again, you’ll see that your to-do item has loaded:

Now is a good time to commit your progress in git. And since we’re using Heroku, deploying is a snap:

git push heroku master heroku open

When you run heroku open though, you’ll see that the page is blank. If we inspect the network traffic and request to FaunaDB, we’ll see an error about how the database secret is missing:

Which makes sense since we haven’t configured this value in Heroku yet. Let’s set it by going to the Heroku dashboard, selecting your application, then clicking on the “Settings” tab. In there you should add the REACT_APP_FAUNA_SECRET key and value used in the .env.local file earlier. Reusing this key is done for demonstration purposes. In a “real” application, you would probably have a separate database and separate keys for each environment.

If you would prefer to use the command line rather than Heroku’s web interface, you can use the following command and replace the secret with your key instead:

heroku config:set REACT_APP_FAUNA_SECRET=fnADzT7kXcACAFHdiKG-lIUWq-hfWIVxqFi4OtTv

Important: as noted in the Heroku docs, you need to trigger a deploy in order for this environment variable to apply in your app:

git commit — allow-empty -m 'Add REACT_APP_FAUNA_SECRET env var' git push heroku master heroku open

After running this last command, your Heroku-hosted app should appear and load the items from your database.

Adding New To-Do Items

We now have all of the pieces in place for accessing our FaunaDB database both locally and a hosted Heroku environment. Now adding items is as simple as calling the mutation we used in the GraphQL Playground earlier. Here is the code for an AddItem component, which uses a bare bones html form to call the createItem mutation:

import React from "react"; import gql from "graphql-tag"; import { useMutation } from "@apollo/react-hooks"; const CREATE_ITEM = gql`  mutation CreateItem($ data: ItemInput!) {    createItem(data: $ data) {      _id    }  } `; const ITEMS_QUERY = gql`  {    allItems {      data {        _id        name      }    }  } `; export function AddItem() {  const [showForm, setShowForm] = React.useState(false);  const [newItemName, setNewItemName] = React.useState(""); const [createItem, { loading }] = useMutation(CREATE_ITEM, {    refetchQueries: [{ query: ITEMS_QUERY }],    onCompleted: () => {      setNewItemName("");      setShowForm(false);    },  }); if (showForm) {    return (      <form        onSubmit={(e) => {          e.preventDefault();          createItem({ variables: { data: { name: newItemName } } });        }}      >        <label>          <input            disabled={loading}            type="text"            value={newItemName}            onChange={(e) => setNewItemName(e.target.value)}            style={{ marginRight: "5px" }}          />        </label>        <input disabled={loading} type="submit" value="Add" />      </form>    );  } return <button onClick={() => setShowForm(true)}>Add Item</button>; }

After adding a reference to AddItem in our App component, we can verify that adding items works as expected. Again, you can see the full commit in the demo app for a recap of this step.

Deleting New To-Do Items

Similar to how we called the automatically generated AddItem mutation to add new items, we can call the generated DeleteItem mutation to remove items from our list. Here’s what our updated ItemList component looks like after adding this mutation:

import React from "react"; import gql from "graphql-tag"; import { useMutation, useQuery } from "@apollo/react-hooks"; const ITEMS_QUERY = gql`  {    allItems {      data {        _id        name      }    }  } `; const DELETE_ITEM = gql`  mutation DeleteItem($ id: ID!) {    deleteItem(id: $ id) {      _id    }  } `; export function ItemList() {  const { data, loading } = useQuery(ITEMS_QUERY); const [deleteItem, { loading: deleteLoading }] = useMutation(DELETE_ITEM, {    refetchQueries: [{ query: ITEMS_QUERY }],  }); if (loading) {    return <div>Loading...</div>;  } return (    <ul>      {data.allItems.data.map((item) => {        return (          <li key={item._id}>            {item.name}{" "}            <button              disabled={deleteLoading}              onClick={(e) => {                e.preventDefault();                deleteItem({ variables: { id: item._id } });              }}            >              Remove            </button>          </li>        );      })}    </ul>  ); }

Reloading our app and adding another item should result in a page that looks like this:

If you click on the “Remove” button for any item, the DELETE_ITEM mutation is fired and the entire list of items is fired upon completion as specified per the refetchQuery option. 

One thing you may have noticed is that in our ITEMS_QUERY, we’re specifying _id as one of the fields we’d like in the result set from our query. This _id field is automatically generated by FaunaDB as a unique identifier for each collection, and should be used when updating or deleting a record.

Marking Items as Complete

This wouldn’t be a fully functional to-do list without the ability to mark items as complete! So let’s add that now. By the time we’re done, we expect the app to look like this:

The first step we need to take is updating our Item schema within FaunaDB since right now the only information we store about an item is its name. Heading to our schema.graphql file, we can add a new field to track the completion state for an item:

type Item {  name: String  isComplete: Boolean } type Query {  allItems: [Item!] }

Now head to the GraphQL tab in the FaunaDB console and click on the “Update Schema” link to upload the newly updated schema file.

Note: there is also an “Override Schema” option, which can be used to rewrite your database’s schema from scratch if you’d like. One consideration to make when choosing to override the schema completely is that the data is deleted from your database. This may be fine for a test environment, but a test or production environment would require a proper database migration instead.

Since the changes we’re making here are additive, there won’t be any conflict with the existing schema so we can keep our existing data.

You can view the mutation itself and its expected schema in the GraphQL Playground in FaunaDB:

This tells us that we can call the deleteItem mutation with a data parameter of type ItemInput. As with our other requests, we could test this mutation in the playground if we wanted. For now, we can add it directly to the application. In ItemList.js, let’s add this mutation with this code as outlined in the example repository.

The references to UPDATE_ITEM are the most relevant changes we’ve made. It’s interesting to note as well that we don’t need the refetchQueries parameter for this mutation. When the update mutation returns, Apollo updates the corresponding item in the cache based on its identifier field (_id in this case) so our React component re-renders appropriately as the cache updates.

We now have all of the functionality for an initial version of a to-do application. As a final step, push your branch one more time to Heroku:

git push heroku master heroku open

Conclusion

Take a moment to pat yourself on the back! You’ve created a brand-new React application, added persistence at a database level with FaunaDB, and can do deployments available to the entire world with the push of a branch to Heroku.

Now that we’ve covered some of the concepts behind provisioning and interacting with FaunaDB, setting up any similar project in the future is an amazingly fast process. Being able to provision a GraphQL-accessible database in minutes is a dream for me when it comes to spinning up a new project. Not only that, but this is a production grade database that you don’t have to worry about configuring or scaling — and instead get to focus on writing the rest of your application instead of playing the role of a database administrator.


The post A Complete Walkthrough of GraphQL APIs with React and FaunaDB appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,

Going Jamstack with React, Serverless, and Airtable

The best way to learn is to build. Let’s learn about this hot new buzzword, Jamstack, by building a site with React, Netlify (Serverless) Functions, and Airtable. One of the ingredients of Jamstack is static hosting, but that doesn’t mean everything on the site has to be static. In fact, we’re going to build an app with full-on CRUD capability, just like a tutorial for any web technology with more traditional server-side access might.

Why these technologies, you ask?

You might already know this, but the “JAM” in Jamstack stands for JavaScript, APIs, and Markup. These technologies individually are not new, so the Jamstack is really just a new and creative way to combine them. You can read more about it over at the Jamstack site.

One of the most important benefits of Jamstack is ease of deployment and hosting, which heavily influence the technologies we are using. By incorporating Netlify Functions (for backend CRUD operations with Airtable), we will be able to deploy our full-stack application to Netlify. The simplicity of this process is the beauty of the Jamstack.

As far as the database, I chose Airtable because I wanted something that was easy to get started with. I also didn’t want to get bogged down in technical database details, so Airtable fits perfectly. Here’s a few of the benefits of Airtable:

  1. You don’t have to deploy or host a database yourself
  2. It comes with an Excel-like GUI for viewing and editing data
  3. There’s a nice JavaScript SDK

What we’re building

For context going forward, we are going to build an app where you can use to track online courses that you want to take. Personally, I take lots of online courses, and sometimes it’s hard to keep up with the ones in my backlog. This app will let track those courses, similar to a Netflix queue.

 

One of the reasons I take lots of online courses is because I make courses. In fact, I have a new one available where you can learn how to build secure and production-ready Jamstack applications using React and Netlify (Serverless) Functions. We’ll cover authentication, data storage in Airtable, Styled Components, Continuous Integration with Netlify, and more! Check it out  →

Airtable setup

Let me start by clarifying that Airtable calls their databases “bases.” So, to get started with Airtable, we’ll need to do a couple of things.

  1. Sign up for a free account
  2. Create a new “base”
  3. Define a new table for storing courses

Next, let’s create a new database. We’ll log into Airtable, click on “Add a Base” and choose the “Start From Scratch” option. I named my new base “JAMstack Demos” so that I can use it for different projects in the future.

Next, let’s click on the base to open it.

You’ll notice that this looks very similar to an Excel or Google Sheets document. This is really nice for being able tower with data right inside of the dashboard. There are few columns already created, but we add our own. Here are the columns we need and their types:

  1. name (single line text)
  2. link (single line text)
  3. tags (multiple select)
  4. purchased (checkbox)

We should add a few tags to the tags column while we’re at it. I added “node,” “react,” “jamstack,” and “javascript” as a start. Feel free to add any tags that make sense for the types of classes you might be interested in.

I also added a few rows of data in the name column based on my favorite online courses:

  1. Build 20 React Apps
  2. Advanced React Security Patterns
  3. React and Serverless

The last thing to do is rename the table itself. It’s called “Table 1” by default. I renamed it to “courses” instead.

Locating Airtable credentials

Before we get into writing code, there are a couple of pieces of information we need to get from Airtable. The first is your API Key. The easiest way to get this is to go your account page and look in the “Overview” section.

Next, we need the ID of the base we just created. I would recommend heading to the Airtable API page because you’ll see a list of your bases. Click on the base you just created, and you should see the base ID listed. The documentation for the Airtable API is really handy and has more detailed instructions for find the ID of a base.

Lastly, we need the table’s name. Again, I named mine “courses” but use whatever you named yours if it’s different.

Project setup

To help speed things along, I’ve created a starter project for us in the main repository. You’ll need to do a few things to follow along from here:

  1. Fork the repository by clicking the fork button
  2. Clone the new repository locally
  3. Check out the starter branch with git checkout starter

There are lots of files already there. The majority of the files come from a standard create-react-app application with a few exceptions. There is also a functions directory which will host all of our serverless functions. Lastly, there’s a netlify.toml configuration file that tells Netlify where our serverless functions live. Also in this config is a redirect that simplifies the path we use to call our functions. More on this soon.

The last piece of the setup is to incorporate environment variables that we can use in our serverless functions. To do this install the dotenv package.

npm install dotenv

Then, create a .env file in the root of the repository with the following. Make sure to use your own API key, base ID, and table name that you found earlier.

AIRTABLE_API_KEY=<YOUR_API_KEY> AIRTABLE_BASE_ID=<YOUR_BASE_ID> AIRTABLE_TABLE_NAME=<YOUR_TABLE_NAME>

Now let’s write some code!

Setting up serverless functions

To create serverless functions with Netlify, we need to create a JavaScript file inside of our /functions directory. There are already some files included in this starter directory. Let’s look in the courses.js file first.

const  formattedReturn  =  require('./formattedReturn'); const  getCourses  =  require('./getCourses'); const  createCourse  =  require('./createCourse'); const  deleteCourse  =  require('./deleteCourse'); const  updateCourse  =  require('./updateCourse'); exports.handler  =  async  (event)  =>  {   return  formattedReturn(200, 'Hello World'); };

The core part of a serverless function is the exports.handler function. This is where we handle the incoming request and respond to it. In this case, we are accepting an event parameter which we will use in just a moment.

We are returning a call inside the handler to the formattedReturn function, which makes it a bit simpler to return a status and body data. Here’s what that function looks like for reference.

module.exports  =  (statusCode, body)  =>  {   return  {     statusCode,     body: JSON.stringify(body),   }; };

Notice also that we are importing several helper functions to handle the interaction with Airtable. We can decide which one of these to call based on the HTTP method of the incoming request.

  • HTTP GET → getCourses
  • HTTP POST → createCourse
  • HTTP PUT → updateCourse
  • HTTP DELETE → deleteCourse

Let’s update this function to call the appropriate helper function based on the HTTP method in the event parameter. If the request doesn’t match one of the methods we are expecting, we can return a 405 status code (method not allowed).

exports.handler = async (event) => {   if (event.httpMethod === 'GET') {     return await getCourses(event);   } else if (event.httpMethod === 'POST') {     return await createCourse(event);   } else if (event.httpMethod === 'PUT') {     return await updateCourse(event);   } else if (event.httpMethod === 'DELETE') {     return await deleteCourse(event);   } else {     return formattedReturn(405, {});   } };

Updating the Airtable configuration file

Since we are going to be interacting with Airtable in each of the different helper files, let’s configure it once and reuse it. Open the airtable.js file.

In this file, we want to get a reference to the courses table we created earlier. To do that, we create a reference to our Airtable base using the API key and the base ID. Then, we use the base to get a reference to the table and export it.

require('dotenv').config(); var Airtable = require('airtable'); var base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base(   process.env.AIRTABLE_BASE_ID ); const table = base(process.env.AIRTABLE_TABLE_NAME); module.exports = { table };

Getting courses

With the Airtable config in place, we can now open up the getCourses.js file and retrieve courses from our table by calling table.select().firstPage(). The Airtable API uses pagination so, in this case, we are specifying that we want the first page of records (which is 20 records by default).

const courses = await table.select().firstPage(); return formattedReturn(200, courses);

Just like with any async/await call, we need to handle errors. Let’s surround this snippet with a try/catch.

try {   const courses = await table.select().firstPage();   return formattedReturn(200, courses); } catch (err) {   console.error(err);   return formattedReturn(500, {}); }

Airtable returns back a lot of extra information in its records. I prefer to simplify these records with only the record ID and the values for each of the table columns we created above. These values are found in the fields property. To do this, I used the an Array map to format the data the way I want.

const { table } = require('./airtable'); const formattedReturn = require('./formattedReturn'); module.exports = async (event) => {   try {     const courses = await table.select().firstPage();     const formattedCourses = courses.map((course) => ({       id: course.id,       ...course.fields,     }));     return formattedReturn(200, formattedCourses);   } catch (err) {     console.error(err);     return formattedReturn(500, {});   } };

How do we test this out? Well, the netlify-cli provides us a netlify dev command to run our serverless functions (and our front-end) locally. First, install the CLI:

npm install -g netlify-cli

Then, run the netlify dev command inside of the directory.

This beautiful command does a few things for us:

  • Runs the serverless functions
  • Runs a web server for your site
  • Creates a proxy for front end and serverless functions to talk to each other on Port 8888.

Let’s open up the following URL to see if this works:

We are able to use /api/* for our API because of the redirect configuration in the netlify.toml file.

If successful, we should see our data displayed in the browser.

Creating courses

Let’s add the functionality to create a course by opening up the createCourse.js file. We need to grab the properties from the incoming POST body and use them to create a new record by calling table.create().

The incoming event.body comes in a regular string which means we need to parse it to get a JavaScript object.

const fields = JSON.parse(event.body);

Then, we use those fields to create a new course. Notice that the create() function accepts an array which allows us to create multiple records at once.

const createdCourse = await table.create([{ fields }]);

Then, we can return the createdCourse:

return formattedReturn(200, createdCourse);

And, of course, we should wrap things with a try/catch:

const { table } = require('./airtable'); const formattedReturn = require('./formattedReturn'); module.exports = async (event) => {   const fields = JSON.parse(event.body);   try {     const createdCourse = await table.create([{ fields }]);     return formattedReturn(200, createdCourse);   } catch (err) {     console.error(err);     return formattedReturn(500, {});   } };

Since we can’t perform a POST, PUT, or DELETE directly in the browser web address (like we did for the GET), we need to use a separate tool for testing our endpoints from now on. I prefer Postman, but I’ve heard good things about Insomnia as well.

Inside of Postman, I need the following configuration.

  • url: localhost:8888/api/courses
  • method: POST
  • body: JSON object with name, link, and tags

After running the request, we should see the new course record is returned.

We can also check the Airtable GUI to see the new record.

Tip: Copy and paste the ID from the new record to use in the next two functions.

Updating courses

Now, let’s turn to updating an existing course. From the incoming request body, we need the id of the record as well as the other field values.

We can specifically grab the id value using object destructuring, like so:

const {id} = JSON.parse(event.body);

Then, we can use the spread operator to grab the rest of the values and assign it to a variable called fields:

const {id, ...fields} = JSON.parse(event.body);

From there, we call the update() function which takes an array of objects (each with an id and fields property) to be updated:

const updatedCourse = await table.update([{id, fields}]);

Here’s the full file with all that together:

const { table } = require('./airtable'); const formattedReturn = require('./formattedReturn'); module.exports = async (event) => {   const { id, ...fields } = JSON.parse(event.body);   try {     const updatedCourse = await table.update([{ id, fields }]);     return formattedReturn(200, updatedCourse);   } catch (err) {     console.error(err);     return formattedReturn(500, {});   } };

To test this out, we’ll turn back to Postman for the PUT request:

  • url: localhost:8888/api/courses
  • method: PUT
  • body: JSON object with id (the id from the course we just created) and the fields we want to update (name, link, and tags)

I decided to append “Updated!!!” to the name of a course once it’s been updated.

We can also see the change in the Airtable GUI.

Deleting courses

Lastly, we need to add delete functionality. Open the deleteCourse.js file. We will need to get the id from the request body and use it to call the destroy() function.

const { id } = JSON.parse(event.body); const deletedCourse = await table.destroy(id);

The final file looks like this:

const { table } = require('./airtable'); const formattedReturn = require('./formattedReturn'); module.exports = async (event) => {   const { id } = JSON.parse(event.body);   try {     const deletedCourse = await table.destroy(id);     return formattedReturn(200, deletedCourse);   } catch (err) {     console.error(err);     return formattedReturn(500, {});   } };

Here’s the configuration for the Delete request in Postman.

  • url: localhost:8888/api/courses
  • method: PUT
  • body: JSON object with an id (the same id from the course we just updated)

And, of course, we can double-check that the record was removed by looking at the Airtable GUI.

Displaying a list of courses in React

Whew, we have built our entire back end! Now, let’s move on to the front end. The majority of the code is already written. We just need to write the parts that interact with our serverless functions. Let’s start by displaying a list of courses.

Open the App.js file and find the loadCourses function. Inside, we need to make a call to our serverless function to retrieve the list of courses. For this app, we are going to make an HTTP request using fetch, which is built right in.

Thanks to the netlify dev command, we can make our request using a relative path to the endpoint. The beautiful thing is that this means we don’t need to make any changes after deploying our application!

const res = await fetch('/api/courses'); const courses = await res.json();

Then, store the list of courses in the courses state variable.

setCourses(courses)

Put it all together and wrap it with a try/catch:

const loadCourses = async () => {   try {     const res = await fetch('/api/courses');     const courses = await res.json();     setCourses(courses);   } catch (error) {     console.error(error);   } };

Open up localhost:8888 in the browser and we should our list of courses.

Adding courses in React

Now that we have the ability to view our courses, we need the functionality to create new courses. Open up the CourseForm.js file and look for the submitCourse function. Here, we’ll need to make a POST request to the API and send the inputs from the form in the body.

The JavaScript Fetch API makes GET requests by default, so to send a POST, we need to pass a configuration object with the request. This options object will have these two properties.

  1. method → POST
  2. body → a stringified version of the input data
await fetch('/api/courses', {   method: 'POST',   body: JSON.stringify({     name,     link,     tags,   }), });

Then, surround the call with try/catch and the entire function looks like this:

const submitCourse = async (e) => {   e.preventDefault();   try {     await fetch('/api/courses', {       method: 'POST',       body: JSON.stringify({         name,         link,         tags,       }),     });     resetForm();     courseAdded();   } catch (err) {     console.error(err);   } };

Test this out in the browser. Fill in the form and submit it.

After submitting the form, the form should be reset, and the list of courses should update with the newly added course.

Updating purchased courses in React

The list of courses is split into two different sections: one with courses that have been purchased and one with courses that haven’t been purchased. We can add the functionality to mark a course “purchased” so it appears in the right section. To do this, we’ll send a PUT request to the API.

Open the Course.js file and look for the markCoursePurchased function. In here, we’ll make the PUT request and include both the id of the course as well as the properties of the course with the purchased property set to true. We can do this by passing in all of the properties of the course with the spread operator and then overriding the purchased property to be true.

const markCoursePurchased = async () => {   try {     await fetch('/api/courses', {       method: 'PUT',       body: JSON.stringify({ ...course, purchased: true }),     });     refreshCourses();   } catch (err) {     console.error(err);   } };

To test this out, click the button to mark one of the courses as purchased and the list of courses should update to display the course in the purchased section.

Deleting courses in React

And, following with our CRUD model, we will add the ability to delete courses. To do this, locate the deleteCourse function in the Course.js file we just edited. We will need to make a DELETE request to the API and pass along the id of the course we want to delete.

const deleteCourse = async () => {   try {     await fetch('/api/courses', {       method: 'DELETE',       body: JSON.stringify({ id: course.id }),     });     refreshCourses();   } catch (err) {     console.error(err);   } };

To test this out, click the “Delete” button next to the course and the course should disappear from the list. We can also verify it is gone completely by checking the Airtable dashboard.

Deploying to Netlify

Now, that we have all of the CRUD functionality we need on the front and back end, it’s time to deploy this thing to Netlify. Hopefully, you’re as excited as I am about now easy this is. Just make sure everything is pushed up to GitHub before we move into deployment.

If you don’t have a Netlify, account, you’ll need to create one (like Airtable, it’s free). Then, in the dashboard, click the “New site from Git” option. Select GitHub, authenticate it, then select the project repo.

Next, we need to tell Netlify which branch to deploy from. We have two options here.

  1. Use the starter branch that we’ve been working in
  2. Choose the master branch with the final version of the code

For now, I would choose the starter branch to ensure that the code works. Then, we need to choose a command that builds the app and the publish directory that serves it.

  1. Build command: npm run build
  2. Publish directory: build

Netlify recently shipped an update that treats React warnings as errors during the build proces. which may cause the build to fail. I have updated the build command to CI = npm run build to account for this.

Lastly, click on the “Show Advanced” button, and add the environment variables. These should be exactly as they were in the local .env that we created.

The site should automatically start building.

We can click on the “Deploys” tab in Netlify tab and track the build progress, although it does go pretty fast. When it is complete, our shiny new app is deployed for the world can see!

Welcome to the Jamstack!

The Jamstack is a fun new place to be. I love it because it makes building and hosting fully-functional, full-stack applications like this pretty trivial. I love that Jamstack makes us mighty, all-powerful front-end developers!

I hope you see the same power and ease with the combination of technology we used here. Again, Jamstack doesn’t require that we use Airtable, React or Netlify, but we can, and they’re all freely available and easy to set up. Check out Chris’ serverless site for a whole slew of other services, resources, and ideas for working in the Jamstack. And feel free to drop questions and feedback in the comments here!


The post Going Jamstack with React, Serverless, and Airtable appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Frontity is React for WordPress

Some developers just prefer working in React. I don’t blame them really, because I like React too. Maybe that’s what they learned first. I’ve been using it long enough there is just some comfort to it. But mostly it is the strong component model that I like. There is just something nice about a codebase where things are constructed from components with clear jobs and responsibilities.

It’s not terribly common to see WordPress sites built with React though. The standard way to use WordPress is through themes that are essentially styles and PHP files that handle the templating. Frontity is changing that though. Frontity is a React-powered framework that digests your WordPress site’s API and builds the entire front end in React with all the powerful tools you’ve come to expect from that type of environment.

OMG, Now That’s a Fast Setup

This is how I was able to get started. At the command line, I did:

npx frontity create my-app

Then I went into the folder it created and did:

npx frontity dev

That instantly spins up a site for you to start working with.

To make it feel more real for me, I did went into frontity.settings.js and changed the source API to point at CSS-Tricks:

{   name: "@frontity/wp-source",   state: {     source: {       api: "https://css-tricks.com/wp-json",     },   }, },

And now look at what I get:

That’s wild. For some projects, that’s straight up ready to deploy.

Check out their intro video which steps through this exact thing

Getting to Work

My first instinct with things like this is to get my hands into the styling right away. The theme that installs by default is the Mars theme and they have a nice guide to help wrap your mind around how it works. The theme uses Emotion for styling, so the components have styles you can mess with right in them. I found the <HeadContainer> component in index.js and immediately did the background: red change!

const HeadContainer = styled.div`   display: flex;   align-items: center;   flex-direction: column;   background-color: red; `;

It hot-module-reloaded that sucker instantly:

Is this one of those client-side only technologies?

That’s what I thought to myself. I mean, one of the advantages of using WordPress as-is is that you get the server rendering for free. That means no SEO worries (we know client-side rendered sites can take a week or more to be crawled for every change). That means resiliency and speed.

Frontity does do server side rendering! It uses Isomorphic rendering, meaning you need a Node server to render the pages, but that means the browser will get fully formed HTML for pages!

It’s a perfect match for Vercel, basically.

Similarly to how easy a new site is to scaffold and run in development, all you have to do to prep it for production is:

npx frontity build

Then run the Node server:

npx frontity serve

Cool.

I also really like that there is community around all this. If you need help, you’ll get it.

This is a best-of-all-worlds scenario.

I’m always very happy building sites with WordPress, and doubly so now that we have the block editor to use. I really like having an editor experience that helps me write and craft the kind of pages I want to create.

But I also like working with component-based architectures that have fast, easy-to-use, hot refreshing local development environments. Once you work in this kind of dev environment, it’s hard to use anything else! Beautiful DX.

And I also also want to make damn sure the sites I deploy to production are fast, robust, resilient, accessible, and SEO friendly.

I’d get all that with a Frontity site.


Another thing I like here is that Automattic themselves is on board with all this. Not just in spirit, but they are literally big investors. I think they are very smart to see this as an important part of the WordPress ecosystem. Building with WordPress doesn’t mean not building with React, especially with Frontity doing so much of the heavy lifting.

The post Frontity is React for WordPress appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

Everything You Need to Know About FLIP Animations in React

With a very recent Safari update, Web Animations API (WAAPI) is now supported without a flag in all modern browsers (except IE).  Here’s a handy Pen where you can check which features your browser supports. The WAAPI is a nice way to do animation (that needs to be done in JavaScript) because it’s native — meaning it requires no additional libraries to work. If you’re completely new to WAAPI, here’s a very good introduction by Dan Wilson.

One of the most efficient approaches to animation is FLIP. FLIP requires a bit of JavaScript to do its thing. 

Let’s take a look at the intersection of using the WAAPI, FLIP, and integrating all that into React. But we’ll start without React first, then get to that.

FLIP and WAAPI

FLIP animations are made much easier by the WAAPI!

Quick refresher on FLIP: The big idea is that you position the element where you want it to end up first. Next, apply transforms to move it to the starting position. Then unapply those transforms. 

Animating transforms is super efficient, thus FLIP is super efficient. Before WAAPI, we had to directly manipulate element’s styles to set transforms and wait for the next frame to unset/invert it:

// FLIP Before the WAAPI el.style.transform = `translateY(200px)`; 
 requestAnimationFrame(() => {   el.style.transform = ''; });

A lot of libraries are built upon this approach.  However, there are several problems with this:

  • Everything feels like a huge hack.
  • It is extremely difficult to reverse the FLIP animation. While CSS transforms are reversed “for free” once a class is removed, this is not the case here. Starting a new FLIP while a previous one is running can cause glitches. Reversing requires parsing a transform matrix with getComputedStyles and using it to calculate the current dimensions before setting a new animation.
  • Advanced animations are close to impossible. For example, to prevent distorting a scaled parent’s children, we need to have access to current scale value each frame. This can only be done by parsing the transform matrix.
  • There’s lots of browser gotchas. For example, sometimes getting a FLIP animation to work flawlessly in Firefox requires calling requestAnimationFrame twice:
requestAnimationFrame(() => {   requestAnimationFrame(() => {     el.style.transform = '';   }); });

We get none of these problems when WAAPI is used. Reversing can be painlessly done with the reverse function.The counter-scaling of children is also possible. And when there is a bug, it is easy to pinpoint the exact culprit since we’re only working with simple functions, like animate and reverse, rather than combing through things like the requestAnimationFrame approach. 

Here’s the outline of the WAAPI version:

el.classList.toggle('someclass'); const keyframes = /* Calculate the size/position diff */; el.animate(keyframes, 2000);

FLIP and React

To understand how FLIP animations work in React, it is important to know how and, most importantly, why they work in plain JavaScript. Recall the anatomy of a FLIP animation:

Diagram. Cache current site and position, make a style change, get new size and position, calculate the difference, set transforms, and cancel transforms. Each item has a purple background, except the last one, indicating they happen before paint.

Everything that has a purple background needs to happen before the “paint” step of rendering. Otherwise, we would see a flash of new styles for a moment which is not good. Things get a little bit more complicated in React since all DOM updates are done for us.

The magic of FLIP animations is that an element is transformed before the browser has a chance to paint. So how do we know the “before paint” moment in React?

Meet the useLayoutEffect hook. If you even wondered what is for… this is it! Anything we pass in this callback happens synchronously after DOM updates but before paint. In other words, this is a great place to set up a FLIP!

Let us do something the FLIP technique is very good for: animating the DOM position. There’s nothing CSS can do if we want to animate how an element moves from one DOM position to another. (Imagine completing a task in a to-do list and moving it to the list of “completed” tasks like when you click on items in the Pen below.)

Let’s look at the simplest example. Clicking on any of the two squares in the following Pen makes them swap positions. Without FLIP, it would happen instantly.

There’s a lot going on there. Notice how all work happens inside lifecycle hook callbacks: useEffect and useLayoutEffect. What makes it a little bit confusing is that the timeline of our FLIP animation is not obvious from code alone since it happens across two React renders. Here’s the anatomy of a React FLIP animation to show the different order of operations:

Diagram. Cache the size and position, retrieve previous size and position from cache, get new size and position, calculate the difference, and play the animation.

Although useEffect always runs after useLayoutEffect and after browser paint, it is important that we cache the element’s position and size after the first render. We won’t get a chance to do it on second render because useLayoutEffect is run after all DOM updates. But the procedure is essentially the same as with vanilla FLIP animations.

Caveats

Like most things, there are some caveats to consider when working with FLIP in React.

Keep it under 100ms

A FLIP animation is calculation. Calculation takes time and before you can show that smooth 60fps transform you need to do quite some work. People won’t notice a delay if it is under 100ms, so make sure everything is below that. The Performance tab in DevTools is a good place to check that.

Unnecessary renders

We can’t use useState for caching size, positions and animation objects because every setState will cause an unnecessary render and slow down the app. It can even cause bugs in the worst of cases. Try using useRef instead and think of it as an object that can be mutated without rendering anything.

Layout thrashing

Avoid repeatedly triggering browser layout. In the context of FLIP animations, that means avoid looping through elements and reading their position with getBoundingClientRect, then immediately animating them with animate. Batch “reads” and “writes” whenever possible. This will allow for extremely smooth animations.

Animation canceling

Try randomly clicking on the squares in the earlier demo while they move, then again after they stop. You will see glitches. In real life, users will interact with elements while they move, so it’s worth making sure they are canceled, paused, and updated smoothly. 

However, not all animations can be reversed with reverse. Sometimes, we want them to stop and then move to a new position (like when randomly shuffling a list of elements). In this case, we need to:

  • obtain a size/position of a moving element
  • finish the current animation
  • calculate the new size and position differences
  • start a new animation

In React, this can be harder than it seems. I wasted a lot of time struggling with it. The current animation object must be cached. A good way to do it is to create a Map so to get the animation by an ID. Then, we need to obtain the size and position of the moving element. There are two ways to do it:

  1. Use a function component: Simply loop through every animated element right in the body of the function and cache the current positions.
  2. Use a class component: Use the getSnapshotBeforeUpdate lifecycle method.

In fact, official React docs recommend using getSnapshotBeforeUpdate “because there may be delays between the “render” phase lifecycles (like render) and “commit” phase lifecycles (like getSnapshotBeforeUpdate and componentDidUpdate).” However, there is no hook counterpart of this method yet. I found that using the body of the function component is fine enough.

Don’t fight the browser

I’ve said it before, but avoid fighting the browser and try to make things happen the way the browser would do it. If we need to animate a simple size change, then consider whether CSS would suffice (e.g.  transform: scale()) . I’ve found that FLIP animations are used best where browsers really can’t help:

  • Animating DOM position change (as we did above)
  • Sharing layout animations

The second is a more complicated version of the first. There are two DOM elements that act and look as one changing its position (while another is unmounted/hidden). This tricks enables some cool animations. For example, this animation is made with a library I built called react-easy-flip that uses this approach:

Libraries

There are quite a few libraries that make FLIP animations in React easier and abstract the boilerplate. Ones that are currently maintained actively include: react-flip-toolkit and mine, react-easy-flip.

If you do not mind something heavier but capable of more general animations, check out framer-motion. It also does cool shared layout animations! There is a video digging into that library.


Resources and references

The post Everything You Need to Know About FLIP Animations in React appeared first on CSS-Tricks.

CSS-Tricks

, , , , , ,
[Top]

React Single File Components Are Here

Shawn Wang is talking about RedwoodJS here:

…  it is the first time React components are being expressed in a single file format with explicit conventions.

Which is the RedwoodJS idea of Cells. To me, it feels like a slightly cleaner version of how Apollo wants you to do it with useQuery. Shawn makes that same connection and I know RedwoodJS uses Apollo, so I’m thinking it’s some nice semantic sugar.

There is a lot of cool stuff going on in RedwoodJS. “A highly opinionated stack” if its helpful to think of it that way, but Tom made clear in our last episode of ShopTalk that it’s not like Rails. Not that Rails is bad (it isn’t), but that this new world can do things in new and better ways that make for long-term healthy software.

The post React Single File Components Are Here appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

The Anatomy of a Tablist Component in Vanilla JavaScript Versus React

If you follow the undercurrent of the JavaScript community, there seems to be a divide as of late. It goes back over a decade. Really, this sort of strife has always been. Perhaps it is human nature.

Whenever a popular framework gains traction, you inevitably see people comparing it to rivals. I suppose that is to be expected. Everyone has a particular favorite.

Lately, the framework everyone loves (to hate?) is React. You often see it pitted against others in head-to-head blog posts and feature comparison matrices of enterprise whitepapers. Yet a few years ago, it seemed like jQuery would forever be king of the hill.

Frameworks come and go. To me, what is more interesting is when React — or any JS framework for that matter — gets pitted against the programming language itself. Because of course, under the hood, it is all built atop JS.

The two are not inherently at odds. I would even go so far as to say that if you do not have a good handle on JS fundamentals, you probably are not going to reap the full benefits of using React. It can still be helpful, similar to using a jQuery plugin without understanding its internals. But I feel like React presupposes more JS familiarity.

HTML is equally important. There exists a fair bit of FUD around how React affects accessibility. I think this narrative is inaccurate. In fact, the ESLint JSX a11y plugin will warn of possible accessibility violations in the console.

Console warnings from eslint-jsx-a11y-plugin
ESLint warnings about empty <a> tags

Recently, an annual study of the top 1 million sites was released. It shows that for sites using JS frameworks, there is an increased likelihood of accessibility problems. This is correlation, not causation.

This does not necessarily mean that the frameworks caused these errors, but it does indicate that home pages with these frameworks had more errors than on average.

In a manner of speaking, React’s magic incantations work regardless of whether you recognize the words. Ultimately, you are still responsible for the outcome.

Philosophical musings aside, I am a firm believer in choosing the best tool for the job. Sometimes, that means building a single page app with a Jamstack approach. Or maybe a particular project is better suited to offloading HTML rendering to the server, where it has historically been handled.

Either way, there inevitably comes the need for JS to augment the user experience. At Reaktiv Studios, to that end I have been attempting to keep most of our React components in sync with our “flat HTML” approach. I have been writing commonly used functionality in vanilla JS as well. This keeps our options open, so that our clients are free to choose. It also allows us to reuse the same CSS.

If I may, I would like to share how I built our <Tabs> and <Accordion> React components. I will also demonstrate how I wrote the same functionality without using a framework.

Hopefully, this lesson will feel like we are making a layered cake. Let us first start with the base markup, then cover the vanilla JS, and finish with how it works in React.

For reference, you can tinker with our live examples:

Reaktiv Studios UI components
Reaktiv Studios UI components

Flat HTML examples

Since we need JavaScript to make interactive widgets either way, I figured the easiest approach — from a server side implementation standpoint — would be to require only the bare minimum HTML. The rest can be augmented with JS.

The following are examples of markup for tabs and accordion components, showing a before/after comparison of how JS affects the DOM.

I have added id="TABS_ID" and id="ACCORDION_ID" for demonstrative purposes. This is to make it more obvious what is happening. But the JS that I will be explaining automatically generates unique IDs if nothing is supplied in the HTML. It would work fine either way, with or without an id specified.

<div class="tabs" id="TABS_ID">   <ul class="tabs__list">     <li class="tabs__item">       Tab 1     </li>     <!-- .tabs__item -->      <li class="tabs__item">       Tab 2     </li>     <!-- .tabs__item -->      <li class="tabs__item" disabled>       Tab 3 (disabled)     </li>     <!-- .tabs__item -->   </ul>   <!-- .tabs__list -->    <div class="tabs__panel">     <p>       Tab 1 content     </p>   </div>   <!-- .tabs__panel -->    <div class="tabs__panel">     <p>       Tab 2 content     </p>   </div>   <!-- .tabs__panel -->    <div class="tabs__panel">     <p>       NOTE: This tab is disabled.     </p>   </div>   <!-- .tabs__panel --> </div> <!-- .tabs -->

Tabs (with ARIA)

<div class="tabs" id="TABS_ID">   <ul class="tabs__list" role="tablist">     <li       aria-controls="tabpanel_TABS_ID_0"       aria-selected="false"       class="tabs__item"       id="tab_TABS_ID_0"       role="tab"       tabindex="0"     >       Tab 1     </li>     <!-- .tabs__item -->      <li       aria-controls="tabpanel_TABS_ID_1"       aria-selected="true"       class="tabs__item"       id="tab_TABS_ID_1"       role="tab"       tabindex="0"     >       Tab 2     </li>     <!-- .tabs__item -->      <li       aria-controls="tabpanel_TABS_ID_2"       aria-disabled="true"       aria-selected="false"       class="tabs__item"       disabled       id="tab_TABS_ID_2"       role="tab"     >       Tab 3 (disabled)     </li>     <!-- .tabs__item -->   </ul>   <!-- .tabs__list -->    <div     aria-hidden="true"     aria-labelledby="tab_TABS_ID_0"     class="tabs__panel"     id="tabpanel_TABS_ID_0"     role="tabpanel"   >     <p>       Tab 1 content     </p>   </div>   <!-- .tabs__panel -->    <div     aria-hidden="false"     aria-labelledby="tab_TABS_ID_1"     class="tabs__panel"     id="tabpanel_TABS_ID_1"     role="tabpanel"   >     <p>       Tab 2 content     </p>   </div>   <!-- .tabs__panel -->    <div     aria-hidden="true"     aria-labelledby="tab_TABS_ID_2"     class="tabs__panel"     id="tabpanel_TABS_ID_2"     role="tabpanel"   >     <p>       NOTE: This tab is disabled.     </p>   </div>   <!-- .tabs__panel --> </div> <!-- .tabs -->

Accordion (without ARIA)

<div class="accordion" id="ACCORDION_ID">   <div class="accordion__item">     Tab 1   </div>   <!-- .accordion__item -->    <div class="accordion__panel">     <p>       Tab 1 content     </p>   </div>   <!-- .accordion__panel -->    <div class="accordion__item">     Tab 2   </div>   <!-- .accordion__item -->    <div class="accordion__panel">     <p>       Tab 2 content     </p>   </div>   <!-- .accordion__panel -->    <div class="accordion__item" disabled>     Tab 3 (disabled)   </div>   <!-- .accordion__item -->    <div class="accordion__panel">     <p>       NOTE: This tab is disabled.     </p>   </div>   <!-- .accordion__panel --> </div> <!-- .accordion -->

Accordion (with ARIA)

<div   aria-multiselectable="true"   class="accordion"   id="ACCORDION_ID"   role="tablist" >   <div     aria-controls="tabpanel_ACCORDION_ID_0"     aria-selected="true"     class="accordion__item"     id="tab_ACCORDION_ID_0"     role="tab"     tabindex="0"   >     <i aria-hidden="true" class="accordion__item__icon"></i>     Tab 1   </div>   <!-- .accordion__item -->    <div     aria-hidden="false"     aria-labelledby="tab_ACCORDION_ID_0"     class="accordion__panel"     id="tabpanel_ACCORDION_ID_0"     role="tabpanel"   >     <p>       Tab 1 content     </p>   </div>   <!-- .accordion__panel -->    <div     aria-controls="tabpanel_ACCORDION_ID_1"     aria-selected="false"     class="accordion__item"     id="tab_ACCORDION_ID_1"     role="tab"     tabindex="0"   >     <i aria-hidden="true" class="accordion__item__icon"></i>     Tab 2   </div>   <!-- .accordion__item -->    <div     aria-hidden="true"     aria-labelledby="tab_ACCORDION_ID_1"     class="accordion__panel"     id="tabpanel_ACCORDION_ID_1"     role="tabpanel"   >     <p>       Tab 2 content     </p>   </div>   <!-- .accordion__panel -->    <div     aria-controls="tabpanel_ACCORDION_ID_2"     aria-disabled="true"     aria-selected="false"     class="accordion__item"     disabled     id="tab_ACCORDION_ID_2"     role="tab"   >     <i aria-hidden="true" class="accordion__item__icon"></i>     Tab 3 (disabled)   </div>   <!-- .accordion__item -->    <div     aria-hidden="true"     aria-labelledby="tab_ACCORDION_ID_2"     class="accordion__panel"     id="tabpanel_ACCORDION_ID_2"     role="tabpanel"   >     <p>       NOTE: This tab is disabled.     </p>   </div>   <!-- .accordion__panel --> </div> <!-- .accordion -->

Vanilla JavaScript examples

Okay. Now that we have seen the aforementioned HTML examples, let us walk through how we get from before to after.

First, I want to cover a few helper functions. These will make more sense in a bit. I figure it is best to get them documented first, so we can stay focused on the rest of the code once we dive in further.

File: getDomFallback.js

This function provides common DOM properties and methods as no-op, rather than having to make lots of typeof foo.getAttribute checks and whatnot. We could forego those types of confirmations altogether.

Since live HTML changes can be a potentially volatile environment, I always feel a bit safer making sure my JS is not bombing out and taking the rest of the page with it. Here is what that function looks like. It simply returns an object with the DOM equivalents of falsy results.

/*   Helper to mock DOM methods, for   when an element might not exist. */ const getDomFallback = () => {   return {     // Props.     children: [],     className: '',     classList: {       contains: () => false,     },     id: '',     innerHTML: '',     name: '',     nextSibling: null,     previousSibling: null,     outerHTML: '',     tagName: '',     textContent: '',      // Methods.     appendChild: () => Object.create(null),     cloneNode: () => Object.create(null),     closest: () => null,     createElement: () => Object.create(null),     getAttribute: () => null,     hasAttribute: () => false,     insertAdjacentElement: () => Object.create(null),     insertBefore: () => Object.create(null),     querySelector: () => null,     querySelectorAll: () => [],     removeAttribute: () => undefined,     removeChild: () => Object.create(null),     replaceChild: () => Object.create(null),     setAttribute: () => undefined,   }; };  // Export. export { getDomFallback };

File: unique.js

This function is a poor man’s UUID equivalent.

It generates a unique string that can be used to associate DOM elements with one another. It is handy, because then the author of an HTML page does not have to ensure that every tabs and accordion component have unique IDs. In the previous HTML examples, this is where TABS_ID and ACCORDION_ID would typically contain the randomly generated numeric strings instead.

// ========== // Constants. // ==========  const BEFORE = '0.'; const AFTER = '';  // ================== // Get unique string. // ==================  const unique = () => {   // Get prefix.   let prefix = Math.random();   prefix = String(prefix);   prefix = prefix.replace(BEFORE, AFTER);    // Get suffix.   let suffix = Math.random();   suffix = String(suffix);   suffix = suffix.replace(BEFORE, AFTER);    // Expose string.   return `$ {prefix}_$ {suffix}`; };  // Export. export { unique };

On larger JavaScript projects, I would typically use npm install uuid. But since we are keeping this simple and do not require cryptographic parity, concatenating two lightly edited Math.random() numbers will suffice for our string uniqueness needs.

File: tablist.js

This file does the bulk of the work. What is cool about it, if I do say so myself, is that there are enough similarities between a tabs component and an accordion that we can handle both with the same *.js file. Go ahead and scroll through the entirety, and then we will break down what each function does individually.

// Helpers. import { getDomFallback } from './getDomFallback'; import { unique } from './unique';  // ========== // Constants. // ==========  // Boolean strings. const TRUE = 'true'; const FALSE = 'false';  // ARIA strings. const ARIA_CONTROLS = 'aria-controls'; const ARIA_DISABLED = 'aria-disabled'; const ARIA_LABELLEDBY = 'aria-labelledby'; const ARIA_HIDDEN = 'aria-hidden'; const ARIA_MULTISELECTABLE = 'aria-multiselectable'; const ARIA_SELECTED = 'aria-selected';  // Attribute strings. const DISABLED = 'disabled'; const ID = 'id'; const ROLE = 'role'; const TABLIST = 'tablist'; const TABINDEX = 'tabindex';  // Event strings. const CLICK = 'click'; const KEYDOWN = 'keydown';  // Key strings. const ENTER = 'enter'; const FUNCTION = 'function';  // Tag strings. const LI = 'li';  // Selector strings. const ACCORDION_ITEM_ICON = 'accordion__item__icon'; const ACCORDION_ITEM_ICON_SELECTOR = `.$ {ACCORDION_ITEM_ICON}`;  const TAB = 'tab'; const TAB_SELECTOR = `[$ {ROLE}=$ {TAB}]`;  const TABPANEL = 'tabpanel'; const TABPANEL_SELECTOR = `[$ {ROLE}=$ {TABPANEL}]`;  const ACCORDION = 'accordion'; const TABLIST_CLASS_SELECTOR = '.accordion, .tabs'; const TAB_CLASS_SELECTOR = '.accordion__item, .tabs__item'; const TABPANEL_CLASS_SELECTOR = '.accordion__panel, .tabs__panel';  // =========== // Get tab ID. // ===========  const getTabId = (id = '', index = 0) => {   return `tab_$ {id}_$ {index}`; };  // ============= // Get panel ID. // =============  const getPanelId = (id = '', index = 0) => {   return `tabpanel_$ {id}_$ {index}`; };  // ============== // Click handler. // ==============  const globalClick = (event = {}) => {   // Get target.   const { key = '', target = getDomFallback() } = event;    // Get parent.   const { parentNode = getDomFallback(), tagName = '' } = target;    // Set later.   let wrapper = getDomFallback();    /*     =====     NOTE:     =====      We test for this, because the method does     not exist on `document.documentElement`.   */   if (typeof target.closest === FUNCTION) {     // Get wrapper.     wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback();   }    // Is `<li>`?   const isListItem = tagName.toLowerCase() === LI;    // Is multi?   const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE;    // Valid key?   const isValidKey = !key || key.toLowerCase() === ENTER;    // Valid target?   const isValidTarget =     !target.hasAttribute(DISABLED) &&     target.getAttribute(ROLE) === TAB &&     parentNode.getAttribute(ROLE) === TABLIST;    // Valid event?   const isValidEvent = isValidKey && isValidTarget;    // Continue?   if (isValidEvent) {     // Get panel.     const panelId = target.getAttribute(ARIA_CONTROLS);     const panel = wrapper.querySelector(`#$ {panelId}`) || getDomFallback();      // Get booleans.     let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE;     let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE;      // List item?     if (isListItem) {       boolPanel = FALSE;       boolTab = TRUE;     }      // [aria-multiselectable="false"]     if (!isMulti) {       // Get tabs & panels.       const childTabs = wrapper.querySelectorAll(TAB_SELECTOR);       const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR);        // Loop through tabs.       childTabs.forEach((tab = getDomFallback()) => {         tab.setAttribute(ARIA_SELECTED, FALSE);       });        // Loop through panels.       childPanels.forEach((panel = getDomFallback()) => {         panel.setAttribute(ARIA_HIDDEN, TRUE);       });     }      // Set individual tab.     target.setAttribute(ARIA_SELECTED, boolTab);      // Set individual panel.     panel.setAttribute(ARIA_HIDDEN, boolPanel);   } };  // ==================== // Add ARIA attributes. // ====================  const addAriaAttributes = () => {   // Get elements.   const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR);    // Loop through.   allWrappers.forEach((wrapper = getDomFallback()) => {     // Get attributes.     const { id = '', classList } = wrapper;     const parentId = id || unique();      // Is accordion?     const isAccordion = classList.contains(ACCORDION);      // Get tabs & panels.     const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR);     const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR);      // Add ID?     if (!wrapper.getAttribute(ID)) {       wrapper.setAttribute(ID, parentId);     }      // Add multi?     if (isAccordion && wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) {       wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE);     }      // ===========================     // Loop through tabs & panels.     // ===========================      for (let index = 0; index < childTabs.length; index++) {       // Get elements.       const tab = childTabs[index] || getDomFallback();       const panel = childPanels[index] || getDomFallback();        // Get IDs.       const tabId = getTabId(parentId, index);       const panelId = getPanelId(parentId, index);        // ===================       // Add tab attributes.       // ===================        // Tab: add icon?       if (isAccordion) {         // Get icon.         let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR);          // Create icon?         if (!icon) {           icon = document.createElement(I);           icon.className = ACCORDION_ITEM_ICON;           tab.insertAdjacentElement(AFTER_BEGIN, icon);         }          // [aria-hidden="true"]         icon.setAttribute(ARIA_HIDDEN, TRUE);       }        // Tab: add id?       if (!tab.getAttribute(ID)) {         tab.setAttribute(ID, tabId);       }        // Tab: add controls?       if (!tab.getAttribute(ARIA_CONTROLS)) {         tab.setAttribute(ARIA_CONTROLS, panelId);       }        // Tab: add selected?       if (!tab.getAttribute(ARIA_SELECTED)) {         const bool = !isAccordion && index === 0;          tab.setAttribute(ARIA_SELECTED, bool);       }        // Tab: add role?       if (tab.getAttribute(ROLE) !== TAB) {         tab.setAttribute(ROLE, TAB);       }        // Tab: add tabindex?       if (tab.hasAttribute(DISABLED)) {         tab.removeAttribute(TABINDEX);         tab.setAttribute(ARIA_DISABLED, TRUE);       } else {         tab.setAttribute(TABINDEX, 0);       }        // Tab: first item?       if (index === 0) {         // Get parent.         const { parentNode = getDomFallback() } = tab;          /*           We do this here, instead of outside the loop.            The top level item isn't always the `tablist`.            The accordion UI only has `<dl>`, whereas           the tabs UI has both `<div>` and `<ul>`.         */         if (parentNode.getAttribute(ROLE) !== TABLIST) {           parentNode.setAttribute(ROLE, TABLIST);         }       }        // =====================       // Add panel attributes.       // =====================        // Panel: add ID?       if (!panel.getAttribute(ID)) {         panel.setAttribute(ID, panelId);       }        // Panel: add hidden?       if (!panel.getAttribute(ARIA_HIDDEN)) {         const bool = isAccordion || index !== 0;          panel.setAttribute(ARIA_HIDDEN, bool);       }        // Panel: add labelled?       if (!panel.getAttribute(ARIA_LABELLEDBY)) {         panel.setAttribute(ARIA_LABELLEDBY, tabId);       }        // Panel: add role?       if (panel.getAttribute(ROLE) !== TABPANEL) {         panel.setAttribute(ROLE, TABPANEL);       }     }   }); };  // ===================== // Remove global events. // =====================  const unbind = () => {   document.removeEventListener(CLICK, globalClick);   document.removeEventListener(KEYDOWN, globalClick); };  // ================== // Add global events. // ==================  const init = () => {   // Add attributes.   addAriaAttributes();    // Prevent doubles.   unbind();    document.addEventListener(CLICK, globalClick);   document.addEventListener(KEYDOWN, globalClick); };  // ============== // Bundle object. // ==============  const tablist = {   init,   unbind, };  // ======= // Export. // =======  export { tablist };

Function: getTabId and getPanelId

These two functions are used to create individually unique IDs for elements in a loop, based on an existing (or generated) parent ID. This is helpful to ensure matching values for attributes like aria-controls="…" and aria-labelledby="…". Think of those as the accessibility equivalents of <label for="…">, telling the browser which elements are related to one another.

const getTabId = (id = '', index = 0) => {   return `tab_$ {id}_$ {index}`; }; 
const getPanelId = (id = '', index = 0) => {   return `tabpanel_$ {id}_$ {index}`; }; 

Function: globalClick

This is a click handler that is applied at the document level. That means we are not having to manually add click handlers to a number of elements. Instead, we use event bubbling to listen for clicks further down in the document, and allow them to propagate up to the top. Conveniently, this is also how we can handle keyboard events such as the Enter key being pressed. Both are necessary to have an accessible UI.

In the first part of the function, we destructure key and target from the incoming event. Next, we destructure the parentNode and tagName from the target.

Then, we attempt to get the wrapper element. This would be the one with either class="tabs" or class="accordion". Because we might actually be clicking on the ancestor element highest in the DOM tree — which exists but possibly does not have the *.closest(…) method — we do a typeof check. If that function exists, we attempt to get the element. Even still, we might come up without a match. So we have one more getDomFallback to be safe.

// Get target. const { key = '', target = getDomFallback() } = event;  // Get parent. const { parentNode = getDomFallback(), tagName = '' } = target;  // Set later. let wrapper = getDomFallback();  /*   =====   NOTE:   =====    We test for this, because the method does   not exist on `document.documentElement`. */ if (typeof target.closest === FUNCTION) {   // Get wrapper.   wrapper = target.closest(TABLIST_CLASS_SELECTOR) || getDomFallback(); } 

Then, we store whether or not the tag that was clicked is a <li>. Likewise, we store a boolean about whether the wrapper element has aria-multiselectable="true". I will get back to that. We need this info later on.

We also interrogate the event a bit, to determine if it was triggered by the user pressing a key. If so, then we are only interested if that key was Enter. We also determine if the click happened on a relevant target. Remember, we are using event bubbling so really the user could have clicked anything.

We want to make sure it:

  • Is not disabled
  • Has role="tab"
  • Has a parent element with role="tablist"

Then we bundle up our event and target booleans into one, as isValidEvent.

// Is `<li>`? const isListItem = tagName.toLowerCase() === LI;  // Is multi? const isMulti = wrapper.getAttribute(ARIA_MULTISELECTABLE) === TRUE;  // Valid key? const isValidKey = !key || key.toLowerCase() === ENTER;  // Valid target? const isValidTarget =   !target.hasAttribute(DISABLED) &&   target.getAttribute(ROLE) === TAB &&   parentNode.getAttribute(ROLE) === TABLIST;  // Valid event? const isValidEvent = isValidKey && isValidTarget; 

Assuming the event is indeed valid, we make it past our next if check. Now, we are concerned with getting the role="tabpanel" element with an id that matches our tab’s aria-controls="…".

Once we have got it, we check whether the panel is hidden, and if the tab is selected. Basically, we first presuppose that we are dealing with an accordion and flip the booleans to their opposites.

This is also where our earlier isListItem boolean comes into play. If the user is clicking an <li> then we know we are dealing with tabs, not an accordion. In which case, we want to flag our panel as being visible (via aria-hiddden="false") and our tab as being selected (via aria-selected="true").

Also, we want to ensure that either the wrapper has aria-multiselectable="false" or is completely missing aria-multiselectable. If that is the case, then we loop through all neighboring role="tab" and all role="tabpanel" elements and set them to their inactive states. Finally, we arrive at setting the previously determined booleans for the individual tab and panel pairing.

// Continue? if (isValidEvent) {   // Get panel.   const panelId = target.getAttribute(ARIA_CONTROLS);   const panel = wrapper.querySelector(`#$ {panelId}`) || getDomFallback();    // Get booleans.   let boolPanel = panel.getAttribute(ARIA_HIDDEN) !== TRUE;   let boolTab = target.getAttribute(ARIA_SELECTED) !== TRUE;    // List item?   if (isListItem) {     boolPanel = FALSE;     boolTab = TRUE;   }    // [aria-multiselectable="false"]   if (!isMulti) {     // Get tabs & panels.     const childTabs = wrapper.querySelectorAll(TAB_SELECTOR);     const childPanels = wrapper.querySelectorAll(TABPANEL_SELECTOR);      // Loop through tabs.     childTabs.forEach((tab = getDomFallback()) => {       tab.setAttribute(ARIA_SELECTED, FALSE);     });      // Loop through panels.     childPanels.forEach((panel = getDomFallback()) => {       panel.setAttribute(ARIA_HIDDEN, TRUE);     });   }    // Set individual tab.   target.setAttribute(ARIA_SELECTED, boolTab);    // Set individual panel.   panel.setAttribute(ARIA_HIDDEN, boolPanel); }

Function: addAriaAttributes

The astute reader might be thinking:

You said earlier that we start with the most bare possible markup, yet the globalClick function was looking for attributes that would not be there. Why would you lie!?

Or perhaps not, for the astute reader would have also noticed the function named addAriaAttributes. Indeed, this function does exactly what it says on the tin. It breathes life into the base DOM structure, by adding all the requisite aria-* and role attributes.

This not only makes the UI inherently more accessible to assistive technologies, but it also ensures the functionality actually works. I prefer to build vanilla JS things this way, rather than pivoting on class="…" for interactivity, because it forces me to think about the entirety of the user experience, beyond what I can see visually.

First off, we get all elements on the page that have class="tabs" and/or class="accordion". Then we check if we have something to work with. If not, then we would exit our function here. Assuming we do have a list, we loop through each of the wrapping elements and pass them into the scope of our function as wrapper.

// Get elements. const allWrappers = document.querySelectorAll(TABLIST_CLASS_SELECTOR);  // Loop through. allWrappers.forEach((wrapper = getDomFallback()) => {   /*     NOTE: Cut, for brevity.   */ }); 

Inside the scope of our looping function, we destructure id and classList from wrapper. If there is no ID, then we generate one via unique(). We set a boolean flag, to identify if we are working with an accordion. This is used later.

We also get decendants of wrapper that are tabs and panels, via their class name selectors.

Tabs:

  • class="tabs__item" or
  • class="accordion__item"

Panels:

  • class="tabs__panel" or
  • class="accordion__panel"

We then set the wrapper’s id if it does not already have one.

If we are dealing with an accordion that lacks aria-multiselectable="false", we set its flag to true. Reason being, if developers are reaching for an accordion UI paradigm — and also have tabs available to them, which are inherently mutually exclusive — then the safer assumption is that the accordion should support expanding and collapsing of several panels.

// Get attributes. const { id = '', classList } = wrapper; const parentId = id || unique();  // Is accordion? const isAccordion = classList.contains(ACCORDION);  // Get tabs & panels. const childTabs = wrapper.querySelectorAll(TAB_CLASS_SELECTOR); const childPanels = wrapper.querySelectorAll(TABPANEL_CLASS_SELECTOR);  // Add ID? if (!wrapper.getAttribute(ID)) {   wrapper.setAttribute(ID, parentId); }  // Add multi? if (isAccordion && wrapper.getAttribute(ARIA_MULTISELECTABLE) !== FALSE) {   wrapper.setAttribute(ARIA_MULTISELECTABLE, TRUE); } 

Next, we loop through tabs. Wherein, we also handle our panels.

You may be wondering why this is an old school for loop, instead of a more modern *.forEach. The reason is that we want to loop through two NodeList instances: tabs and panels. Assuming they each map 1-to-1 we know they both have the same *.length. This allows us to have one loop instead of two.

Let us peer inside of the loop. First, we get unique IDs for each tab and panel. These would look like one of the two following scenarios. These are used later on, to associate tabs with panels and vice versa.

  • tab_WRAPPER_ID_0 or
    tab_GENERATED_STRING_0
  • tabpanel_WRAPPER_ID_0 or
    tabpanel_GENERATED_STRING_0
for (let index = 0; index < childTabs.length; index++) {   // Get elements.   const tab = childTabs[index] || getDomFallback();   const panel = childPanels[index] || getDomFallback();    // Get IDs.   const tabId = getTabId(parentId, index);   const panelId = getPanelId(parentId, index);    /*     NOTE: Cut, for brevity.   */ } 

As we loop through, we first ensure that an expand/collapse icon exists. We create it if necessary, and set it to aria-hidden="true" since it is purely decorative.

Next, we check on attributes for the current tab. If an id="…" does not exist on the tab, we add it. Likewise, if aria-controls="…" does not exist we add that as well, pointing to our newly created panelId.

You will notice there is a little pivot here, checking if we do not have aria-selected and then further determining if we are not in the context of an accordion and if the index is 0. In that case, we want to make our first tab look selected. The reason is that though an accordion can be fully collapsed, tabbed content cannot. There is always at least one panel visible.

Then we ensure that role="tab" exists.

It is worth noting we do some extra work, based on whether the tab is disabled. If so, we remove tabindex so that the tab cannot receive :focus. If the tab is not disabled, we add tabindex="0" so that it can receive :focus.

We also set aria-disabled="true", if need be. You might be wondering if that is redundant. But it is necessary to inform assistive technologies that the tab is not interactive. Since our tab is either a <div> or <li>, it technically cannot be disabled like an <input>. Our styles pivot on [disabled], so we get that for free. Plus, it is less cognitive overhead (as a developer creating HTML) to only worry about one attribute.

ℹ️ Fun Fact: It is also worth noting the use of hasAttribute(…) to detect disabled, instead of getAttribute(…). This is because the mere presence of disabled will cause form elements to be disabled.

If the HTML is compiled, via tools such as Parcel

  • Markup like this: <tag disabled>
  • Is changed to this: <tag disabled="">

In which case, getting the attribute is still a falsy string.

In the days of XHTML, that would have been disabled="disabled". But really, it was only ever the existence of the attribute that mattered. Not its value. That is why we simply test if the element has the disabled attribute.

Lastly, we check if we are on the first iteration of our loop where index is 0. If so, we go up one level to the parentNode. If that element does not have role="tablist", then we add it.

We do this via parentNode instead of wrapper because in the context of tabs (not accordion) there is a <ul>element around the tab <li> that needs role="tablist". In the case of an accordion, it would be the outermost <div> ancestor. This code accounts for both.

// Tab: add icon? if (isAccordion) {   // Get icon.   let icon = tab.querySelector(ACCORDION_ITEM_ICON_SELECTOR);    // Create icon?   if (!icon) {     icon = document.createElement(I);     icon.className = ACCORDION_ITEM_ICON;     tab.insertAdjacentElement(AFTER_BEGIN, icon);   }    // [aria-hidden="true"]   icon.setAttribute(ARIA_HIDDEN, TRUE); }  // Tab: add id? if (!tab.getAttribute(ID)) {   tab.setAttribute(ID, tabId); }  // Tab: add controls? if (!tab.getAttribute(ARIA_CONTROLS)) {   tab.setAttribute(ARIA_CONTROLS, panelId); }  // Tab: add selected? if (!tab.getAttribute(ARIA_SELECTED)) {   const bool = !isAccordion && index === 0;    tab.setAttribute(ARIA_SELECTED, bool); }  // Tab: add role? if (tab.getAttribute(ROLE) !== TAB) {   tab.setAttribute(ROLE, TAB); }  // Tab: add tabindex? if (tab.hasAttribute(DISABLED)) {   tab.removeAttribute(TABINDEX);   tab.setAttribute(ARIA_DISABLED, TRUE); } else {   tab.setAttribute(TABINDEX, 0); }  // Tab: first item? if (index === 0) {   // Get parent.   const { parentNode = getDomFallback() } = tab;    /*     We do this here, instead of outside the loop.      The top level item isn't always the `tablist`.      The accordion UI only has `<dl>`, whereas     the tabs UI has both `<div>` and `<ul>`.   */   if (parentNode.getAttribute(ROLE) !== TABLIST) {     parentNode.setAttribute(ROLE, TABLIST);   } }

Continuing within the earlier for loop, we add attributes for each panel. We add an id if needed. We also set aria-hidden to either true or false depending on the context of being an accordion (or not).

Likewise, we ensure that our panel points back to its tab trigger via aria-labelledby="…", and that role="tabpanel" has been set.

// Panel: add ID? if (!panel.getAttribute(ID)) {   panel.setAttribute(ID, panelId); }  // Panel: add hidden? if (!panel.getAttribute(ARIA_HIDDEN)) {   const bool = isAccordion || index !== 0;    panel.setAttribute(ARIA_HIDDEN, bool); }  // Panel: add labelled? if (!panel.getAttribute(ARIA_LABELLEDBY)) {   panel.setAttribute(ARIA_LABELLEDBY, tabId); }  // Panel: add role? if (panel.getAttribute(ROLE) !== TABPANEL) {   panel.setAttribute(ROLE, TABPANEL); } 

At the very end of the file, we have a few setup and teardown functions. As a way to play nicely with other JS that might be in the page, we provide an unbind function that removes our global event listeners. It can be called by itself, via tablist.unbind() but is mostly there so that we can unbind() before (re-)binding. That way we prevent doubling up.

Inside our init function, we call addAriaAttributes() which modifies the DOM to be accessible. We then call unbind() and then add our event listeners to the document.

Finally, we bundle both methods into a parent object and export it under the name tablist. That way, when dropping it into a flat HTML page, we can call tablist.init() when we are ready to apply our functionality.

// ===================== // Remove global events. // =====================  const unbind = () => {   document.removeEventListener(CLICK, globalClick);   document.removeEventListener(KEYDOWN, globalClick); };  // ================== // Add global events. // ==================  const init = () => {   // Add attributes.   addAriaAttributes();    // Prevent doubles.   unbind();    document.addEventListener(CLICK, globalClick);   document.addEventListener(KEYDOWN, globalClick); };  // ============== // Bundle object. // ==============  const tablist = {   init,   unbind, };  // ======= // Export. // =======  export { tablist };

React examples

There is a scene in Batman Begins where Lucius Fox (played by Morgan Freeman) explains to a recovering Bruce Wayne (Christian Bale) the scientific steps he took to save his life after being poisoned.

Lucius Fox: “I analyzed your blood, isolating the receptor compounds and the protein-based catalyst.”

Bruce Wayne: “Am I meant to understand any of that?”

Lucius Fox: “Not at all, I just wanted you to know how hard it was. Bottom line, I synthesized an antidote.”

Morgan Freeman and Christian Bale, sitting inside the Batmobile
“How do I configure Webpack?”

↑ When working with a framework, I think in those terms.

Now that we know “hard” it is — not really, but humor me — to do raw DOM manipulation and event binding, we can better appreciate the existence of an antidote. React abstracts a lot of that complexity away, and handles it for us automatically.

File: Tabs.js

Now that we are diving into React examples, we will start with the <Tabs> component.

// ============= // Used like so… // =============  <Tabs>   <div label="Tab 1">     <p>       Tab 1 content     </p>   </div>   <div label="Tab 2">     <p>       Tab 2 content     </p>   </div> </Tabs>

Here is the content from our Tabs.js file. Note that in React parlance, it is standard practice to name the file with the same capitalization as its export default component.

We start out with the same getTabId and getPanelId functions as in our vanilla JS approach, because we still need to make sure to accessibly map tabs to components. Take a look at the entirey of the code, and then we will continue to break it down.

import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { v4 as uuid } from 'uuid'; import cx from 'classnames';  // UI. import Render from './Render';  // =========== // Get tab ID. // ===========  const getTabId = (id = '', index = 0) => {   return `tab_$ {id}_$ {index}`; };  // ============= // Get panel ID. // =============  const getPanelId = (id = '', index = 0) => {   return `tabpanel_$ {id}_$ {index}`; };  // ========== // Is active? // ==========  const getIsActive = ({ activeIndex = null, index = null, list = [] }) => {   // Index matches?   const isMatch = index === parseFloat(activeIndex);    // Is first item?   const isFirst = index === 0;    // Only first item exists?   const onlyFirstItem = list.length === 1;    // Item doesn't exist?   const badActiveItem = !list[activeIndex];    // Flag as active?   const isActive = isMatch || onlyFirstItem || (isFirst && badActiveItem);    // Expose boolean.   return !!isActive; };  getIsActive.propTypes = {   activeIndex: PropTypes.number,   index: PropTypes.number,   list: PropTypes.array, };  // ================ // Get `<ul>` list. // ================  const getTabsList = ({ activeIndex = null, id = '', list = [], setActiveIndex = () => {} }) => {   // Build new list.   const newList = list.map((item = {}, index) => {     // =========     // Get data.     // =========      const { props: itemProps = {} } = item;     const { disabled = null, label = '' } = itemProps;     const idPanel = getPanelId(id, index);     const idTab = getTabId(id, index);     const isActive = getIsActive({ activeIndex, index, list });      // =======     // Events.     // =======      const handleClick = (event = {}) => {       const { key = '' } = event;        if (!disabled) {         // Early exit.         if (key && key.toLowerCase() !== 'enter') {           return;         }          setActiveIndex(index);       }     };      // ============     // Add to list.     // ============      return (       <li         aria-controls={idPanel}         aria-disabled={disabled}         aria-selected={isActive}         className="tabs__item"         disabled={disabled}         id={idTab}         key={idTab}         role="tab"         tabIndex={disabled ? null : 0}         // Events.         onClick={handleClick}         onKeyDown={handleClick}       >         {label || `$ {index + 1}`}       </li>     );   });    // ==========   // Expose UI.   // ==========    return (     <Render if={newList.length}>       <ul className="tabs__list" role="tablist">         {newList}       </ul>     </Render>   ); };  getTabsList.propTypes = {   activeIndex: PropTypes.number,   id: PropTypes.string,   list: PropTypes.array,   setActiveIndex: PropTypes.func, };  // ================= // Get `<div>` list. // =================  const getPanelsList = ({ activeIndex = null, id = '', list = [] }) => {   // Build new list.   const newList = list.map((item = {}, index) => {     // =========     // Get data.     // =========      const { props: itemProps = {} } = item;     const { children = '', className = null, style = null } = itemProps;     const idPanel = getPanelId(id, index);     const idTab = getTabId(id, index);     const isActive = getIsActive({ activeIndex, index, list });      // =============     // Get children.     // =============      let content = children || item;      if (typeof content === 'string') {       content = <p>{content}</p>;     }      // =================     // Build class list.     // =================      const classList = cx({       tabs__panel: true,       [String(className)]: className,     });      // ==========     // Expose UI.     // ==========      return (       <div         aria-hidden={!isActive}         aria-labelledby={idTab}         className={classList}         id={idPanel}         key={idPanel}         role="tabpanel"         style={style}       >         {content}       </div>     );   });    // ==========   // Expose UI.   // ==========    return newList; };  getPanelsList.propTypes = {   activeIndex: PropTypes.number,   id: PropTypes.string,   list: PropTypes.array, };  // ========== // Component. // ==========  const Tabs = ({   children = '',   className = null,   selected = 0,   style = null,   id: propsId = uuid(), }) => {   // ===============   // Internal state.   // ===============    const [id] = useState(propsId);   const [activeIndex, setActiveIndex] = useState(selected);    // =================   // Build class list.   // =================    const classList = cx({     tabs: true,     [String(className)]: className,   });    // ===============   // Build UI lists.   // ===============    const list = Array.isArray(children) ? children : [children];    const tabsList = getTabsList({     activeIndex,     id,     list,     setActiveIndex,   });    const panelsList = getPanelsList({     activeIndex,     id,     list,   });    // ==========   // Expose UI.   // ==========    return (     <Render if={list[0]}>       <div className={classList} id={id} style={style}>         {tabsList}         {panelsList}       </div>     </Render>   ); };  Tabs.propTypes = {   children: PropTypes.node,   className: PropTypes.string,   id: PropTypes.string,   selected: PropTypes.number,   style: PropTypes.object, };  export default Tabs;

Function: getIsActive

Due to a <Tabs> component always having something active and visible, this function contains some logic to determine whether an index of a given tab should be the lucky winner. Essentially, in sentence form the logic goes like this.

This current tab is active if:

  • Its index matches the activeIndex, or
  • The tabs UI has only one tab, or
  • It is the first tab, and the activeIndex tab does not exist.
const getIsActive = ({ activeIndex = null, index = null, list = [] }) => {   // Index matches?   const isMatch = index === parseFloat(activeIndex);    // Is first item?   const isFirst = index === 0;    // Only first item exists?   const onlyFirstItem = list.length === 1;    // Item doesn't exist?   const badActiveItem = !list[activeIndex];    // Flag as active?   const isActive = isMatch || onlyFirstItem || (isFirst && badActiveItem);    // Expose boolean.   return !!isActive; };

Function: getTabsList

This function generates the clickable <li role="tabs"> UI, and returns it wrapped in a parent <ul role="tablist">. It assigns all the relevant aria-* and role attributes, and handles binding the onClickand onKeyDown events. When an event is triggered, setActiveIndex is called. This updates the component’s internal state.

It is noteworthy how the content of the <li> is derived. That is passed in as <div label="…"> children of the parent <Tabs> component. Though this is not a real concept in flat HTML, it is a handy way to think about the relationship of the content. The children of that <div> become the the innards of our role="tabpanel" later.

const getTabsList = ({ activeIndex = null, id = '', list = [], setActiveIndex = () => {} }) => {   // Build new list.   const newList = list.map((item = {}, index) => {     // =========     // Get data.     // =========      const { props: itemProps = {} } = item;     const { disabled = null, label = '' } = itemProps;     const idPanel = getPanelId(id, index);     const idTab = getTabId(id, index);     const isActive = getIsActive({ activeIndex, index, list });      // =======     // Events.     // =======      const handleClick = (event = {}) => {       const { key = '' } = event;        if (!disabled) {         // Early exit.         if (key && key.toLowerCase() !== 'enter') {           return;         }          setActiveIndex(index);       }     };      // ============     // Add to list.     // ============      return (       <li         aria-controls={idPanel}         aria-disabled={disabled}         aria-selected={isActive}         className="tabs__item"         disabled={disabled}         id={idTab}         key={idTab}         role="tab"         tabIndex={disabled ? null : 0}         // Events.         onClick={handleClick}         onKeyDown={handleClick}       >         {label || `$ {index + 1}`}       </li>     );   });    // ==========   // Expose UI.   // ==========    return (     <Render if={newList.length}>       <ul className="tabs__list" role="tablist">         {newList}       </ul>     </Render>   ); };

Function: getPanelsList

This function parses the incoming children of the top level component and extracts the content. It also makes use of getIsActive to determine whether (or not) to apply aria-hidden="true". As one might expect by now, it adds all the other relevant aria-* and role attributes too. It also applies any extra className or style that was passed in.

It also is “smart” enough to wrap any string content — anything lacking a wrapping tag already — in <p> tags for consistency.

const getPanelsList = ({ activeIndex = null, id = '', list = [] }) => {   // Build new list.   const newList = list.map((item = {}, index) => {     // =========     // Get data.     // =========      const { props: itemProps = {} } = item;     const { children = '', className = null, style = null } = itemProps;     const idPanel = getPanelId(id, index);     const idTab = getTabId(id, index);     const isActive = getIsActive({ activeIndex, index, list });      // =============     // Get children.     // =============      let content = children || item;      if (typeof content === 'string') {       content = <p>{content}</p>;     }      // =================     // Build class list.     // =================      const classList = cx({       tabs__panel: true,       [String(className)]: className,     });      // ==========     // Expose UI.     // ==========      return (       <div         aria-hidden={!isActive}         aria-labelledby={idTab}         className={classList}         id={idPanel}         key={idPanel}         role="tabpanel"         style={style}       >         {content}       </div>     );   });    // ==========   // Expose UI.   // ==========    return newList; };

Function: Tabs

This is the main component. It sets an internal state for an id, to essentially cache any generated uuid() so that it does not change during the lifecycle of the component. React is finicky about its key attributes (in the previous loops) changing dynamically, so this ensures they remain static once set.

We also employ useState to track the currently selected tab, and pass down a setActiveIndex function to each <li> to monitor when they are clicked. After that, it is pretty straightfowrard. We call getTabsList and getPanelsList to build our UI, and then wrap it all up in <div role="tablist">.

It accepts any wrapper level className or style, in case anyone wants further tweaks during implementation. Providing other developers (as consumers) this flexibility means that the likelihood of needing to make further edits to the core component is lower. Lately, I have been doing this as a “best practice” for all components I create.

const Tabs = ({   children = '',   className = null,   selected = 0,   style = null,   id: propsId = uuid(), }) => {   // ===============   // Internal state.   // ===============    const [id] = useState(propsId);   const [activeIndex, setActiveIndex] = useState(selected);    // =================   // Build class list.   // =================    const classList = cx({     tabs: true,     [String(className)]: className,   });    // ===============   // Build UI lists.   // ===============    const list = Array.isArray(children) ? children : [children];    const tabsList = getTabsList({     activeIndex,     id,     list,     setActiveIndex,   });    const panelsList = getPanelsList({     activeIndex,     id,     list,   });    // ==========   // Expose UI.   // ==========    return (     <Render if={list[0]}>       <div className={classList} id={id} style={style}>         {tabsList}         {panelsList}       </div>     </Render>   ); };

If you are curious about the <Render> function, you can read more about that in this example.

File: Accordion.js

// ============= // Used like so… // =============  <Accordion>   <div label="Tab 1">     <p>       Tab 1 content     </p>   </div>   <div label="Tab 2">     <p>       Tab 2 content     </p>   </div> </Accordion>

As you may have deduced — due to the vanilla JS example handling both tabs and accordion — this file has quite a few similarities to how Tabs.js works.

Rather than belabor the point, I will simply provide the file’s contents for completeness and then speak about the specific areas in which the logic differs. So, take a gander at the contents and I will explain what makes <Accordion> quirky.

import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { v4 as uuid } from 'uuid'; import cx from 'classnames';  // UI. import Render from './Render';  // =========== // Get tab ID. // ===========  const getTabId = (id = '', index = 0) => {   return `tab_$ {id}_$ {index}`; };  // ============= // Get panel ID. // =============  const getPanelId = (id = '', index = 0) => {   return `tabpanel_$ {id}_$ {index}`; };  // ============================== // Get `tab` and `tabpanel` list. // ==============================  const getTabsAndPanelsList = ({   activeItems = {},   id = '',   isMulti = true,   list = [],   setActiveItems = () => {}, }) => {   // Build new list.   const newList = [];    // Loop through.   list.forEach((item = {}, index) => {     // =========     // Get data.     // =========      const { props: itemProps = {} } = item;      const {       children = '',       className = null,       disabled = null,       label = '',       style = null,     } = itemProps;      const idPanel = getPanelId(id, index);     const idTab = getTabId(id, index);     const isActive = !!activeItems[index];      // =======     // Events.     // =======      const handleClick = (event = {}) => {       const { key = '' } = event;        if (!disabled) {         // Early exit.         if (key && key.toLowerCase() !== 'enter') {           return;         }          // Keep active items?         const state = isMulti ? activeItems : null;          // Update active item.         const newState = {           ...state,           [index]: !activeItems[index],         };          // Set active item.         setActiveItems(newState);       }     };      // =============     // Get children.     // =============      let content = children || item;      if (typeof content === 'string') {       content = <p>{content}</p>;     }      // =================     // Build class list.     // =================      const classList = cx({       accordion__panel: true,       [String(className)]: className,     });      // ========     // Add tab.     // ========      newList.push(       <div         aria-controls={idPanel}         aria-disabled={disabled}         aria-selected={isActive}         className="accordion__item"         disabled={disabled}         id={idTab}         key={idTab}         role="tab"         tabIndex={disabled ? null : 0}         // Events.         onClick={handleClick}         onKeyDown={handleClick}       >         <i aria-hidden="true" className="accordion__item__icon" />         {label || `$ {index + 1}`}       </div>     );      // ==========     // Add panel.     // ==========      newList.push(       <div         aria-hidden={!isActive}         aria-labelledby={idTab}         className={classList}         id={idPanel}         key={idPanel}         role="tabpanel"         style={style}       >         {content}       </div>     );   });    // ==========   // Expose UI.   // ==========    return newList; };  getTabsAndPanelsList.propTypes = {   activeItems: PropTypes.object,   id: PropTypes.string,   isMulti: PropTypes.bool,   list: PropTypes.array,   setActiveItems: PropTypes.func, };  // ========== // Component. // ==========  const Accordion = ({   children = '',   className = null,   isMulti = true,   selected = {},   style = null,   id: propsId = uuid(), }) => {   // ===============   // Internal state.   // ===============    const [id] = useState(propsId);   const [activeItems, setActiveItems] = useState(selected);    // =================   // Build class list.   // =================    const classList = cx({     accordion: true,     [String(className)]: className,   });    // ===============   // Build UI lists.   // ===============    const list = Array.isArray(children) ? children : [children];    const tabsAndPanelsList = getTabsAndPanelsList({     activeItems,     id,     isMulti,     list,     setActiveItems,   });    // ==========   // Expose UI.   // ==========    return (     <Render if={list[0]}>       <div         aria-multiselectable={isMulti}         className={classList}         id={id}         role="tablist"         style={style}       >         {tabsAndPanelsList}       </div>     </Render>   ); };  Accordion.propTypes = {   children: PropTypes.node,   className: PropTypes.string,   id: PropTypes.string,   isMulti: PropTypes.bool,   selected: PropTypes.object,   style: PropTypes.object, };  export default Accordion;

Function: handleClick

While most of our <Accordion> logic is similar to <Tabs>, it differs in how it stores the currently active tab.

Since <Tabs> are always mutually exclusive, we only really need a single numeric index. Easy peasy.

However, because an <Accordion> can have concurrently visible panels — or be used in a mutually exclusive manner — we need to represent that to useState in a way that could handle both.

If you were beginning to think…

“I would store that in an object.”

…then congrats. You are right!

This function does a quick check to see if isMulti has been set to true. If so, we use the spread syntax to apply the existing activeItems to our newState object. We then set the current index to its boolean opposite.

const handleClick = (event = {}) => {   const { key = '' } = event;    if (!disabled) {     // Early exit.     if (key && key.toLowerCase() !== 'enter') {       return;     }      // Keep active items?     const state = isMulti ? activeItems : null;      // Update active item.     const newState = {       ...state,       [index]: !activeItems[index],     };      // Set active item.     setActiveItems(newState);   } };

For reference, here is how our activeItems object looks if only the first accordion panel is active and a user clicks the second. Both indexes would be set to true. This allows for viewing two expanded role="tabpanel" simultaneously.

/*   Internal representation   of `activeItems` state. */  {   0: true,   1: true, } 

Whereas if we were not operating in isMulti mode — when the wrapper has aria-multiselectable="false" — then activeItems would only ever contain one key/value pair.

Because rather than spreading the current activeItems, we would be spreading null. That effectively wipes the slate clean, before recording the currently active tab.

/*   Internal representation   of `activeItems` state. */  {   1: true, } 

Conclusion

Still here? Awesome.

Hopefully you found this article informative, and maybe even learned a bit more about accessibility and JS(X) along the way. For review, let us look one more time at our flat HTML example and and the React usage of our <Tabs>component. Here is a comparison of the markup we would write in a vanilla JS approach, versus the JSX it takes to generate the same thing.

I am not saying that one is better than the other, but you can see how React makes it possible to distill things down into a mental model. Working directly in HTML, you always have to be aware of every tag.

HTML

<div class="tabs">   <ul class="tabs__list">     <li class="tabs__item">       Tab 1     </li>     <li class="tabs__item">       Tab 2     </li>   </ul>   <div class="tabs__panel">     <p>       Tab 1 content     </p>   </div>   <div class="tabs__panel">     <p>       Tab 2 content     </p>   </div> </div>

JSX

<Tabs>   <div label="Tab 1">     Tab 1 content   </div>   <div label="Tab 2">     Tab 2 content   </div> </Tabs>

↑ One of these probably looks preferrable, depending on your point of view.

Writing code closer to the metal means more direct control, but also more tedium. Using a framework like React means you get more functionality “for free,” but also it can be a black box.

That is, unless you understand the underlying nuances already. Then you can fluidly operate in either realm. Because you can see The Matrix for what it really is: Just JavaScript™. Not a bad place to be, no matter where you find yourself.

The post The Anatomy of a Tablist Component in Vanilla JavaScript Versus React appeared first on CSS-Tricks.

CSS-Tricks

, , , , , ,
[Top]

Creating a Gauge in React

, ,
[Top]

React Integration Testing: Greater Coverage, Fewer Tests

Integration tests are a natural fit for interactive websites, like ones you might build with React. They validate how a user interacts with your app without the overhead of end-to-end testing. 

This article follows an exercise that starts with a simple website, validates behavior with unit and integration tests, and demonstrates how integration testing delivers greater value from fewer lines of code. The content assumes a familiarity with React and testing in JavaScript. Experience with Jest and React Testing Library is helpful but not required.

There are three types of tests:

  • Unit tests verify one piece of code in isolation. They are easy to write, but can miss the big picture.
  • End-to-end tests (E2E) use an automation framework — such as Cypress or Selenium — to interact with your site like a user: loading pages, filling out forms, clicking buttons, etc. They are generally slower to write and run, but closely match the real user experience.
  • Integration tests fall somewhere in between. They validate how multiple units of your application work together but are more lightweight than E2E tests. Jest, for example, comes with a few built-in utilities to facilitate integration testing; Jest uses jsdom under the hood to emulate common browser APIs with less overhead than automation, and its robust mocking tools can stub out external API calls.

Another wrinkle: In React apps, unit and integration are written the same way, with the same tools. 

Getting started with React tests

I created a simple React app (available on GitHub) with a login form. I wired this up to reqres.in, a handy API I found for testing front-end projects.

You can log in successfully:

…or encounter an error message from the API:

The code is structured like this:

LoginModule/ ├── components/ ⎪   ├── Login.js // renders LoginForm, error messages, and login confirmation ⎪   └── LoginForm.js // renders login form fields and button ├── hooks/ ⎪    └── useLogin.js // connects to API and manages state └── index.js // stitches everything together

Option 1: Unit tests

If you’re like me, and like writing tests — perhaps with your headphones on and something good on Spotify — then you might be tempted to knock out a unit test for every file. 

Even if you’re not a testing aficionado, you might be working on a project that’s “trying to be good with testing” without a clear strategy and a testing approach of “I guess each file should have its own test?”

That would look something like this (where I’ve added unit to test file names for clarity):

LoginModule/ ├── components/ ⎪   ├── Login.js ⎪   ├── Login.unit.test.js ⎪   ├── LoginForm.js ⎪   └── LoginForm.unit.test.js ├── hooks/ ⎪   ├── useLogin.js  ⎪   └── useLogin.unit.test.js ├── index.js └── index.unit.test.js

I went through the exercise of adding each of these unit tests on on GitHub, and created a test:coverage:unit  script to generate a coverage report (a built-in feature of Jest). We can get to 100% coverage with the four unit test files:

100% coverage is usually overkill, but it’s achievable for such a simple codebase.

Let’s dig into one of the unit tests created for the onLogin React hook. Don’t worry if you’re not well-versed in React hooks or how to test them.

test('successful login flow', async () => {   // mock a successful API response   jest     .spyOn(window, 'fetch')     .mockResolvedValue({ json: () => ({ token: '123' }) }); 
   const { result, waitForNextUpdate } = renderHook(() => useLogin()); 
   act(() => {     result.current.onSubmit({       email: 'test@email.com',       password: 'password',     });   }); 
   // sets state to pending   expect(result.current.state).toEqual({     status: 'pending',     user: null,     error: null,   }); 
   await waitForNextUpdate(); 
   // sets state to resolved, stores email address   expect(result.current.state).toEqual({     status: 'resolved',     user: {       email: 'test@email.com',     },     error: null,   }); });

This test was fun to write (because React Hooks Testing Library makes testing hooks a breeze), but it has a few problems. 

First, the test validates that a piece of internal state changes from 'pending' to 'resolved'; this implementation detail is not exposed to the user, and therefore, probably not a good thing to be testing. If we refactor the app, we’ll have to update this test, even if nothing changes from the user’s perspective.

Additionally, as a unit test, this is just part of the picture. If we want to validate other features of the login flow, such as the submit button text changing to “Loading,” we’ll have to do so in a different test file.

Option 2: Integration tests

Let’s consider the alternative approach of adding one integration test to validate this flow:

LoginModule/ ├── components/ ⎪   ├─ Login.js ⎪   └── LoginForm.js ├── hooks/ ⎪   └── useLogin.js  ├── index.js └── index.integration.test.js

I implemented this test and a test:coverage:integration script to generate a coverage report. Just like the unit tests, we can get to 100% coverage, but this time it’s all in one file and requires fewer lines of code.

Here’s the integration test covering a successful login flow:

test('successful login', async () => {   // mock a successful API response   jest     .spyOn(window, 'fetch')     .mockResolvedValue({ json: () => ({ token: '123' }) }); 
   const { getByLabelText, getByText, getByRole } = render(<LoginModule />); 
   const emailField = getByLabelText('Email');   const passwordField = getByLabelText('Password');   const button = getByRole('button'); 
   // fill out and submit form   fireEvent.change(emailField, { target: { value: 'test@email.com' } });   fireEvent.change(passwordField, { target: { value: 'password' } });   fireEvent.click(button); 
   // it sets loading state   expect(button.disabled).toBe(true);   expect(button.textContent).toBe('Loading...'); 
   await waitFor(() => {     // it hides form elements     expect(button).not.toBeInTheDocument();     expect(emailField).not.toBeInTheDocument();     expect(passwordField).not.toBeInTheDocument(); 
     // it displays success text and email address     const loggedInText = getByText('Logged in as');     expect(loggedInText).toBeInTheDocument();     const emailAddressText = getByText('test@email.com');     expect(emailAddressText).toBeInTheDocument();   }); });

I really like this test, because it validates the entire login flow from the user’s perspective: the form, the loading state, and the success confirmation message. Integration tests work really well for React apps for precisely this use case; the user experience is the thing we want to test, and that almost always involves several different pieces of code working together.

This test has no specific knowledge of the components or hook that makes the expected behavior work, and that’s good. We should be able to rewrite and restructure such implementation details without breaking the tests, so long as the user experience remains the same.

I’m not going to dig into the other integration tests for the login flow’s initial state and error handling, but I encourage you to check them out on GitHub.

So, what does need a unit test?

Rather than thinking about unit vs. integration tests, let’s back up and think about how we decide what needs to be tested in the first place. LoginModule needs to be tested because it’s an entity we want consumers (other files in the app) to be able to use with confidence.

The onLogin hook, on the other hand, does not need to be tested because it’s only an implementation detail of LoginModule. If our needs change, however, and onLogin has use cases elsewhere, then we would want to add our own (unit) tests to validate its functionality as a reusable utility. (We’d also want to move the file because it wouldn’t be specific to LoginModule anymore.)

There are still plenty of use cases for unit tests, such as the need to validate reusable selectors, hooks, and plain functions. When developing your code, you might also find it helpful to practice test-driven development with a unit test, even if you later move that logic higher up to an integration test.

Additionally, unit tests do a great job of exhaustively testing against multiple inputs and use cases. For example, if my form needed to show inline validations for various scenarios (e.g. invalid email, missing password, short password), I would cover one representative case in an integration test, then dig into the specific cases in a unit test.

Other goodies

While we’re here, I want to touch on few syntactic tricks that helped my integration tests stay clear and organized.

Big waitFor Blocks

Our test needs to account for the delay between the loading and success states of LoginModule:

const button = getByRole('button'); fireEvent.click(button); 
 expect(button).not.toBeInTheDocument(); // too soon, the button is still there!

We can do this with DOM Testing Library’s waitFor helper:

const button = getByRole('button'); fireEvent.click(button); 
 await waitFor(() => {   expect(button).not.toBeInTheDocument(); // ahh, that's better });

But, what if we want to test some other items too? There aren’t a lot of good examples of how to handle this online, and in past projects, I’ve dropped additional items outside of the waitFor:

// wait for the button await waitFor(() => {   expect(button).not.toBeInTheDocument(); }); 
 // then test the confirmation message const confirmationText = getByText('Logged in as test@email.com'); expect(confirmationText).toBeInTheDocument();

This works, but I don’t like it because it makes the button condition look special, even though we could just as easily switch the order of these statements:

// wait for the confirmation message await waitFor(() => {   const confirmationText = getByText('Logged in as test@email.com');   expect(confirmationText).toBeInTheDocument(); }); 
 // then test the button expect(button).not.toBeInTheDocument();

It’s much better, in my opinion, to group everything related to the same update together inside the waitFor callback:

await waitFor(() => {   expect(button).not.toBeInTheDocument();      const confirmationText = getByText('Logged in as test@email.com');   expect(confirmationText).toBeInTheDocument(); });

Interestingly, an empty waitFor will also get the job done, because waitFor has a default timeout of 50ms. I find this slightly less declarative than putting your expectations inside of the waitFor, but some indentation-averse developers may prefer it: 

await waitFor(() => {}); // or maybe a custom util, `await waitForRerender()` 
 expect(button).not.toBeInTheDocument(); // I pass!

For tests with a few steps, we can have multiple waitFor blocks in row:

const button = getByRole('button'); const emailField = getByLabelText('Email'); 
 // fill out form fireEvent.change(emailField, { target: { value: 'test@email.com' } }); 
 await waitFor(() => {   // check button is enabled   expect(button.disabled).toBe(false); }); 
 // submit form fireEvent.click(button); 
 await waitFor(() => {   // check button is no longer present   expect(button).not.toBeInTheDocument(); });

Inline it comments

Another testing best practice is to write fewer, longer tests; this allows you to correlate your test cases to significant user flows while keeping tests isolated to avoid unexpected behavior. I subscribe to this approach, but it can present challenges in keeping code organized and documenting desired behavior. We need future developers to be able to return to a test and understand what it’s doing, why it’s failing, etc.

For example, let’s say one of these expectations starts to fail:

it('handles a successful login flow', async () => {   // beginning of test hidden for clarity 
   expect(button.disabled).toBe(true);   expect(button.textContent).toBe('Loading...'); 
   await waitFor(() => {     expect(button).not.toBeInTheDocument();     expect(emailField).not.toBeInTheDocument();     expect(passwordField).not.toBeInTheDocument(); 
     const confirmationText = getByText('Logged in as test@email.com');     expect(confirmationText).toBeInTheDocument();   }); });

A developer looking into this can’t easily determine what is being tested and might have trouble deciding whether the failure is a bug (meaning we should fix the code) or a change in behavior (meaning we should fix the test).

My favorite solution to this problem is using the lesser-known test syntax for each test, and adding inline it-style comments describing each key behavior being tested:

test('successful login', async () => {   // beginning of test hidden for clarity 
   // it sets loading state   expect(button.disabled).toBe(true);   expect(button.textContent).toBe('Loading...'); 
   await waitFor(() => {     // it hides form elements     expect(button).not.toBeInTheDocument();     expect(emailField).not.toBeInTheDocument();     expect(passwordField).not.toBeInTheDocument(); 
     // it displays success text and email address     const confirmationText = getByText('Logged in as test@email.com');     expect(confirmationText).toBeInTheDocument();   }); });

These comments don’t magically integrate with Jest, so if you get a failure, the failing test name will correspond to the argument you passed to your test tag, in this case 'successful login'. However, Jest’s error messages contain surrounding code, so these it comments still help identify the failing behavior. Here’s the error message I got when I removed the not from one of my expectations:

For even more explicit errors, there’s package called jest-expect-message that allows you to define error messages for each expectation:

expect(button, 'button is still in document').not.toBeInTheDocument();

Some developers prefer this approach, but I find it a little too granular in most situations, since a single it often involves multiple expectations.

Next steps for teams

Sometimes I wish we could make linter rules for humans. If so, we could set up a prefer-integration-tests rule for our teams and call it a day.

But alas, we need to find a more analog solution to encourage developers to opt for integration tests in a situation, like the LoginModule example we covered earlier. Like most things, this comes down to discussing your testing strategy as a team, agreeing on something that makes sense for the project, and — hopefully — documenting it in an ADR.

When coming up with a testing plan, we should avoid a culture that pressures developers to write a test for every file. Developers need to feel empowered to make smart testing decisions, without worrying that they’re “not testing enough.” Jest’s coverage reports can help with this by providing a sanity check that you’re achieving good coverage, even if the tests are consolidated that the integration level.

I still don’t consider myself an expert on integration tests, but going through this exercise helped me break down a use case where integration testing delivered greater value than unit testing. I hope that sharing this with your team, or going through a similar exercise on your codebase, will help guide you in incorporating integration tests into your workflow.

The post React Integration Testing: Greater Coverage, Fewer Tests appeared first on CSS-Tricks.

CSS-Tricks

, , , , , ,
[Top]

Using Formik to Handle Forms in React

There is no doubt that web forms play an integral role in our web site or applications. By default, they provide a useful set of elements and features — from legends and fieldsets to native validation and states — but they only get us so far when we start to consider the peculiarities of using them. For example, how can we manipulate the state of a form? How about different forms of validation? Even hooking a form up to post submissions is a daunting effort at times.

Component-driven front-end libraries, like React, can ease the task of wiring web forms but can also get verbose and redundant. That’s why I want to introduce you to Formik, a small library that solves the three most annoying parts of writing forms in React:

  1. State manipulation
  2. Form validation (and error messages)
  3. Form submission

We’re going to build a form together in this post. We’ll start with a React component then integrate Formik while demonstrating the way it handles state, validation, and submissions.

Creating a form as a React component

Components live and breathe through their state and prop. What HTML form elements have in common with React components is that they naturally keep some internal state. Their values are also automatically stored in their value attribute.

Allowing form elements to manage their own state in React makes them uncontrolled components. That’s just a fancy way of saying the DOM handles the state instead of React. And while that works, it is often easier to use controlled components, where React handles the state and serves as the single source of truth rather than the DOM.

The markup for a straightforward HTML form might look something like this:

<form>   <div className="formRow">     <label htmlFor="email">Email address</label>     <input type="email" name="email" className="email" />   </div>   <div className="formRow">     <label htmlFor="password">Password</label>     <input type="password" name="password" className="password" />   </div>   <button type="submit">Submit</button> </form>

We can convert that into a controlled React component like so:

function HTMLForm() {   const [email, setEmail] = React.useState("");   const [password, setPassword] = React.useState(""); 
   return (     <form>       <div className="formRow">         <label htmlFor="email">Email address</label>         <input           type="email"           name="email"           className="email"           value={email}           onChange={e => setEmail(e.target.value)}         />       </div>       <div className="formRow">         <label htmlFor="password">Password</label>         <input           type="password"           name="password"           className="password"           value={password}           onChange={e => setPassword(e.target.value)}         />       </div>       <button type="submit">Submit</button>     </form>   ); }

This is a bit verbose but it comes with some benefits:

  1. We get a single source of truth for form values in the state.
  2. We can validate the form when and how we want.
  3. We get performance perks by loading what we need and when we need it.

OK, so why Formik again?

As it is with anything JavaScript, there’s already a bevy of form management libraries out there, like React Hook Form and Redux Form, that we can use. But there are several things that make Formik stand out from the pack:

  1. It’s declarative: Formik eliminates redundancy through abstraction and taking responsibility for state, validation and submissions.
  2. It offers an Escape Hatch: Abstraction is good, but forms are peculiar to certain patterns. Formik abstracts for you but also let’s you control it should you need to.
  3. It co-locates form states: Formik keeps everything that has to do with your form within your form components.
  4. It’s adaptable: Formik doesn’t enforce any rules on you. You can use as less or as much Formik as you need.
  5. Easy to use: Formik just works.

Sound good? Let’s implement Formik into our form component.

Going Formik

We will be building a basic login form to get our beaks wet with the fundamentals. We’ll be touching on three different ways to work with Formik:

  1. Using the useFormik hook
  2. Using Formik with React context
  3. Using withFormik as a higher-order component

I’ve created a demo with the packages we need, Formik and Yup.

Method 1: Using the useFormik hook

As it is right now, our form does nothing tangible. To start using Formik, we need to import the useFormik hook. When we use the hook, it returns all of the Formik functions and variables that help us manage the form. If we were to log the returned values to the console, we get this:

Showing console output of the various hooks and objects that are logged by Formik.

We’ll call useFormik and pass it initialValues to start. Then, an onSubmit handler fires when a form submission happens. Here’s how that looks:

// This is a React component function BaseFormik() {   const formik = useFormik({     initialValues: {       email: "",       password: ""     },     onSubmit(values) {       // This will run when the form is submitted     }   });     // If you're curious, you can run this Effect  //  useEffect(() => {  //   console.log({formik});  // }, []) 
   return (     // Your actual form   ) }

Then we’ll bind Formik to our form elements:

// This is a React component function BaseFormik() {   const formik = useFormik({     initialValues: {       email: "",       password: ""     },     onSubmit(values) {       // This will run when the form is submitted     }   });     // If you're curious, you can run this Effect  //  useEffect(() => {  //   console.log({formik});  // }, []) 
   return (   // We bind "onSubmit" to "formik.handleSubmit"   <form className="baseForm" onSubmit={formik.handleSubmit} noValidate>     <input       type="email"       name="email"       id="email"       className="email formField"       value={formik.values.email} // We also bind our email value       onChange={formik.handleChange} // And, we bind our "onChange" event.     />   </form>   ) }

This is how the binding works:

  1. It handles form submission with onSubmit={formik.handleSubmit}.
  2. It handles the state of inputs with value={formik.values.email} and onChange={formik.handleChange}.

If you take a closer look, we didn’t have to set up our state, nor handle the onChange or onSubmit events as we’d typically do with React. The complete change to our form goes:

However as you might have noticed, our form contains some redundancy. We had to drill down formik and manually bind the form input’s value and onChange event. That means we should de-structure the returned value and immediately bind the necessary props to a dependent field, like this:

// This is a React component function BaseFormik() {   const {getFieldProps, handleSubmit} = useFormik({     initialValues: {       email: "",       password: ""     },     onSubmit(values) {       // This will run when the form is submitted     }   });     // If you're curious, you can run this Effect  //  useEffect(() => {  //   console.log({formik});  // }, []) 
   return (   <form className="baseForm" onSubmit={handleSubmit} noValidate>     <input       type="email"       id="email"       className="email formField"       {...getFieldProps("email")} // We pass the name of the dependent field     />   </form>   ) }

Let’s take things even further with the included <Formik/>  component.

Method 2: Using Formik with React context

The <Formik/> component exposes various other components that adds more abstraction and sensible defaults. For example, components like <Form/>, <Field/>, and <ErrorMessage/> are ready to go right out of the box.

Keep in mind, you don’t have to use these components when working with <Formik/> but they do require <Formik/> (or withFormik) when using them.

Using <Formik/> requires an overhaul because it uses the render props pattern as opposed to hooks with useFormik. The render props pattern isn’t something new in React. It is a pattern that enables code re-usability between components — something hooks solve better. Nevertheless, <Formik/> has a bagful of custom components that make working with forms much easier.

import { Formik } from "formik"; 
 function FormikRenderProps() {   const initialValues = {     email: "",     password: ""   };   function onSubmit(values) {     // Do stuff here...     alert(JSON.stringify(values, null, 2));   }   return (       <Formik {...{ initialValues, onSubmit }}>         {({ getFieldProps, handleSubmit }) => (             <form className="baseForm" onSubmit={handleSubmit} noValidate>               <input                 type="email"                 id="email"                 className="email formField"                 {...getFieldProps("email")}               />             </form>         )}       </Formik>   ); }

Notice that initialValues and onSubmit have been completely detached from useFormik. This means we are able to pass the props that <Formik/> needs, specifically initialValues and useFormik.

<Formik/> returns a value that’s been de-structured into getFieldProps and handleSubmit. Everything else basically remains the same as the first method using useFormik.

Here’s a refresher on React render props if you’re feeling a little rusty.

We haven’t actually put any <Formik/> components to use just yet. I’ve done this intentionally to demonstrate Formik’s adaptability. We certainly do want to use those components for our form fields, so let’s rewrite the component so it uses the <Form/> component.

import { Formik, Field, Form } from "formik"; 
 function FormikRenderProps() {   const initialValues = {     email: "",     password: ""   };   function onSubmit(values) {     // Do stuff here...     alert(JSON.stringify(values, null, 2));   }   return (       <Formik {...{ initialValues, onSubmit }}>         {() => (             <Form className="baseForm" noValidate>               <Field                 type="email"                 id="email"                 className="email formField"                 name="email"               />             </Form>         )}       </Formik>   ); }

We replaced <form/> with <Form/> and removed the onSubmit handler since Formik handles that for us. Remember, it takes on all the responsibilities for handling forms.

We also replaced <input/> with <Field/> and removed the bindings. Again, Formik handles that.

There’s also no need to bother with the returned value from <Formik/> anymore. You guessed it, Formik handles that as well.

Formik handles everything for us. We can now focus more on the business logic of our forms rather than things that can essentially be abstracted.

We’re pretty much set to go and guess what? We’ve haven’t been concerned with state managements or form submissions!

“What about validation?” you may ask. We haven’t touched on that because it’s a whole new level on its own. Let’s touch on that before jumping to the last method.

Form validation with Formik

If you’ve ever worked with forms (and I bet you have), then you’re aware that validation isn’t something to neglect.

We want to take control of when and how to validate so new opportunities open up to create better user experiences. Gmail, for example, will not let you input a password unless the email address input is validated and authenticated. We could also do something where we validate on the spot and display messaging without additional interactions or page refreshes.

Here are three ways that Formik is able to handle validation:

  1. At the form level
  2. At the field level
  3. With manual triggers

Validation at the form level means validating the form as a whole. Since we have immediate access to form values, we can validate the entire form at once by either:

Both validate and validationSchema are functions that return an errors object with key/value pairings that those of initialValues. We can pass those to  useFormik, <Formik/> or withFormik

While validate is used for custom validations, validationSchema is used with a third-party library like Yup. 

Here’s an example using validate:

// Pass the `onSubmit` function that gets called when the form is submitted. const formik = useFormik({   initialValues: {     email: "",     password: ""   },   // We've added a validate function   validate() {     const errors = {};     // Add the touched to avoid the validator validating all fields at once     if (formik.touched.email && !formik.values.email) {       errors.email = "Required";     } else if (       !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}$ /i.test(formik.values.email)     ) {       errors.email = "Invalid email address";     }     if (formik.touched.password && !formik.values.password) {       errors.password = "Required";     } else if (formik.values.password.length <= 8) {       errors.password = "Must be more than 8 characters";     }     return errors;   },   onSubmit(values) {     // Do stuff here...   } }); // ...

And here we go with an example using validationSchema instead:

const formik = useFormik({   initialValues: {     email: "",     password: ""   },   // We used Yup here.   validationSchema: Yup.object().shape({     email: Yup.string()       .email("Invalid email address")       .required("Required"),     password: Yup.string()       .min(8, "Must be more than 8 characters")       .required("Required")   }),   onSubmit(values) {     // Do stuff here...   } });

Validating at the field level or using manual triggers are fairly simple to understand. Albeit, you’ll likely use form level validation most of the time. It’s also worth checking out the docs to see other use cases.

Method 3: Using withFormik as a higher-order component

withFormik is a higher-order component and be used that way if that’s your thing. Write the form, then expose it through Formik.

A couple of practical examples

So far, we’ve become acquainted with Formik, covered the benefits of using it for creating forms in React, and covered a few methods to implement it as a React component while demonstrating various ways we can use it for validation. What we haven’t done is looked at examples of those key concepts.

So, let’s look at a couple of practical applications: displaying error messages and generating a username based on what’s entered in the email input.

Displaying error messages

We’ve built our form and validated it. And we’ve caught some errors that can be found in our errors object. But it’s no use if we aren’t actually displaying those errors.

Formik makes this a pretty trivial task. All we need to do is check the errors object returned by any of the methods we’ve looked at — <Formik/>, useFormik or withFormik — and display them:

<label className="formFieldLabel" htmlFor="email">   Email address   <span className="errorMessage">     {touched["email"] && errors["email"]}   </span> </label> <div className="formFieldWrapInner">   <input     type="email"     id="email"     className="email formField"     {...getFieldProps("email")}   /> </div>

If there’s an error during validation, {touched["email"] && errors["email"]} will display it to the user.

We could do the same with <ErrorMessage/>. With this, we only need to tell it the name of the dependent field to watch:

<ErrorMessage name="email">   {errMsg => <span className="errorMessage">{errMsg}</span>} </ErrorMessage>

Generating a username from an email address

Imagine a form that automatically generates a username for your users based on their email address. In other words, whatever the user types into the email input gets pulled out, stripped of @ and everything after it, and leaves us with a username with what’s left.

For example: jane@doe.com produces @jane.

Formik exposes helpers that can “intercept” its functionality and lets us perform some effects.In the case of auto-generating a username, one way will be through Formik’s setValues:

onSubmit(values) {   // We added a `username` value for the user which is everything before @ in their email address.   setValues({     ...values,     username: `@$ {values.email.split("@")[0]}`   }); }

Type in an email address and password, then submit the form to see your new username!

Wrapping up

Wow, we covered a lot of ground in a short amount of space. While this is merely the tip of the iceberg as far as covering all the needs of a form and what Formik is capable of doing, I hope this gives you a new tool to reach for the next time you find yourself tackling forms in a React application.

If you’re ready to take Formik to the next level, I’d suggest looking through their resources as a starting point. There are so many goodies in there and it’s a good archive of what Formik can do as well as more tutorials that get into deeper use cases.

Good luck with your forms!

The post Using Formik to Handle Forms in React appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Building a Real-Time Chat App with React and Firebase

In this article, we’ll cover key concepts for authenticating a user with Firebase in a real-time chat application. We’ll integrate third-party auth providers (e.g. Google, Twitter and GitHub) and, once users are signed in, we’ll learn how to store user chat data in the Firebase Realtime Database, where we can sync data with a NoSQL cloud database.

The client application is going to be built in React, as it is one of the most popular JavaScript frameworks out there, but the concepts can also be applied to other frameworks.

But first, what is Firebase?

Firebase is Google’s mobile platform for quickly developing apps. Firebase provides a suite of tools for authenticating applications, building reactive client apps, reporting analytics, as well as a host of other helpful resources for managing apps in general. It also provides back-end management for web, iOS, Android, and Unity, a 3D development platform.

Out of the box, Firebase is packaged with features that help developers like ourselves focus on building apps while it handles all server-side logic. Things like:

  • Authentication: This includes support for email and password authentication as well as single sign-on capabilities (via Facebook, Twitter and Google).
  • Realtime database: This is a “NoSQL” database that updates in real time.
  • Cloud functions: These run extra server-side logic.
  • Static hosting: This is a means of serving assets pre-built instead of rendering at runtime.
  • Cloud storage: This gives us a place to store media assets.

Firebase offers a generous free tier that includes authentication and access to their Realtime Database. The authentication providers we’ll be covering email and password — Google and GitHub — are free on that side as well. The Realtime Database allows up to 100 simultaneous connections and 1 gigabyte storage per month. A full table of pricing can be found on the Firebase website.

Here’s what we’re making

We’re going to build an application called Chatty. It will allow only authenticated users to send and read messages and users can sign up by providing their email and creating a password, or by authenticating through a Google or GitHub account. Check out source code if you want to refer to it or take a peek as we get started.

We’ll end up with something like this:

Setting up

You’re going to need a Google account to use Firebase, so snag one if you haven’t already. And once you do, we can officially kick the tires on this thing.

First off, head over to the Firebase Console and click the “Add project” option.

Next, let’s enter a name for the project. I’m going with Chatty.

You can choose to add analytics to your project, but it’s not required. Either way, click continue to proceed and Firebase will take a few seconds to delegate resources for the project.

Once that spins up, we are taken to the Firebase dashboard But, before we can start using Firebase in our web app, we have to get the configuration details down for our project. So, click on the web icon in the dashboard.

Then, enter a name for the app and click Register app.

Next up, we’ll copy and store the configuration details on the next screen in a safe place. That will come in handy in the next step.

Again, we’re going to authenticate users via email and password, with additional options for single sign-on with a Google or GitHub account. We need to enable these from the Authentication tab in the dashboard, but we’ll go through each of them one at a time.

Email and password authentication

There’s a Sign-in method tab in the Firebase dashboard. Click the Email/Password option and enable it.

Now we can use it in our app!

Setting up the web app

For our web app, we’ll be using React but most of the concepts can be applied to any other framework. Well need Node.js for a React setup, so download and install it if you haven’t already.

We’ll use create-react-app to bootstrap a new React project. This downloads and installs the necessary packages required for a React application. In the terminal, cd into where you’d like our Chatty project to go and run this to initialize it:

npx create-react-app chatty

This command does the initial setup for our react app and installs the dependencies in package.json. We’ll also install some additional packages. So, let’s cd into the project itself and add packages for React Router and Firebase.

cd chatty yarn add react-router-dom firebase

We already know why we need Firebase, but why React Router? Our chat app will have a couple of views we can use React Router to handle navigating between pages.

With that done, we can officially start the app:

yarn start

This starts a development server and opens a URL in your default browser. If everything got installed correctly, you should see a screen like this:

Looking at the folder structure, you would see something similar to this:

For our chat app, this is the folder structure we’ll be using:

  • /components: contains reusable widgets used in different pages
  • /helpers: a set of reusable functions
  • /pages: the app views
  • /services: third-party services that we’re using (e.g. Firebase)
  • App.js: the root component

Anything else in the folder is unnecessary for this project and can safely be removed. From here, let’s add some code to src/services/firebase.js so the app can talk with Firebase.

import firebase from 'firebase';

Let’s get Firebase into the app

We’ll import and initialize Firebase using the configuration details we copied earlier when registering the app in the Firebase dashboard. Then, we’ll export the authentication and database modules.

const config = {   apiKey: "ADD-YOUR-DETAILS-HERE",   authDomain: "ADD-YOUR-DETAILS-HERE",   databaseURL: "ADD-YOUR-DETAILS-HERE" }; firebase.initializeApp(config); export const auth = firebase.auth; export const db = firebase.database();

Let’s import our dependencies in src/App.js:

import React, { Component } from 'react'; import {   Route,   BrowserRouter as Router,   Switch,   Redirect, } from "react-router-dom"; import Home from './pages/Home'; import Chat from './pages/Chat'; import Signup from './pages/Signup'; import Login from './pages/Login'; import { auth } from './services/firebase';

These are ES6 imports. Specifically, we’re importing React and other packages needed to build out the app. We’re also importing all the pages of our app that we’ll configure later to our router.

Next up is routing

Our app has public routes (accessible without authentication) and a private route (accessible only with authentication). Because React doesn’t provide a way to check the authenticated state, we’ll create higher-order components (HOCs) for both types of routes.

Our HOCs will:

  • wrap a <Route>,
  • pass props from the router to the <Route>,
  • render the component depending on the authenticated state, and
  • redirect the user to a specified route if the condition is not met

Let’s write the code for our <PrivateRoute> HOC.

function PrivateRoute({ component: Component, authenticated, ...rest }) {   return (     <Route       {...rest}       render={(props) => authenticated === true         ? <Component {...props} />         : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}     />   ) }

It receives three props: the component to render if the condition is true, the authenticated state, and the ES6 spread operator to get the remaining parameters passed from the router. It checks if authenticated is true and renders the component passed, else it redirects to/login.

function PublicRoute({ component: Component, authenticated, ...rest }) {   return (     <Route       {...rest}       render={(props) => authenticated === false         ? <Component {...props} />         : <Redirect to='/chat' />}     />   ) }

The <PublicRoute> is pretty much the same. It renders our public routes and redirects to the /chat path if the authenticated state becomes true. We can use the HOCs in our render method:

render() {   return this.state.loading === true ? <h2>Loading...</h2> : (     <Router>       <Switch>         <Route exact path="/" component={Home}></Route>         <PrivateRoute path="/chat" authenticated={this.state.authenticated} component={Chat}></PrivateRoute>         <PublicRoute path="/signup" authenticated={this.state.authenticated} component={Signup}></PublicRoute>         <PublicRoute path="/login" authenticated={this.state.authenticated} component={Login}></PublicRoute>       </Switch>     </Router>   ); }

Checking for authentication

It would be nice to show a loading indicator while we verify if the user is authenticated. Once the check is complete, we render the appropriate route that matches the URL. We have three public routes — <Home>, <Login> and <Signup> — and a private one called <Chat>.

Let’s write the logic to check if the user is indeed authenticated.

class App extends Component {   constructor() {     super();     this.state = {       authenticated: false,       loading: true,     };   } }  export default App;

Here we’re setting the initial state of the app. Then, we’re using the componentDidMount lifecycle hook to check if the user is authenticated. So, let’s add this after the constructor:

componentDidMount() {   this.removelistener = auth().onAuthStateChanged((user) => {     if (user) {       this.setState({         authenticated: true,         loading: false,       });     } else {       this.setState({         authenticated: false,         loading: false,       });     }   }) }

Firebase provides an intuitive method called onAuthStateChanged that is triggered when the authenticated state changes. We use this to update our initial state. user is null if the user is not authenticated. If the user is true, we update authenticated to true; else we set it to false. We also set loading to false either way.

Registering users with email and password

Users will be able to register for Chatty through email and password. The helpers folder contains a set of methods that we’ll use to handle some authentication logic. Inside this folder, let’s create a new file called auth.js and add this:

import { auth } from "../services/firebase";

We import the auth module from the service we created earlier.

export function signup(email, password) {   return auth().createUserWithEmailAndPassword(email, password); } 
 export function signin(email, password) {   return auth().signInWithEmailAndPassword(email, password); }

We have two methods here: signup andsignin:

  • signup will create a new user using their email and password. 
  • signin will log in an existing user created with email and password.

Let’s create our <Signup> page by creating a new file Signup.js file in the pages folder. This is the markup for the UI:

import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import { signup } from '../helpers/auth'; 
 export default class SignUp extends Component { 
   render() {     return (       <div>         <form onSubmit={this.handleSubmit}>           <h1>             Sign Up to           <Link to="/">Chatty</Link>           </h1>           <p>Fill in the form below to create an account.</p>           <div>             <input placeholder="Email" name="email" type="email" onChange={this.handleChange} value={this.state.email}></input>           </div>           <div>             <input placeholder="Password" name="password" onChange={this.handleChange} value={this.state.password} type="password"></input>           </div>           <div>             {this.state.error ? <p>{this.state.error}</p> : null}             <button type="submit">Sign up</button>           </div>           <hr></hr>           <p>Already have an account? <Link to="/login">Login</Link></p>         </form>       </div>     )   } }
Email? Check. Password? Check. Submit button? Check. Our form is looking good.

The form and input fields are bound to a method we haven’t created yet, so let’s sort that out. Just before the render() method, we’ll add the following:

constructor(props) {   super(props);   this.state = {     error: null,     email: '',     password: '',   };   this.handleChange = this.handleChange.bind(this);   this.handleSubmit = this.handleSubmit.bind(this); }

We’re setting the initial state of the page. We’re also binding the handleChange and handleSubmit methods to the component’s this scope.

handleChange(event) {   this.setState({     [event.target.name]: event.target.value   }); }

Next up, we’ll add the handleChange method that our input fields are bound to. The method uses computed properties to dynamically determine the key and set the corresponding state variable.

async handleSubmit(event) {   event.preventDefault();   this.setState({ error: '' });   try {     await signup(this.state.email, this.state.password);   } catch (error) {     this.setState({ error: error.message });   } }

For handleSubmit, we’re preventing the default behavior for form submissions (which simply reloads the browser, among other things). We’re also clearing up the error state variable, then using the signup() method imported from helpers/auth to pass the email and password entered by the user.

If the registration is successful, users get redirected to the /Chats route. This is possible with the combination of onAuthStateChanged and the HOCs we created earlier. If registration fails, we set the error variable which displays a message to users.

Authenticating users with email and password

The login page is identical to the signup page. The only difference is we’ll be using the signin method from the helpers we created earlier. That said, let’s create yet another new file in the pages directory, this time called Login.js, with this code in it:

import React, { Component } from "react"; import { Link } from "react-router-dom"; import { signin, signInWithGoogle, signInWithGitHub } from "../helpers/auth"; 
 export default class Login extends Component {   constructor(props) {     super(props);     this.state = {       error: null,       email: "",       password: ""     };     this.handleChange = this.handleChange.bind(this);     this.handleSubmit = this.handleSubmit.bind(this);   } 
   handleChange(event) {     this.setState({       [event.target.name]: event.target.value     });   } 
   async handleSubmit(event) {     event.preventDefault();     this.setState({ error: "" });     try {       await signin(this.state.email, this.state.password);     } catch (error) {       this.setState({ error: error.message });     }   } 
   render() {     return (       <div>         <form           autoComplete="off"           onSubmit={this.handleSubmit}         >           <h1>             Login to             <Link to="/">               Chatty             </Link>           </h1>           <p>             Fill in the form below to login to your account.           </p>           <div>             <input               placeholder="Email"               name="email"               type="email"               onChange={this.handleChange}               value={this.state.email}             />           </div>           <div>             <input               placeholder="Password"               name="password"               onChange={this.handleChange}               value={this.state.password}               type="password"             />           </div>           <div>             {this.state.error ? (               <p>{this.state.error}</p>             ) : null}             <button type="submit">Login</button>           </div>           <hr />           <p>             Don't have an account? <Link to="/signup">Sign up</Link>           </p>         </form>       </div>     );   } }

Again, very similar to before. When the user successfully logs in, they’re redirected to /chat.

Authenticating with a Google account

Firebase allows us to authenticate users with a valid Google account. We’ve got to enable it in the Firebase dashboard just like we did for email and password.

Select the Google option and enable it in the settings.

On that same page, we also need to scroll down to add a domain to the list of domains that are authorized to access feature. This way, we avoid spam from any domain that is not whitelisted. For development purposes, our domain is localhost, so we’ll go with that for now.

We can switch back to our editor now. We’ll add a new method to helpers/auth.js to handle Google authentication.

export function signInWithGoogle() {   const provider = new auth.GoogleAuthProvider();   return auth().signInWithPopup(provider); }

Here, we’re creating an instance of the GoogleAuthProvider. Then, we’re calling signInWithPopup with the provider as a parameter. When this method is called, a pop up will appear and take the user through the Google sign in flow before redirecting them back to the app. You’ve likely experienced it yourself at some point in time.

Let’s use it in our signup page by importing the method:

import { signin, signInWithGoogle } from "../helpers/auth";

Then, let’s add a button to trigger the method, just under the Sign up button:

<p>Or</p> <button onClick={this.googleSignIn} type="button">   Sign up with Google </button>

Next, we’ll add the onClick handler:

async googleSignIn() {   try {     await signInWithGoogle();   } catch (error) {     this.setState({ error: error.message });   } }

Oh, and we should remember to bind the handler to the component:

constructor() {   // ...   this.githubSignIn = this.githubSignIn.bind(this); }

That’s all we need! When the button is clicked, it takes users through the Google sign in flow and, if successful, the app redirects the user to the chat route.

Authenticating with a GitHub account

We’re going to do the same thing with GitHub. May as well give folks more than one choice of account.

Let’s walk through the steps. First, we’ll enable GitHub sign in on Firebase dashboard, like we did for email and Google.

You will notice both the client ID and client secret fields are empty, but we do have our authorization callback URL at the bottom. Copy that, because we’ll use it when we do our next thing, which is register the app on GitHub.

Once that’s done, we’ll get a client ID and secret which we can now add to the Firebase console.

Let’s switch back to the editor and add a new method to helpers/auth.js:

export function signInWithGitHub() {   const provider = new auth.GithubAuthProvider();   return auth().signInWithPopup(provider); }

It’s similar to the Google sign in interface, but this time we’re creating a GithubAuthProvider. Then, we’ll call signInWithPopup with the provider.

In pages/Signup.js, we update our imports to include the signInWithGitHub method:

import { signup, signInWithGoogle, signInWithGitHub } from "../helpers/auth";

We add a button for GitHub sign up:

<button type="button" onClick={this.githubSignIn}>   Sign up with GitHub </button>

Then we add a click handler for the button which triggers the GitHub sign up flow:

async githubSignIn() {   try {     await signInWithGitHub();   } catch (error) {     this.setState({ error: error.message });   } }

Let’s remember again to bind the handler to the component:

constructor() {   // ...   this.githubSignIn = this.githubSignIn.bind(this); }

Now we’ll get the same sign-in and authentication flow that we have with Google, but with GitHub.

Reading data from Firebase

Firebase has two types of databases: A product they call Realtime Database and another called Cloud Firestore. Both databases are NoSQL-like databases, meaning the database is structured as key-value pairs. For this tutorial, we’ll use the Realtime Database.

This is the structure we’ll be using for our app. We have a root node chats with children nodes. Each child has a content, timestamp, and user ID. One of the tabs you’ll notice is Rules which is how we set permissions on the contents of the database.

Firebase database rules are defined as key-value pairs as well. Here, we’ll set our rules to allow only authenticated users to read and write to the chat node. There are a lot more firebase rules. worth checking out.

Let’s write code to read from the database. First, create a new file called Chat.js  in the pages  folder and add this code to import React, Firebase authentication, and Realtime Database:

import React, { Component } from "react"; import { auth } from "../services/firebase"; import { db } from "../services/firebase"

Next, let’s define the initial state of the app:

export default class Chat extends Component {   constructor(props) {     super(props);     this.state = {       user: auth().currentUser,       chats: [],       content: '',       readError: null,       writeError: null     };   }   async componentDidMount() {     this.setState({ readError: null });     try {       db.ref("chats").on("value", snapshot => {         let chats = [];         snapshot.forEach((snap) => {           chats.push(snap.val());         });         this.setState({ chats });       });     } catch (error) {       this.setState({ readError: error.message });     }   } }

The real main logic takes place in componentDidMount. db.ref("chats") is a reference to the chats path in the database. We listen to the value event which is triggered anytime a new value is added to the chats node. What is returned from the database is an array-like object that we loop through and push each object into an array. Then, we set the chats state variable to our resulting array. If there is an error, we set the readError state variable to the error message.

One thing to note here is that a connection is created between the client and our Firebase database because we used the .on() method. This means any time a new value is added to the database, the client app is updated in real-time which means users can see new chats without a page refresh Nice!.

After componentDidMount, we can render our chats like so:

render() {   return (     <div>       <div className="chats">         {this.state.chats.map(chat => {           return <p key={chat.timestamp}>{chat.content}</p>         })}       </div>       <div>         Login in as: <strong>{this.state.user.email}</strong>       </div>     </div>   ); }

This renders the array of chats. We render the email of the currently logged in user.

Writing data to Firebase

At the moment, users can only read from the database but are unable to send messages. What we need is a form with an input field that accepts a message and a button to send the message to the chat.

So, let’s modify the markup like so:

return (     <div>       <div className="chats">         {this.state.chats.map(chat => {           return <p key={chat.timestamp}>{chat.content}</p>         })}       </div>       {# message form #}       <form onSubmit={this.handleSubmit}>         <input onChange={this.handleChange} value={this.state.content}></input>         {this.state.error ? <p>{this.state.writeError}</p> : null}         <button type="submit">Send</button>       </form>       <div>         Login in as: <strong>{this.state.user.email}</strong>       </div>     </div>   ); }

We have added a form with an input field and a button. The value of the input field is bound to our state variable content and we call handleChange when its value changes.

handleChange(event) {   this.setState({     content: event.target.value   }); }

handleChange gets the value from the input field and sets on our state variable. To submit the form, we call handleSubmit:

async handleSubmit(event) {   event.preventDefault();   this.setState({ writeError: null });   try {     await db.ref("chats").push({       content: this.state.content,       timestamp: Date.now(),       uid: this.state.user.uid     });     this.setState({ content: '' });   } catch (error) {     this.setState({ writeError: error.message });   } }

We set any previous errors to null. We create a reference to the chats node in the database and use push() to create a unique key and pushe the object to it.

As always, we have to bind our methods to the component:

constructor(props) {   // ...   this.handleChange = this.handleChange.bind(this);   this.handleSubmit = this.handleSubmit.bind(this); }

Now a user can add new messages to the chats and see them in real-time! How cool is that?

Demo time!

Enjoy your new chat app!

Congratulations! You have just built a chat tool that authenticates users with email and password, long with options to authenticate through a Google or GitHub account.

I hope this give you a good idea of how handy Firebase can be to get up and running with authentication on an app. We worked on a chat app, but the real gem is the signup and sign-in methods we created to get into it. That’s something useful for many apps.

Questions? Thoughts? Feedback? Let me know in the comments!

The post Building a Real-Time Chat App with React and Firebase appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]