Tag: React

Anima 4.0: Go Straight From Design to React in the Design Handoff

Imagine this scenario: You get an email from the design team. It contains a link to a high-fidelity prototype of a React app they want you to build. You click the link and get ready to inspect the work only to find… the components have already been built.

Huh?

It might sound like a dream or wishful thinking, but that’s exactly what Anima 4.0 does. For years, Anima has worked to streamline the handoff between design and development, and with it’s latest update, designers are brought fully into the fold by turning designs into developer-friendly code.

Let’s repeat that again, but more specifically: Anima 4.0 lets you cherry-pick elements straight from a design and get fully written React components that just work.

The easiest design handoff ever

Anima isn’t your typical design-to-development workflow. It actually feels a little inaccurate to say that it facilitates handoffs because development is part of the process all along.

Consider what’s involved in a design handoff. Sure, it varies by organization, but they generally flow something like this:

  • Design creates high-fidelity mockups.
  • Design creates a package of the work, possibly including assets, like images and fonts.
  • Design and development meet up and talk things out, possibly with an interactive prototype.
  • Development gets started.
  • Development demos the work.
  • Design requests changes.
  • Development makes those changes.
  • And on and on…

With Anima 4.0, that process is more like this:

  • Design creates code-based prototypes.
  • Development works alongside, with the ability to reference prototypes, grab assets, generate code, and test things out.
Development is an integrated component of the design process, where code is always whether you’re in the prototype or the design application.

So, what we have is less of a handoff and more of a productive and collaborative process that saves boatloads of time… and frustration to boot.

No more “How does this thing work?”

That’s probably the question I ask the most with any design handoff. Front-enders have to be aware of so many things and that often leads to lengthy meetings and numerous emails about how things are supposed to work.

  • Where does this link to?
  • Does this have an active state?
  • Will this image be SVG?
  • …you know how it goes

That’s where Anima shines. The deliverable is not just a flat design, but a fully interactive prototype. All of the links, states, assets, and anything else you can think of is right there for you to view and interact with, including animations and effects.

Need an asset? It’s available right in the prototype and already written into the generated code.

Oh, and if your design is responsive (which, of course, it is), it’s easy as cake to see how it behaves at any breakpoint, whether you’re using the integrated browser in the design application or in the Anima prototype.

The design can be previewed in a real browser at any time directly in the design app.

Getting the responsiveness of a design down pat is probably one of the more time-consuming parts of a project. I’ve had so many back-and-forth discussions with designers that would have never happened if it was possible to test the design in a real browser during design in the design tooling that designers are probably already using, including Sketch, Figma and Adobe XD. And because Anima generates all the code, that would have saved a lot of my time trying to get the breakpoints just right. It would have also saved the designers time without having to document that behavior and answer all my questions.

How cool is it that designers can test their designs in an actual browser that’s built into their design app?!

No more “That’s not how it was designed!”

Not only do you have a prototype that realistically simulates a live site, but you get all the code you need! And no, this isn’t like the HTML and CSS generators you’ve probably seen in the past. Anima outputs extremely clean code, complete with semantic HTML elements and modern CSS features. Here’s the CSS I got from a quick design of a hero component I threw together:

@import url("https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css"); .hero {   background-color: transparent;   flex-shrink: 0;   height: 1037px;   position: relative;   width: 505px; } .hero-container {   background-color: var(--royal-blue);   height: 1024px;   left: 0px;   position: absolute;   top: 0px;   width: 505px; } .shape-circle {   background-color: transparent;   height: 444px;   left: 283px;   position: absolute;   top: 593px;   width: 222px; } .shape-dots {   background-color: transparent;   height: 646px;   left: 43px;   position: absolute;   top: 189px;   width: 418px; } .shape-triangle {   background-color: transparent;   height: 332px;   left: 0px;   position: absolute;   top: 79px;   width: 269px; } .video {   background-color: transparent;   height: 294px;   left: 43px;   overflow: hidden;   position: absolute;   top: 278px;   width: 418px; } :root {   --royal-blue: rgba(67,83,255,1.0); }

