Tag: React

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

, ,

Everything You Need to Know About FLIP Animations in React

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

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

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

FLIP and WAAPI

FLIP animations are made much easier by the WAAPI!

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

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

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

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

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

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

Here’s the outline of the WAAPI version:

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

FLIP and React

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

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

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

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

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

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

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

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

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

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

Caveats

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

Keep it under 100ms

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

Unnecessary renders

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

Layout thrashing

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

Animation canceling

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

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

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

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

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

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

Don’t fight the browser

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

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

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

Libraries

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

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


Resources and references

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

CSS-Tricks

, , , , , ,
[Top]

React Single File Components Are Here

Shawn Wang is talking about RedwoodJS here:

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

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

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

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

CSS-Tricks

, , , ,
[Top]

The Anatomy of a Tablist Component in Vanilla JavaScript Versus React

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

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

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

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

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

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

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

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

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

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

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

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

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

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

For reference, you can tinker with our live examples:

Reaktiv Studios UI components
Reaktiv Studios UI components

Flat HTML examples

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

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

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

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

Tabs (with ARIA)

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

Accordion (without ARIA)

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

Accordion (with ARIA)

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

Vanilla JavaScript examples

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

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

File: getDomFallback.js

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

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

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

File: unique.js

This function is a poor man’s UUID equivalent.

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

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

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

File: tablist.js

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

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

Function: getTabId and getPanelId

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

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

Function: globalClick

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

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

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

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

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

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

We want to make sure it:

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

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

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

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

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

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

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

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

Function: addAriaAttributes

The astute reader might be thinking:

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

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

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

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

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

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

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

Tabs:

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

Panels:

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

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

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

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

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

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

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

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

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

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

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

Then we ensure that role="tab" exists.

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

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

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

If the HTML is compiled, via tools such as Parcel

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

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

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

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

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

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

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

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

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

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

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

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

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

React examples

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

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

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

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

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

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

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

File: Tabs.js

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

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

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

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

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

Function: getIsActive

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

This current tab is active if:

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

Function: getTabsList

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

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

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

Function: getPanelsList

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

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

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

Function: Tabs

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

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

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

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

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

File: Accordion.js

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

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

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

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

Function: handleClick

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

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

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

If you were beginning to think…

“I would store that in an object.”

…then congrats. You are right!

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

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

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

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

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

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

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

Conclusion

Still here? Awesome.

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

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

HTML

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

JSX

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

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

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

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

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

CSS-Tricks

, , , , , ,
[Top]

Creating a Gauge in React

, ,
[Top]

React Integration Testing: Greater Coverage, Fewer Tests

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

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

There are three types of tests:

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

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

Getting started with React tests

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

You can log in successfully:

…or encounter an error message from the API:

The code is structured like this:

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

Option 1: Unit tests

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

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

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

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

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

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

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

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

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

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

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

Option 2: Integration tests

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

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

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

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

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

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

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

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

So, what does need a unit test?

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

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

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

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

Other goodies

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

Big waitFor Blocks

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Inline it comments

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

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

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

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

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

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

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

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

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

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

Next steps for teams

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

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

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

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

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

CSS-Tricks

, , , , , ,
[Top]

Using Formik to Handle Forms in React

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

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

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

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

Creating a form as a React component

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

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

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

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

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

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

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

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

OK, so why Formik again?

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

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

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

Going Formik

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

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

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

Method 1: Using the useFormik hook

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

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

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

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

Then we’ll bind Formik to our form elements:

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

This is how the binding works:

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

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

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

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

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

Method 2: Using Formik with React context

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Form validation with Formik

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

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

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

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

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

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

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

Here’s an example using validate:

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

And here we go with an example using validationSchema instead:

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

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

Method 3: Using withFormik as a higher-order component

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

A couple of practical examples

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

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

Displaying error messages

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

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

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

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

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

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

Generating a username from an email address

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

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

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

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

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

Wrapping up

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

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

Good luck with your forms!

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

CSS-Tricks

, , , ,
[Top]

Building a Real-Time Chat App with React and Firebase

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

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

But first, what is Firebase?

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

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

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

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

Here’s what we’re making

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

We’ll end up with something like this:

Setting up

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

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

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

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

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

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

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

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

Email and password authentication

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

Now we can use it in our app!

Setting up the web app

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

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

npx create-react-app chatty

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

cd chatty yarn add react-router-dom firebase

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

With that done, we can officially start the app:

yarn start

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

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

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

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

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

import firebase from 'firebase';

Let’s get Firebase into the app

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

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

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

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

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

Next up is routing

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

Our HOCs will:

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

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

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

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

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

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

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

Checking for authentication

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

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

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

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

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

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

Registering users with email and password

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

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

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

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

We have two methods here: signup andsignin:

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

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

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

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

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

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

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

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

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

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

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

Authenticating users with email and password

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

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

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

Authenticating with a Google account

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

Select the Google option and enable it in the settings.

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

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

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

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

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

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

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

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

Next, we’ll add the onClick handler:

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

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

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

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

Authenticating with a GitHub account

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

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

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

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

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

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

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

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

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

We add a button for GitHub sign up:

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

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

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

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

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

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

Reading data from Firebase

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

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

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

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

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

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

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

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

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

After componentDidMount, we can render our chats like so:

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

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

Writing data to Firebase

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

So, let’s modify the markup like so:

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

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

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

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

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

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

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

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

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

Demo time!

Enjoy your new chat app!

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

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

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

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

CSS-Tricks

, , , ,
[Top]

React Suspense in Practice

This post is about understanding how Suspense works, what it does, and seeing how it can integrate into a real web app. We’ll look at how to integrate routing and data loading with Suspense in React. For routing, I’ll be using vanilla JavaScript, and I’ll be using my own micro-graphql-react GraphQL library for data.

If you’re wondering about React Router, it seems great, but I’ve never had the chance to use it. My own side project has a simple enough routing story that I always just did it by hand. Besides, using vanilla JavaScript will give us a better look at how Suspense works.

A little background

Let’s talk about Suspense itself. Kingsley Silas provides a thorough overview of it, but the first thing to note is that it’s still an experimental API. That means — and React’s docs say the same — not to lean on it yet for production-ready work. There’s always a chance it will change between now and when it’s fully complete, so please bear that in mind.

That said, Suspense is all about maintaining a consistent UI in the face of asynchronous dependencies, such as lazily loaded React components, GraphQL data, etc. Suspense provides low-level API’s that allow you to easily maintain your UI while your app is managing these things.

But what does “consistent” mean in this case? It means not rendering a UI that’s partially complete. It means, if there are three data sources on the page, and one of them has completed, we don’t want to render that updated piece of state, with a spinner next to the now-outdated other two pieces of state.

What we do want to do is indicate to the user that data are loading, while continuing to show either the old UI, or an alternative UI which indicates we’re waiting on data; Suspense supports either, which I’ll get into.

What exactly Suspense does

This is all less complicated than it may seem. Traditionally in React, you’d set state, and your UI would update. Life was simple. But it also led to the sorts of inconsistencies described above. What Suspense adds is the ability to have a component notify React at render time that it’s waiting for asynchronous data; this is called suspending, and it can happen anywhere in a component’s tree, as many times as needed, until the tree is ready. When a component suspends, React will decline to render the pending state update until all suspended dependencies have been satisfied.

So what happens when a component suspends? React will look up the tree, find the first <Suspense> component, and render its fallback. I’ll be providing plenty of examples, but for now, know that you can provide this:

<Suspense fallback={<Loading />}>

…and the <Loading /> component will render if any child components of <Suspense> are suspended.

But what if we already have a valid, consistent UI, and the user loads new data, causing a component to suspend? This would cause the entire existing UI to un-render, and the fallback to show. That’d still be consistent, but hardly a good UX. We’d prefer the old UI stay on the screen while the new data are loading.