Lots of precise numbers in there that normally would have taken some time-consuming guesswork. And those are class names and custom properties I can actually pronounce! Will I change any of that code? Maybe! But at least I was part of the process all along, and have a solid head start that I would have otherwise spent time writing myself.

But, the real gem here is that Anima 4.0 goes where no other platform has gone because it can…

Turn anything into a functional React component

All it took was a single click and here’s what I got:

import React from "react";  function App(props) {   return (     <div className={`hero $ {props.className || ""}`}>       <div className="hero-container"></div>       <img         className="shape-circle"         src="https://anima-uploads.s3.amazonaws.com/projects/5f8e220bdff56f27ee5b7cc7/releases/5f9082de53033dac763b4b6c/img/desktop-hd-learn-path-2-DC8E0494-121C-40B1-8AE1-3C8BEAC833A7.png"       />       <img         className="shape-triangle"         src="https://anima-uploads.s3.amazonaws.com/projects/5f8e220bdff56f27ee5b7cc7/releases/5f9082de53033dac763b4b6c/img/desktop-hd-home-triangle2x-BA81FE1D-AE06-47A2-91D5-20EC51D5F0F8.png"       />       <img         className="shape-dots"         src="https://anima-uploads.s3.amazonaws.com/projects/5f8e220bdff56f27ee5b7cc7/releases/5f9082de53033dac763b4b6c/img/desktop-hd-home-rectangle2x-4EFFE4A8-CAD1-47C7-A175-D3256F2E5124.png"       />       <div className="video">         <iframe           style="border: 0; pointer-events: auto;"           id="ytplayer"           type="text/html"           width="100%"           height="100%"           src="https://www.youtube.com/embed/rk71kS4cY7E?rel=0"           frameborder="0"           allowfullscreen="allowfullscreen"           mozallowfullscreen="mozallowfullscreen"           msallowfullscreen="msallowfullscreen"           oallowfullscreen="oallowfullscreen"           webkitallowfullscreen="webkitallowfullscreen"         ></iframe>       </div>     </div>   ); }  export default App;

This is real — and brand new in Anima 4.0! And I can do this with any element in the Anima interface. Select an element, mark it as a component, then generate the code.

You can expect the same for Vue and Angular in future releases.

Why this is a big deal

Perhaps it’s obvious by now, but I see tons of benefits from where I sit as a front-end developer. Getting HTML and CSS is great, but having a tool like this that integrates with modern frameworks and code practices is more than impressive — it’s a game-changer. There’s a lot less context switching and time spent on things that I’d rather spend doing better work (or getting started on the next project)!

Like many of you, I straddle the line between design and development and see how this fills a lot of the gaps on the design side of things as well. I can’t get over the in-app browser previews. All of the time spent design QA’ing responsive breakpoints instantly opens up when that stuff can be done at the point of design — not to mention the time saved with the code it generates.

Here’s a quick video of moving from Adobe XD to a real rendered React components in the browser:

Anima 4.0 is available… today

As in, it literally shipped today, October 27. In fact, there’s a virtual party happening and you’re invited. I’m told it’s going to be an epic geeky event with great folks, demos, and even gifts. Hope to see you there!


The post Anima 4.0: Go Straight From Design to React in the Design Handoff appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,

Pre-Caching Image with React Suspense

Suspense is an exciting, upcoming feature of React that will enable developers to easily allow their components to delay rendering until they’re “ready,” leading to a much smoother user experience. “Ready,” in this context, can mean a number of things. For example, your data loading utility can tie into Suspense, allowing for consistent loading states to be displayed when any data are in flight, without needing to manually track loading state per query. Then, when your data are available, and your component is “ready,” it’ll render. This is the subject that’s most commonly discussed with Suspense, and I’ve written about it previously; however, data loading is only one use case among many where Suspense can improve user experience. Another one I want to talk about today is image preloading.