To support this, React provides a second API, useTransition, which effectively makes a state change in memory. In other words, it allows you to set state in memory while keeping your existing UI on screen; React will literally keep a second copy of your component tree rendered in memory, and set state on that tree. Components may suspend, but only in memory, so your existing UI will continue to show on the screen. When the state change is complete, and all suspensions have resolved, the in-memory state change will render onto the screen. Obviously you want to provide feedback to your user while this is happening, so useTransition provides a pending boolean, which you can use to display some sort of inline “loading” notification while suspensions are being resolved in memory.

When you think about it, you probably don’t want your existing UI to show indefinitely while your loading is pending. If the user tries to do something, and a long period of time elapses before it’s finished, you should probably consider the existing UI outdated and invalid. At this point, you probably will want your component tree to suspend, and your <Suspense> fallback to display.

To accomplish this, useTransition takes a timeoutMs value. This indicates the amount of time you’re willing to let the in-memory state change run, before you suspend.

const Component = props => {   const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });   // ..... };

Here, startTransition is a function. When you want to run a state change “in memory,” you call startTransition, and pass a lambda expression that does your state change.

startTransition(() => {   dispatch({ type: LOAD_DATA_OR_SOMETHING, value: 42 }); })

You can call startTransition wherever you want. You can pass it to child components, etc. When you call it, any state change you perform will happen in memory. If a suspension happens, isPending will become true, which you can use to display some sort of inline loading indicator.

That’s it. That’s what Suspense does.

The rest of this post will get into some actual code to leverage these features.

Example: Navigation

To tie navigation into Suspense, you’ll be happy to know that React provides a primitive to do this: React.lazy. It’s a function that takes a lambda expression that returns a Promise, which resolves to a React component. The result of this function call becomes your lazily loaded component. It sounds complicated, but it looks like this:

const SettingsComponent = lazy(() => import("./modules/settings/settings"));

SettingsComponent is now a React component that, when rendered (but not before), will call the function we passed in, which will call import() and load the JavaScript module located at ./modules/settings/settings.

The key piece is this: while that import() is in flight, the component rendering SettingsComponent will suspend. It seems we have all the pieces in hand, so let’s put them together and build some Suspense-based navigation.

Navigation helpers

But first, for context, I’ll briefly cover how navigation state is managed in this app, so the Suspense code will make more sense.

I’ll be using my booklist app. It’s just a side project of mine I mainly keep around to mess around with bleeding-edge web technology. It was written by me alone, so expect parts of it to be a bit unrefined (especially the design).

The app is small, with about eight different modules a user can browse to, without any deeper navigation. Any search state a module might use is stored in the URL’s query string. With this in mind, there are a few methods which scrape the current module name, and search state from the URL. This code uses the query-string and history packages from npm, and looks somewhat like this (some details have been removed for simplicity, like authentication).