Have you ever made, or used a web app where, after landing on a screen, your place on it staggers and jumps as images download and render? We call that content reflow and it can both be jarring and unpleasant. Suspense can help with this. You know how I said that Suspense is all about holding a component back from rendering until it’s ready? Fortunately, “ready” in this context is pretty open-ended — and for our purposes can included “images we need that have preloaded.” Let’s see how!

Quick crash course on Suspense

Before we dive into specifics, let’s take a quick look at how Suspense works. It has two main parts. The first is the concept of a component suspending. This means React attempts to render our component, but it’s not “ready.” When this happens, the nearest “fallback” in the component tree will render. We’ll look at making fallbacks shortly (it’s fairly straightforward), but the way in which a component tells React it’s not ready is by throwing a promise. React will catch that promise, realize the component isn’t ready, and render the fallback. When the promise resolves, React will again attempt to r.ender. Rinse, wash and repeat. Yes, I’m over-simplifying things a tad, but this is the gist of how Suspense works and we’ll expand on some of these concepts as we go.

The second part of Suspense is the introduction of “transition” state updates. This means we set state, but tell React that the state change may cause a component to suspend, and if this happens, to not render a fallback. Instead, we want to continue viewing the current screen, until the state update is ready, at which point it’ll render. And, of course, React provides us with a “pending” boolean indicator that lets the developer know this is in progress so we can provide inline loading feedback.

Let’s preload some images!

First off, I want to note that there’s a full demo of what we’re making at the end of this article. Feel free to open the demo now if you just want to jump into the code. It’ll show how to preload images with Suspense, combined with transition state updates. The rest of this post will build that code up step-by-step, explaining the how’s the why’s along the way.

OK, let’s go!

We want our component to suspend until all of its images have preloaded. To make things as simple as possible, let’s make a <SuspenseImage> component that receives a src attribute, preloads the image, handles the exception throwing, and then renders an <img> when everything’s ready. Such a component would allow us to seamlessly drop our <SuspenseImage> component wherever we want an image displayed, and Suspense would handle the grunt work of holding onto it until everything is ready.

We can start by making a preliminary sketch of the code:

const SuspenseImg = ({ src, ...rest }) => {   // todo: preload and throw somehow   return <img alt="" src={src} {...rest} />; }; 

So we have two things to sort out: (1) how to preload an image, and (2) tying in exception throwing. The first part is pretty straightforward. We’re all used to using images in HTML via <img src="some-image.png"> but we can also create images imperatively using the Image() object in JavaScript; moreover, images we create like this have an onload callback that fires when the image has … loaded. It looks like this:

const img = new Image(); img.onload = () => {   // image is loaded }; 

But how do we tie that into exception throwing? If you’re like me, your first inclination might be something like this:

const SuspenseImg = ({ src, ...rest }) => {   throw new Promise((resolve) => {     const img = new Image();     img.onload = () => {       resolve();     };   });   return <img alt="" src={src} {...rest} />; }; 

The problem, of course, is that this will always throw a promise. Every single time React attempts to render a <SuspenseImg> instance, a new promise will be created, and promptly thrown. Instead, we only want to throw a promise until the image has loaded. There’s an old saying that every problem in computer science can be solved by adding a layer of indirection (except for the problem of too many layers of indirection) so let’s do just that and build an image cache. When we read a src, the cache will check if it’s loaded that image, and if not, it’ll begin the preload, and throw the exception. And, if the image is preloaded, it’ll just return true and let React get on with rendering our image. 

Here’s what our <SuspenseImage> component looks like:

export const SuspenseImg = ({ src, ...rest }) => {   imgCache.read(src);   return <img src={src} {...rest} />; };

And here’s what a minimal version of our cache looks like:

const imgCache = {   __cache: {},   read(src) {     if (!this.__cache[src]) {       this.__cache[src] = new Promise((resolve) => {         const img = new Image();         img.onload = () => {           this.__cache[src] = true;           resolve(this.__cache[src]);         };         img.src = src;       }).then((img) => {         this.__cache[src] = true;       });     }     if (this.__cache[src] instanceof Promise) {       throw this.__cache[src];     }     return this.__cache[src];   } };

It’s not perfect, but it’s good enough for now. Let’s go ahead and put it to use.

The implementation

Remember, there’s a link to the fully working demo below, so if I move too fast at any particular step, don’t despair. We’ll explain things as well go.

Let’s start by defining our fallback. We define a fallback by placing a Suspense tag in our component tree, and pass our fallback via the fallback prop. Any component which suspends will search upward for the nearest Suspense tag, and render its fallback (but if no Suspense tag is found, an error will be thrown). A real app would likely have many Suspense tags throughout, defining specific fallbacks for its various modules, but for this demo, we only need a single one wrapping our root app.

function App() {   return (     <Suspense fallback={<Loading />}>       <ShowImages />     </Suspense>   ); }

The <Loading> component is a basic spinner, but in a real app, you’d likely want to render some sort of empty shell of the actual component you’re trying to render, to provide a more seamless experience. 

With that in place, our <ShowImages> component eventually renders our images with this:

<FlowItems>   {images.map(img => (     <div key={img}>       <SuspenseImg alt="" src={img} />     </div>   ))} </FlowItems>

On initial load, our loading spinner will show, until our initial images are ready, at which point they all show at once, without any staggered reflow jankiness.

Transition state update

Once the images are in place, when we load the next batch of them, we’d like to have them show up after they’ve loaded, of course, but keep the existing images on the screen while they load. We do this with the useTransition hook. This returns a startTransition function, and an isPending boolean, which indicates that our state update is in progress, but has suspended (or even if it hasn’t suspended, may still be true if the state update is simply taking too long). Lastly, when calling useTransition, you need to pass a timeoutMs value, which is the maximum amount of time the isPending flag can be true, before React just gives up and renders the fallback (note, the timeoutMs argument will likely be removed in the near future, with the transition state updates simply waiting as long as necessary when updating existing content).

Here’s what mine looks like:

const [startTransition, isPending] = useTransition({ timeoutMs: 10000 });

We’ll allow for 10 seconds to pass before our fallback shows, which is likely too long in real life, but is suitable for the purposes of this demo, especially when you might be purposefully slowing your network speed down in DevTools to experiment.

Here’s how we use it. When you click the button to load more images, the code looks like this:

startTransition(() => {   setPage(p => p + 1); });

That state update will trigger a new data load using my GraphQL client micro-graphql-react, which, being Suspense-compatible, will throw a promise for us while the query is in flight. Once the data come back, our component will attempt to render, and suspend again while our images are preloading. While all of this is happening, our isPending value will be true, which will allow us to display a loading spinner on top of our existing content.

Avoiding network waterfalls 

You might be wondering how React blocks rendering while image preloading is taking place. With the code above, when we do this:

{images.map(img => (

…along with our <SuspenseImage> rendered therein, will React attempt to render the first image, Suspend, then re-attempt the list, get past the first image, which is now in our cache, only to suspend on the second image, then the third, fourth, etc. If you’ve read about Suspense before, you might be wondering if we need to manually preload all the images in our list before all this rendering occurs.

It turns out there’s no need to worry, and no need for awkward preloading because React is fairly smart about how it renders things in a Suspense world. As React is making its way through our component tree, it doesn’t just stop when it hits a suspension. Instead, it continues rendering all other paths through our component tree. So, yeah, when it attempts to render image zero, a suspension will occur, but React will continue attempting to render images 1 through N, and only then suspend.

You can see this in action by looking at the Network tab in the full demo, when you click the “Next images” button. You should see the entire bucket of images immediately show up in the network list, resolve one by one, and when all finished, the results should show up on screen. To really amplify this effect, you might want to slow your network speed down to “Fast 3G.”

For fun, we can force Suspense to waterfall over our images by manually reading each image from our cache before React attempts to render our component, diving through every path in the component tree.

images.forEach((img) => imgCache.read(img));

I created a demo that illustrates this. If you similarly look at the Network tab when a new set of images comes in, you’ll see them added sequentially in the network list (but don’t run this with your network speed slowed down).

Suspend late

There’s a corollary to keep in mind when using Suspense: suspend as late in the rendering and as low in the component tree as possible. If you have some sort of <ImageList> which renders a bunch of suspending images, make sure each and every image suspends in its own component so React can reach it separately, and so none will block the others, resulting in a waterfall. 

The data loading version of this rule is that data should be loaded as late as possible by the components that actually need it. That means we should avoid doing something like this in a single component:

const { data1 } = useSuspenseQuery(QUERY1, vars1); const { data2 } = useSuspenseQuery(QUERY2, vars2);

The reason we want to avoid that is because query one will suspend, followed by query two, causing a waterfall. If this is simply unavoidable, we’ll need to manually preload both queries before the suspensions.

The demo

Here’s the demo I promised. It’s the same one I linked up above.

If you run it with your dev tools open, make sure you uncheck the box that says “Disable Cache” in the DevTools Network tab, or you’ll defeat the entire demo. 

The code is almost identical to what I showed earlier. One improvement in the demo is that our cache read method has this line:

setTimeout(() => resolve({}), 7000);

It’s nice to have all our images preloaded nicely, but in real life we probably don’t want to hold up rendering indefinitely just because one or two straggling images are coming in slowly. So after some amount of time, we just give the green light, even though the image isn’t ready yet. The user will see an image or two flicker in, but it’s better than enduring the frustration of frozen software. I’ll also note that seven seconds is probably excessive, but for this demo, I’m assuming users might be slowing network speeds in DevTools to see Suspense features more clearly, and wanted to support that.

The demo also has a precache images checkbox. It’s checked by default, but you can uncheck it to replace the <SuspenseImage> component with a regular ol’ <img> tag, if you want to compare the Suspense version to “normal React” (just don’t check it while results are coming in, or the whole UI may suspend, and render the fallback).

Lastly, as always with CodeSandbox, some state may occasionally get out of sync, so hit the refresh button if things start to look weird or broken.

Odds and ends

There was one massive bug I accidentally made when putting this demo together. I didn’t want multiple runs of the demo to lose their effect as the browser caches images it’s already downloaded. So I manually modify all of the URLs with a cache buster:

const [cacheBuster, setCacheBuster] = useState(INITIAL_TIME); 
 const { data } = useSuspenseQuery(GET_IMAGES_QUERY, { page }); const images = data.allBooks.Books.map(   (b) => b.smallImage + `?cachebust=$  {cacheBuster}` );

INITIAL_TIME is defined at the modules level (i.e. globally) with this line:

const INITIAL_TIME = +new Date();

And if you’re wondering why I didn’t do this instead:

const [cacheBuster, setCacheBuster] = useState(+new Date());

…it’s because this does horrible, horrible things. On first render, the images attempt to render. The cache causes a suspension, and React cancels the render, and shows our fallback. When all of the promises have resolved, React will attempt this initial render anew, and our initial useState call will re-run, which means that this:

const [cacheBuster, setCacheBuster] = useState(+new Date());

…will re-run, with a new initial value, causing an entirely new set of image URLs, which will suspend all over again, ad infinitum. The component will never run, and the CodeSandbox demo grinds to a halt (making this frustrating to debug).

This might seem like a weird one-off problem caused by a unique requirement for this particular demo, but there’s a larger lesson: rendering should be pure, without side effects. React should be able to re-attempt rendering your component any number of times, and (given the same initial props) the same exact state should come out the other end.


The post Pre-Caching Image with React Suspense appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

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

, , , , ,
[Top]

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]