import createHistory from "history/createBrowserHistory"; import queryString from "query-string"; export const history = createHistory(); export function getCurrentUrlState() {   let location = history.location;   let parsed = queryString.parse(location.search);   return {     pathname: location.pathname,     searchState: parsed   }; } export function getCurrentModuleFromUrl() {   let location = history.location;   return location.pathname.replace(///g, "").toLowerCase(); }

I have an appSettings reducer that holds the current module and searchState values for the app, and uses these methods to sync with the URL when needed.

The pieces of a Suspense-based navigation

Let’s get started with some Suspense work. First, let’s create the lazy-loaded components for our modules.

const ActivateComponent = lazy(() => import("./modules/activate/activate")); const AuthenticateComponent = lazy(() =>   import("./modules/authenticate/authenticate") ); const BooksComponent = lazy(() => import("./modules/books/books")); const HomeComponent = lazy(() => import("./modules/home/home")); const ScanComponent = lazy(() => import("./modules/scan/scan")); const SubjectsComponent = lazy(() => import("./modules/subjects/subjects")); const SettingsComponent = lazy(() => import("./modules/settings/settings")); const AdminComponent = lazy(() => import("./modules/admin/admin"));

Now we need a method that chooses the right component based on the current module. If we were using React Router, we’d have some nice <Route /> components. Since we’re rolling this manually, a switch will do.

export const getModuleComponent = moduleToLoad => {   if (moduleToLoad == null) {     return null;   }   switch (moduleToLoad.toLowerCase()) {     case "activate":       return ActivateComponent;     case "authenticate":       return AuthenticateComponent;     case "books":       return BooksComponent;     case "home":       return HomeComponent;     case "scan":       return ScanComponent;     case "subjects":       return SubjectsComponent;     case "settings":       return SettingsComponent;     case "admin":       return AdminComponent;   }      return HomeComponent; };

The whole thing put together

With all the boring setup out of the way, let’s see what the entire app root looks like. There’s a lot of code here, but I promise, relatively few of these lines pertain to Suspense, and I’ll cover all of it.

const App = () => {   const [startTransitionNewModule, isNewModulePending] = useTransition({     timeoutMs: 3000   });   const [startTransitionModuleUpdate, moduleUpdatePending] = useTransition({     timeoutMs: 3000   });   let appStatePacket = useAppState();   let [appState, _, dispatch] = appStatePacket;   let Component = getModuleComponent(appState.module);   useEffect(() => {     startTransitionNewModule(() => {       dispatch({ type: URL_SYNC });     });   }, []);   useEffect(() => {     return history.listen(location => {       if (appState.module != getCurrentModuleFromUrl()) {         startTransitionNewModule(() => {           dispatch({ type: URL_SYNC });         });       } else {         startTransitionModuleUpdate(() => {           dispatch({ type: URL_SYNC });         });       }     });   }, [appState.module]);   return (     <AppContext.Provider value={appStatePacket}>       <ModuleUpdateContext.Provider value={moduleUpdatePending}>         <div>           <MainNavigationBar />           {isNewModulePending ? <Loading /> : null}           <Suspense fallback={<LongLoading />}>             <div id="main-content" style={{ flex: 1, overflowY: "auto" }}>               {Component ? <Component updating={moduleUpdatePending} /> : null}             </div>           </Suspense>         </div>       </ModuleUpdateContext.Provider>     </AppContext.Provider>   ); };

First, we have two different calls to useTransition. We’ll use one for routing to a new module, and the other for updating search state for the current module. Why the difference? Well, when a module’s search state is updating, that module will likely want to display an inline loading indicator. That updating state is held by the moduleUpdatePending variable, which you’ll see I put on context for the active module to grab, and use as needed:

<div>   <MainNavigationBar />   {isNewModulePending ? <Loading /> : null}   <Suspense fallback={<LongLoading />}>     <div id="main-content" style={{ flex: 1, overflowY: "auto" }}>       {Component ? <Component updating={moduleUpdatePending} /> : null} // highlight     </div>   </Suspense> </div>

The appStatePacket is the result of the app state reducer I discussed above (but did not show). It contains various pieces of application state which rarely change (color theme, offline status, current module, etc).

let appStatePacket = useAppState();

A little later, I grab whichever component happens to be active, based on the current module name. Initially this will be null.

let Component = getModuleComponent(appState.module);

The first call to useEffect will tell our appSettings reducer to sync with the URL at startup.

useEffect(() => {   startTransitionNewModule(() => {     dispatch({ type: URL_SYNC });   }); }, []);

Since this is the initial module the web app navigates to, I wrap it in startTransitionNewModule to indicate that a fresh module is loading. While it might be tempting to have the appSettings reducer have the initial module name as its initial state, doing this prevents us from calling our startTransitionNewModule callback, which means our Suspense boundary would render the fallback immediately, instead of after the timeout.

The next call to useEffect sets up a history subscription. No matter what, when the url changes we tell our app settings to sync against the URL. The only difference is which startTransition that same call is wrapped in.

useEffect(() => {   return history.listen(location => {     if (appState.module != getCurrentModuleFromUrl()) {       startTransitionNewModule(() => {         dispatch({ type: URL_SYNC });       });     } else {       startTransitionModuleUpdate(() => {         dispatch({ type: URL_SYNC });       });     }   }); }, [appState.module]);

If we’re browsing to a new module, we call startTransitionNewModule. If we’re loading a component that hasn’t been loaded already, React.lazy will suspend, and the pending indicator visible only to the app’s root will set, which will show a loading spinner at the top of the app while the lazy component is fetched and loaded. Because of how useTransition works, the current screen will continue to show for three seconds. If that time expires and the component is still not ready, our UI will suspend, and the fallback will render, which will show the <LongLoading /> component:

{isNewModulePending ? <Loading /> : null} <Suspense fallback={<LongLoading />}>   <div id="main-content" style={{ flex: 1, overflowY: "auto" }}>     {Component ? <Component updating={moduleUpdatePending} /> : null}   </div> </Suspense>

If we’re not changing modules, we call startTransitionModuleUpdate:

startTransitionModuleUpdate(() => {   dispatch({ type: URL_SYNC }); });

If the update causes a suspension, the pending indicator we’re putting on context will be triggered. The active component can detect that and show whatever inline loading indicator it wants. As before, if the suspension takes longer than three seconds, the same Suspense boundary from before will be triggered… unless, as we’ll see later, there’s a Suspense boundary lower in the tree.

One important thing to note is that these three-second timeouts apply not only to the component loading, but also being ready to display. If the component loads in two seconds, and, when rendering in memory (since we’re inside of a startTransition call) suspends, the useTransition will continue to wait for up to one more second before Suspending.

In writing this blog post, I used Chrome’s slow network modes to help force loading to be slow, to test my Suspense boundaries. The settings are in the Network tab of Chrome’s dev tools.

Let’s open our app to the settings module. This will be called:

dispatch({ type: URL_SYNC });

Our appSettings reducer will sync with the URL, then set module to “settings.” This will happen inside of startTransitionNewModule so that, when the lazy-loaded component attempts to render, it’ll suspend. Since we’re inside startTransitionNewModule, the isNewModulePending will switch over to true, and the <Loading /> component will render.

If the component is still not ready to render within three seconds, the in-memory version of our component tree will switch over, suspend, and our Suspense boundary will render the <LongLoading /> component.
When it’s done, the settings module will show.

So what happens when we browse somewhere new? Basically the same thing as before, except this call:

dispatch({ type: URL_SYNC });

…will come from the second instance of useEffect. Let’s browse to the books module and see what happens. First, the inline spinner shows as expected:

If the three-second timeout elapses, our Suspense boundary will render its fallback:
And, eventually, our books module loads:

Searching and updating

Let’s stay within the books module, and update the URL search string to kick off a new search. Recall from before that we were detecting the same module in that second useEffect call and using a dedicated useTransition call for it. From there, we were putting the pending indicator on context for whichever module was active for us to grab and use.

Let’s see some code to actually use that. There’s not really much Suspense-related code here. I’m grabbing the value from context, and if true, rendering an inline spinner on top of my existing results. Recall that this happens when a useTransition call has begun, and the app is suspended in memory. While that’s happening, we continue to show the existing UI, but with this loading indicator.

const BookResults: SFC<{ books: any; uiView: any }> = ({ books, uiView }) => {   const isUpdating = useContext(ModuleUpdateContext);   return (     <>       {!books.length ? (         <div           className="alert alert-warning"           style={{ marginTop: "20px", marginRight: "5px" }}         >           No books found         </div>       ) : null}       {isUpdating ? <Loading /> : null}       {uiView.isGridView ? (         <GridView books={books} />       ) : uiView.isBasicList ? (         <BasicListView books={books} />       ) : uiView.isCoversList ? (         <CoversView books={books} />       ) : null}     </>   ); };

Let’s set a search term and see what happens. First, the inline spinner displays.

Then, if the useTransition timeout expires, we’ll get the Suspense boundary’s fallback. The books module defines its own Suspense boundary in order to provide a more fine-tuned loading indicator, which looks like this:

This is a key point. When making Suspense boundary fallbacks, try not to throw up any sort of spinner and “loading” message. That made sense for our top-level navigation because there’s not much else to do. But when you’re in a specific part of your application, try to make your fallback re-use many of the same components with some sort of loading indicator where the data would be — but with everything else disabled.

This is what the relevant components look like for my books module:

const RenderModule: SFC<{}> = ({}) => {   const uiView = useBookSearchUiView();   const [lastBookResults, setLastBookResults] = useState({     totalPages: 0,     resultsCount: 0   });   return (     <div className="standard-module-container margin-bottom-lg">       <Suspense fallback={<Fallback uiView={uiView} {...lastBookResults} />}>         <MainContent uiView={uiView} setLastBookResults={setLastBookResults} />       </Suspense>     </div>   ); }; const Fallback: SFC<{   uiView: BookSearchUiView;   totalPages: number;   resultsCount: number; }> = ({ uiView, totalPages, resultsCount }) => {   return (     <>       <BooksMenuBarDisabled         totalPages={totalPages}         resultsCount={resultsCount}       />       {uiView.isGridView ? (         <GridViewShell />       ) : (         <h1>           Books are loading <i className="fas fa-cog fa-spin"></i>         </h1>       )}     </>   ); };

A quick note on consistency

Before we move on, I’d like to point out one thing from the earlier screenshots. Look at the inline spinner that displays while the search is pending, then look at the screen when that search suspended, and next, the finished results:

Notice how there’s a “C++” label to the right of the search pane, with an option to remove it from the search query? Or rather, notice how that label is only on the second two screenshots? The moment the URL updates, the application state governing that label is updated; however, that state does not initially display. Initially, the state update suspends in memory (since we used useTransition), and the prior UI continues to show.

Then the fallback renders. The fallback renders a disabled version of that same search bar, which does show the current search state (by choice). We’ve now removed our prior UI (since by now it’s quite old, and stale) and are waiting on the search shown in the disabled menu bar.

This is the sort of consistency Suspense gives you, for free.

You can spend your time crafting nice application states, and React does the leg work of surmising whether things are ready, without you needing to juggle promises.

Nested Suspense boundaries

Let’s suppose our top-level navigation takes a while to load our books component to the extent that our “Still loading, sorry” spinner from the Suspense boundary renders. From there, the books component loads and the new Suspense boundary inside the books component renders. But, then, as rendering continues, our book search query fires, and suspends. What will happen? Will the top-level Suspense boundary continue to show, until everything is ready, or will the lower-down Suspense boundary in books take over?

The answer is the latter. As new Suspense boundaries render lower in the tree, their fallback will replace the fallback of whatever antecedent Suspense fallback was already showing. There’s currently an unstable API to override this, but if you’re doing a good job of crafting your fallbacks, this is probably the behavior you want. You don’t want “Still loading, sorry” to just keep showing. Rather, as soon as the books component is ready, you absolutely want to display that shell with the more targeted waiting message.

Now, what if our books module loads and starts to render while the startTransition spinner is still showing and then suspends? In other words, imagine that our startTransition has a timeout of three seconds, the books component renders, the nested Suspense boundary is in the component tree after one second, and the search query suspends. Will the remaining two seconds elapse before that new nested Suspense boundary renders the fallback, or will the fallback show immediately? The answer, perhaps surprisingly, is that the new Suspense fallback will show immediately by default. That’s because it’s best to show a new, valid UI as quickly as possible, so the user can see that things are happening, and progressing. 

How data fits in

Navigation is fine, but how does data loading fit into all of this?

It fits in completely and transparently. Data loading triggers suspensions just like navigation with React.lazy, and it hooks into all the same useTransition and Suspense boundaries. This is what’s so amazing about Suspense: all your async dependencies seamlessly work in this same system. Managing these various async requests manually to ensure consistency was a nightmare before Suspense, which is precisely why nobody did it. Web apps were notorious for cascading spinners that stopped at unpredictable times, producing inconsistent UIs that were only partially finished.

OK, but how do we actually tie data loading into this? Data loading in Suspense is paradoxically both more complex, and also simple.

I’ll explain.

If you’re waiting on data, you’ll throw a promise in the component that reads (or attempts to read) the data. The promise should be consistent based on the data request. So, four repeated requests for that same “C++” search query should throw the same, identical promise. This implies some sort of caching layer to manage all this. You’ll likely not write this yourself. Instead, you’ll just hope, and wait for the data library you use to update itself to support Suspense.

This is already done in my micro-graphql-react library. Instead of using the useQuery hook, you’ll use the useSuspenseQuery hook, which has an identical API, but throws a consistent promise when you’re waiting on data.

Wait, what about preloading?!

Has your brain turned to mush reading other things on Suspense that talked about waterfalls, fetch-on-render, preloading, etc? Don’t worry about it. Here’s what it all means.

Let’s say you lazy load the books component, which renders and then requests some data, which causes a new Suspense. The network request for the component and the network request for the data will happen one after the other—in a waterfall fashion.

But here’s the key part: the application state that led to whatever initial query that ran when the component loaded was already available when you started loading the component (which, in this case, is the URL). So why not “start” the query as soon as you know you’ll need it? As soon as you browse to /books, why not fire off the current search query right then and there, so it’s already in flight when the component loads.

The micro-graphql-react module does indeed have a preload method, and I urge you to use it. Preloading data is a nice performance optimization, but it has nothing to do with Suspense. Classic React apps could (and should) preload data as soon as they know they’ll need it. Vue apps should preload data as soon as they know they’ll need it. Svelte apps should… you get the point.

Preloading data is orthogonal to Suspense, which is something you can do with literally any framework. It’s also something we all should have been doing already, even though nobody else was.

But seriously, how do you preload?

That’s up to you. At the very least, the logic to run the current search absolutely needs to be completely separated into its own, standalone module. You should literally make sure this preload function is in a file by itself. Don’t rely on webpack to treeshake; you’ll likely face abject sadness the next time you audit your bundles.

You have a preload() method in its own bundle, so call it. Call it when you know you’re about to navigate to that module. I assume React Router has some sort of API to run code on a navigation change. For the vanilla routing code above, I call the method in that routing switch from before. I had omitted it for brevity, but the books entry actually looks like this:

switch (moduleToLoad.toLowerCase()) {   case "activate":     return ActivateComponent;   case "authenticate":     return AuthenticateComponent;   case "books":     // preload!!!     booksPreload();     return BooksComponent;

That’s it. Here’s a live demo to play around with:

To modify the Suspense timeout value, which defaults to 3000ms, navigate to Settings, and check out the misc tab. Just be sure to refresh the page after modifying it.

Wrapping up

I’ve seldom been as excited for anything in the web dev ecosystem as I am for Suspense. It’s an incredibly ambitious system for managing one of the trickiest problems in web development: asynchrony.

The post React Suspense in Practice appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

What React Does (and Doesn’t Do)

With a name as big as React, it’s bound to cause some Stream-Crossing Confusion, as I like to call it. How do you center a <div> in React? Dave Ceddia:

React cares exactly zero about styling. Think of it as generating the barebones HTML. React will put elements on the page, but everything after that is the job of CSS: how they appear, what they look like, how they’re positioned, and how centered or uncentered they are.

“How to center a div in React” is… not a React problem. It’s a CSS problem. You don’t need “react” in your Google query. Once you figure it out, use React to apply the right CSS class name to your components

How do you center a <div> in WordPress? How do you center a <div> in Vue? On one hand, they are broken questions. Those technologies don’t have anything to do with centering things. Centering on the web is something CSS does. On the other hand, higher-level tech is sometimes involved. Maybe there is some Gutenberg thing in WordPress that handles centering on this particular site that you should use for editorial consistency. Maybe there is some styling library being used which means you can’t write regular CSS — you have to write the way the library wants you to write it. Complicated, all this “making websites” stuff.

Direct Link to ArticlePermalink

The post What React Does (and Doesn’t Do) appeared first on CSS-Tricks.

CSS-Tricks

,
[Top]