Tag: React

React Component Tests for Humans

React component tests should be interesting, straightforward, and easy for a human to build and maintain.

Yet, the current state of the testing library ecosystem is not sufficient to motivate developers to write consistent JavaScript tests for React components. Testing React components—and the DOM in general—often require some kind of higher-level wrapper around popular testing frameworks like Jest or Mocha.

Here’s the problem

Writing component tests with the tools available today is boring, and even when you get to writing them, it takes lots of hassle. Expressing test logic following a jQuery-like style (chaining) is confusing. It doesn’t jive with how React components are usually built.

The Enzyme code below is readable, but a bit too bulky because it uses too many words to express something that is ultimately simple markup.

expect(screen.find(".view").hasClass("technologies")).to.equal(true); expect(screen.find("h3").text()).toEqual("Technologies:"); expect(screen.find("ul").children()).to.have.lengthOf(4); expect(screen.contains([   <li>JavaScript</li>,   <li>ReactJs</li>,   <li>NodeJs</li>,   <li>Webpack</li> ])).to.equal(true); expect(screen.find("button").text()).toEqual("Back"); expect(screen.find("button").hasClass("small")).to.equal(true);

The DOM representation is just this:

<div className="view technologies">   <h3>Technologies:</h3>   <ul>     <li>JavaScript</li>     <li>ReactJs</li>     <li>NodeJs</li>     <li>Webpack</li>   </ul>   <button className="small">Back</button> </div>

What if you need to test heavier components? While the syntax is still bearable, it doesn’t help your brain grasp the structure and logic. Reading and writing several tests like this is bound to wear you out—it certainly wears me out. That’s because React components follow certain principles to generate HTML code at the end. Tests that express the same principles, on the other hand, are not straightforward. Simply using JavaScript chaining won’t help in the long run.

There are two main issues with testing in React:

  • How to even approach writing tests specifically for components
  • How to avoid all the unnecessary noise

Let’s further expand those before jumping into the real examples.

Approaching React component tests

A simple React component may look like this:

function Welcome(props) {   return <h1>Hello, {props.name}</h1>; }

This is a function that accepts a props object and returns a DOM node using the JSX syntax.

Since a component can be represented by a function, it is all about testing functions. We need to account for arguments and how they influence the returned result. Applying that logic to React components, the focus in the tests should be on setting up props and testing for the DOM rendered in the UI. Since user actions like mouseover, click, typing, etc. may also lead to UI changes, you will need to find a way to programmatically trigger those too.

Hiding the unnecessary noise in tests

Tests require a certain level of readability achieved by both slimming the wording down and following a certain pattern to describe each scenario.

Component tests flow through three phases:

  1. Preparation (setup): The component props are prepared.
  2. Render (action): The component needs to render its DOM to the UI before either triggering any actions on it or testing for certain texts and attributes. That’s when actions can be programmatically triggered.
  3. Validation (verify): The expectations are set, verifying certain side effects over the component markup.

Here is an example:

it("should click a large button", () => {   // 1️⃣ Preparation   // Prepare component props   props.size = "large";    // 2️⃣ Render   // Render the Button's DOM and click on it   const component = mount(<Button {...props}>Send</Button>);   simulate(component, { type: "click" });    // 3️⃣ Validation   // Verify a .clicked class is added    expect(component, "to have class", "clicked"); });

For simpler tests, the phases can merge:

it("should render with a custom text", () => {   // Mixing up all three phases into a single expect() call   expect(     // 1️⃣ Preparation     <Button>Send</Button>,      // 2️⃣ Render     "when mounted",     // 3️⃣ Validation     "to have text",      "Send"   ); });

Writing component tests today

Those two examples above look logical but are anything but trivial. Most of the testing tools do not provide such a level of abstraction, so we have to handle it ourselves. Perhaps the code below looks more familiar.

it("should display the technologies view", () => {   const container = document.createElement("div");   document.body.appendChild(container);      act(() => {     ReactDOM.render(<ProfileCard {...props} />, container);   });      const button = container.querySelector("button");      act(() => {     button.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));   });      const details = container.querySelector(".details");      expect(details.classList.contains("technologies")).toBe(true);   expect(details.querySelector("h3").textContent, "to be", "Technologies");   expect(details.querySelector("button").textContent, "to be", "View Bio"); });

Compare that with the same test, only with an added layer of abstraction:

it("should display the technologies view", () => {   const component = mount(<ProfileCard {...props} />);    simulate(component, {     type: "click",     target: "button",   });    expect(     component,     "queried for first",     ".details",     "to exhaustively satisfy",     <div className="details technologies">       <h3>Technologies</h3>       <div>         <button>View Bio</button>       </div>     </div>   ); });

It does look much better. Less code, obvious flow, and more DOM instead of JavaScript. This is not a fiction test, but something you can achieve with UnexpectedJS today.

The following section is a deep dive into testing React components without getting too deep into UnexpectedJS. Its documentation more than does the job. Instead, we’ll focus on usage, examples, and possibilities.

Writing React Tests with UnexpectedJS

UnexpectedJS is an extensible assertion toolkit compatible with all test frameworks. It can be extended with plugins, and some of those plugins are used in the test project below. Probably the best thing about this library is the handy syntax it provides to describe component test cases in React.

The example: A Profile Card component

The subject of the tests is a Profile card component.

A card component where the persons name, photo, and number of posts are displayed to the left in a single column with a light red background, and the bio is displayed on the right in paragraph form with a title against a white background.

And here is the full component code of ProfileCard.js:

// ProfileCard.js export default function ProfileCard({   data: {     name,     posts,     isOnline = false,     bio = "",     location = "",     technologies = [],     creationDate,     onViewChange,   }, }) {   const [isBioVisible, setIsBioVisible] = useState(true);    const handleBioVisibility = () => {     setIsBioVisible(!isBioVisible);     if (typeof onViewChange === "function") {       onViewChange(!isBioVisible);     }   };    return (     <div className="ProfileCard">       <div className="avatar">         <h2>{name}</h2>         <i className="photo" />         <span>{posts} posts</span>         <i className={`status $ {isOnline ? "online" : "offline"}`} />       </div>       <div className={`details $ {isBioVisible ? "bio" : "technologies"}`}>         {isBioVisible ? (           <>             <h3>Bio</h3>             <p>{bio !== "" ? bio : "No bio provided yet"}</p>             <div>               <button onClick={handleBioVisibility}>View Skills</button>               <p className="joined">Joined: {creationDate}</p>             </div>           </>         ) : (           <>             <h3>Technologies</h3>             {technologies.length > 0 && (               <ul>                 {technologies.map((item, index) => (                   <li key={index}>{item}</li>                 ))}               </ul>             )}             <div>               <button onClick={handleBioVisibility}>View Bio</button>               {!!location && <p className="location">Location: {location}</p>}             </div>           </>         )}       </div>     </div>   ); }

We will work with the component’s desktop version. You can read more about device-driven code split in React but note that testing mobile components is still pretty straightforward.

Setting up the example project

Not all tests are covered in this article, but we will certainly look at the most interesting ones. If you want to follow along, view this component in the browser, or check all its tests, go ahead and clone the GitHub repo.

## 1. Clone the project: git clone git@github.com:moubi/profile-card.git  ## 2. Navigate to the project folder: cd profile-card  ## 3. Install the dependencies: yarn  ## 4. Start and view the component in the browser: yarn start  ## 5. Run the tests: yarn test

Here’s how the <ProfileCard /> component and UnexpectedJS tests are structured once the project has spun up:

/src   └── /components       ├── /ProfileCard       |   ├── ProfileCard.js       |   ├── ProfileCard.scss       |   └── ProfileCard.test.js       └── /test-utils            └── unexpected-react.js

Component tests

Let’s take a look at some of the component tests. These are located in src/components/ProfileCard/ProfileCard.test.js. Note how each test is organized by the three phases we covered earlier.

  1. Setting up required component props for each test.
beforeEach(() => {   props = {     data: {       name: "Justin Case",       posts: 45,       creationDate: "01.01.2021",     },   }; });

Before each test, a props object with the required <ProfileCard /> props is composed, where props.data contains the minimum info for the component to render.

  1. Render with a default set of props.

This test checks the whole DOM produced by the component when passing name, posts, and creationDate fields.

Here’s what the result produces in the UI:

And here’s the test case for it:

it("should render default", () => {   // "to exhaustively satisfy" ensures all classes/attributes are also matching   expect(     <ProfileCard {...props} />,     "when mounted",     "to exhaustively satisfy",     <div className="ProfileCard">       <div className="avatar">         <h2>Justin Case</h2>         <i className="photo" />         <span>45{" posts"}</span>         <i className="status offline" />       </div>       <div className="details bio">         <h3>Bio</h3>         <p>No bio provided yet</p>         <div>           <button>View Skills</button>           <p className="joined">{"Joined: "}01.01.2021</p>         </div>       </div>     </div>   ); });
  1. Render with status online.

Now we check if the profile renders with the “online” status icon.

And the test case for that:

it("should display online icon", () => {   // Set the isOnline prop   props.data.isOnline = true;    // The minimum to test for is the presence of the .online class   expect(     <ProfileCard {...props} />,     "when mounted",     "queried for first",     ".status",     "to have class",     "online"   ); });
  1. Render with bio text.

<ProfileCard /> accepts any arbitrary string for its bio.

So, let’s write a test case for that:

it("should display online icon", () => {   // Set the isOnline prop   props.data.isOnline = true;    // The minimum to test for is the presence of the .online class   expect(     <ProfileCard {...props} />,     "when mounted",     "queried for first",     ".status",     "to have class",     "online"   ); });
  1. Render “Technologies” view with an empty list.

Clicking on the “View Skills” link should switch to a list of technologies for this user. If no data is passed, then the list should be empty.

Here’s that test case:

it("should display the technologies view", () => {   // Mount <ProfileCard /> and obtain a ref   const component = mount(<ProfileCard {...props} />);    // Simulate a click on the button element ("View Skills" link)   simulate(component, {     type: "click",     target: "button",   });    // Check if the .details element contains the technologies view   expect(     component,     "queried for first",     ".details",     "to exhaustively satisfy",     <div className="details technologies">       <h3>Technologies</h3>       <div>         <button>View Bio</button>       </div>     </div>   ); });
  1. Render a list of technologies.

If a list of technologies is passed, it will display in the UI when clicking on the “View Skills” link.

Yep, another test case:

it("should display list of technologies", () => {   // Set the list of technologies   props.data.technologies = ["JavaScript", "React", "NodeJs"];     // Mount ProfileCard and obtain a ref   const component = mount(<ProfileCard {...props} />);    // Simulate a click on the button element ("View Skills" link)   simulate(component, {     type: "click",     target: "button",   });    // Check if the list of technologies is present and matches prop values   expect(     component,     "queried for first",     ".technologies ul",     "to exhaustively satisfy",     <ul>       <li>JavaScript</li>       <li>React</li>       <li>NodeJs</li>     </ul>   ); });
  1. Render a user location.

That information should render in the DOM only if it was provided as a prop.

The test case:

it("should display location", () => {   // Set the location    props.data.location = "Copenhagen, Denmark";    // Mount <ProfileCard /> and obtain a ref   const component = mount(<ProfileCard {...props} />);      // Simulate a click on the button element ("View Skills" link)   // Location render only as part of the Technologies view   simulate(component, {     type: "click",     target: "button",   });    // Check if the location string matches the prop value   expect(     component,     "queried for first",     ".location",     "to have text",     "Location: Copenhagen, Denmark"   ); });
  1. Calling a callback when switching views.

This test does not compare DOM nodes but does check if a function prop passed to <ProfileCard /> is executed with the correct argument when switching between the Bio and Technologies views.

it("should call onViewChange prop", () => {   // Create a function stub (dummy)   props.data.onViewChange = sinon.stub();      // Mount ProfileCard and obtain a ref   const component = mount(<ProfileCard {...props} />);    // Simulate a click on the button element ("View Skills" link)   simulate(component, {     type: "click",     target: "button",   });    // Check if the stub function prop is called with false value for isBioVisible   // isBioVisible is part of the component's local state   expect(     props.data.onViewChange,     "to have a call exhaustively satisfying",     [false]   ); });

Running all the tests

Now, all of the tests for <ProfileCard /> can be executed with a simple command:

yarn test

Notice that tests are grouped. There are two independent tests and two groups of tests for each of the <ProfileCard /> views—bio and technologies. Grouping makes test suites easier to follow and is a nice way to organize logically-related UI units.

Some final words

Again, this is meant to be a fairly simple example of how to approach React component tests. The essence is to look at components as simple functions that accept props and return a DOM. From that point on, choosing a testing library should be based on the usefulness of the tools it provides for handling component renders and DOM comparisons. UnexpectedJS happens to be very good at that in my experience.

What should be your next steps? Look at the GitHub project and give it a try if you haven’t already! Check all the tests in ProfileCard.test.js and perhaps try to write a few of your own. You can also look at src/test-utils/unexpected-react.js which is a simple helper function exporting features from the third-party testing libraries.

And lastly, here are a few additional resources I’d suggest checking out to dig even deeper into React component testing:


The post React Component Tests for Humans appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,

Use CSS Variables instead of React Context

Turns out you can use several different libraries to pass color information around components. Or, you could use custom properties, built right into CSS, have no decline in your own developer experience, and deliver a faster experience to your users. Kent proves it here, with demos.

For the record, you could go a step further than Kent has here and not use CSS-in-JS at all, but still be updating CSS custom properties from button clicks in React and managing the state there and such. I’m telling ya, one of the main jobs of a UI component library like React is managing state, and CSS might as well know about that state so you can use it to do any styling you need to do.

Wait, not use CSS-in-JS? Kent:

I’ve never been so productive working with CSS than when I added a real programming language to it.

Extreme side eye, Kent.

We should be calling it CSS-in-React, also, since React is the only major framework that doesn’t have a blessed solution for styling.

Direct Link to ArticlePermalink


The post Use CSS Variables instead of React Context appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

Some React Blog Posts I’ve Bookmarked and Read Lately

  • The React Hooks Announcement In Retrospect: 2 Years Later — Ryan Carniato considers hooks to be the most significant turning point in front end in the past five years, but he also says hooks have muddied the waters as well.
  • Mediator Component in React — Robin Wieruch’s article made me think just how un-opinionated React is and how architecturally on-your-own you are. It’s tough to find the right abstractions.
  • No One Ever Got Fired for Choosing React — Jake Lazaroff’s article is a good balance to the above. Sometimes you pick a library like React because it solves problems you’re likely to run into and lets you get to work.
  • A React “if component — I kinda like how JSX does conditional rendering, but hey, a lightweight abstraction like this might just float your boat.
  • State of the React Ecosystem in 2021 — Hooks are big. State management is all over the place, and state machines are growing in popularity. webpack is still the main bundler. Everyone is holding their breath about Suspense… so suspenseful.
  • Blitz.js — “The Fullstack React Framework” — interesting to see a framework built on top of another framework (Next.js).
  • Introducing Zero-Bundle-Size React Server Components — Feels like it will be a big deal, but will shine brightest when frameworks and hosts zero-in on offerings around it. I’m sure Vercel and Netlify are all  👀.

The post Some React Blog Posts I’ve Bookmarked and Read Lately appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , , ,
[Top]

3 Approaches to Integrate React with Custom Elements

In my role as a web developer who sits at the intersection of design and code, I am drawn to Web Components because of their portability. It makes sense: custom elements are fully-functional HTML elements that work in all modern browsers, and the shadow DOM encapsulates the right styles with a decent surface area for customization. It’s a really nice fit, especially for larger organizations looking to create consistent user experiences across multiple frameworks, like Angular, Svelte and Vue.

In my experience, however, there is an outlier where many developers believe that custom elements don’t work, specifically those who work with React, which is, arguably, the most popular front-end library out there right now. And it’s true, React does have some definite opportunities for increased compatibility with the web components specifications; however, the idea that React cannot integrate deeply with Web Components is a myth.

In this article, I am going to walk through how to integrate a React application with Web Components to create a (nearly) seamless developer experience. We will look at React best practices its and limitations, then create generic wrappers and custom JSX pragmas in order to more tightly couple our custom elements and today’s most popular framework.

Coloring in the lines

If React is a coloring book — forgive the metaphor, I have two small children who love to color — there are definitely ways to stay within the lines to work with custom elements. To start, we’ll write a very simple custom element that attaches a text input to the shadow DOM and emits an event when the value changes. For the sake of simplicity, we’ll be using LitElement as a base, but you can certainly write your own custom element from scratch if you’d like.

Our super-cool-input element is basically a wrapper with some styles for a plain ol’ <input> element that emits a custom event. It has a reportValue method for letting users know the current value in the most obnoxious way possible. While this element might not be the most useful, the techniques we will illustrate while plugging it into React will be helpful for working with other custom elements.

Approach 1: Use ref

According to React’s documentation for Web Components, “[t]o access the imperative APIs of a Web Component, you will need to use a ref to interact with the DOM node directly.”

This is necessary because React currently doesn’t have a way to listen to native DOM events (preferring, instead, to use it’s own proprietary SyntheticEvent system), nor does it have a way to declaratively access the current DOM element without using a ref.

We will make use of React’s useRef hook to create a reference to the native DOM element we have defined. We will also use React’s useEffect and useState hooks to gain access to the input’s value and render it to our app. We will also use the ref to call our super-cool-input’s reportValue method if the value is ever a variant of the word “rad.”

One thing to take note of in the example above is our React component’s useEffect block.

useEffect(() => {   coolInput.current.addEventListener('custom-input', eventListener);      return () => {     coolInput.current.removeEventListener('custom-input', eventListener);   } });

The useEffect block creates a side effect (adding an event listener not managed by React), so we have to be careful to remove the event listener when the component needs a change so that we don’t have any unintentional memory leaks.

While the above example simply binds an event listener, this is also a technique that can be employed to bind to DOM properties (defined as entries on the DOM object, rather than React props or DOM attributes).

This isn’t too bad. We have our custom element working in React, and we’re able to bind to our custom event, access the value from it, and call our custom element’s methods as well. While this does work, it is verbose and doesn’t really look like React.

Approach 2: Use a wrapper

Our next attempt at using our custom element in our React application is to create a wrapper for the element. Our wrapper is simply a React component that passes down props to our element and creates an API for interfacing with the parts of our element that aren’t typically available in React.

Here, we have moved the complexity into a wrapper component for our custom element. The new CoolInput React component manages creating a ref while adding and removing event listeners for us so that any consuming component can pass props in like any other React component.

function CoolInput(props) {   const ref = useRef();   const { children, onCustomInput, ...rest } = props;      function invokeCallback(event) {     if (onCustomInput) {       onCustomInput(event, ref.current);     }   }      useEffect(() => {     const { current } = ref;     current.addEventListener('custom-input', invokeCallback);     return () => {       current.removeEventListener('custom-input', invokeCallback);     }   });      return <super-cool-input ref={ref} {...rest}>{children}</super-cool-input>; }

On this component, we have created a prop, onCustomInput, that, when present, triggers an event callback from the parent component. Unlike a normal event callback, we chose to add a second argument that passes along the current value of the CoolInput’s internal ref.

Using these same techniques, it is possible to create a generic wrapper for a custom element, such as this reactifyLitElement component from Mathieu Puech. This particular component takes on defining the React component and managing the entire lifecycle.

Approach 3: Use a JSX pragma

One other option is to use a JSX pragma, which is sort of like hijacking React’s JSX parser and adding our own features to the language. In the example below, we import the package jsx-native-events from Skypack. This pragma adds an additional prop type to React elements, and any prop that is prefixed with onEvent adds an event listener to the host.

To invoke a pragma, we need to import it into the file we are using and call it using the /** @jsx <PRAGMA_NAME> */ comment at the top of the file. Your JSX compiler will generally know what to do with this comment (and Babel can be configured to make this global). You might have seen this in libraries like Emotion.

An <input> element with the onEventInput={callback} prop will run the callback function whenever an event with the name 'input' is dispatched. Let’s see how that looks for our super-cool-input.

The code for the pragma is available on GitHub. If you want to bind to native properties instead of React props, you can use react-bind-properties. Let’s take a quick look at that:

import React from 'react'  /**  * Convert a string from camelCase to kebab-case  * @param {string} string - The base string (ostensibly camelCase)  * @return {string} - A kebab-case string  */ const toKebabCase = string => string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$  1-$  2').toLowerCase()  /** @type {Symbol} - Used to save reference to active listeners */ const listeners = Symbol('jsx-native-events/event-listeners')  const eventPattern = /^onEvent/  export default function jsx (type, props, ...children) {   // Make a copy of the props object   const newProps = { ...props }   if (typeof type === 'string') {     newProps.ref = (element) => {       // Merge existing ref prop       if (props && props.ref) {         if (typeof props.ref === 'function') {           props.ref(element)         } else if (typeof props.ref === 'object') {           props.ref.current = element         }       }        if (element) {         if (props) {           const keys = Object.keys(props)           /** Get all keys that have the `onEvent` prefix */           keys             .filter(key => key.match(eventPattern))             .map(key => ({               key,               eventName: toKebabCase(                 key.replace('onEvent', '')               ).replace('-', '')             })           )           .map(({ eventName, key }) => {             /** Add the listeners Map if not present */             if (!element[listeners]) {               element[listeners] = new Map()             }              /** If the listener hasn't be attached, attach it */             if (!element[listeners].has(eventName)) {               element.addEventListener(eventName, props[key])               /** Save a reference to avoid listening to the same value twice */               element[listeners].set(eventName, props[key])             }           })         }       }     }   }      return React.createElement.apply(null, [type, newProps, ...children]) }

Essentially, this code converts any existing props with the onEvent prefix and transforms them to an event name, taking the value passed to that prop (ostensibly a function with the signature (e: Event) => void) and adding it as an event listener on the element instance.

Looking forward

As of the time of this writing, React recently released version 17. The React team had initially planned to release improvements for compatibility with custom elements; unfortunately, those plans seem to have been pushed back to version 18.

Until then it will take a little extra work to use all the features custom elements offer with React. Hopefully, the React team will continue to improve support to bridge the gap between React and the web platform.


The post 3 Approaches to Integrate React with Custom Elements appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Theming and Theme Switching with React and styled-components

I recently had a project with a requirement to support theming on the website. It was a bit of a strange requirement, as the application is mostly used by a handful of administrators. An even bigger surprise was that they wanted not only to choose between pre-created themes, but build their own themes. I guess the people want what they want!

Let’s distill that into a complete list of more detailed requirements, then get it done!

  • Define a theme (i.e. background color, font color, buttons, links, etc.)
  • Create and save multiple themes
  • Select and apply a theme
  • Switch themes
  • Customize a theme

We delivered exactly that to our client, and the last I heard, they were using it happily!

Let’s get into building exactly that. We’re going to use React and styled-components. All the source code used in the article can be found in the GitHub Repository.

The setup

Let’s set up a project with React and styled-components. To do that, we will be using the create-react-app. It gives us the environment we need to develop and test React applications quickly.

Open a command prompt and use this command to create the project:

npx create-react-app theme-builder

The last argument, theme-builder, is just the name of the project (and thus, the folder name). You can use anything you like.

It may take a while. When done, navigate it to it in the command line with cd theme-builder. Open the file src/App.js file and replace the content with the following:

import React from 'react';  function App() {   return (     <h1>Theme Builder</h1>   ); }  export default App;

This is a basic React component that we will modify soon. Run the following command from the project root folder to start the app:

# Or, npm run start yarn start

You can now access the app using the URL http://localhost:3000.

A simple heading 1 that says Theme Builder in black on a white background.

create-react-app comes with the test file for the App component. As we will not be writing any tests for the components as part of this article, you can choose to delete that file.

We have to install a few dependencies for our app. So let’s install those while we’re at it:

# Or, npm i ... yarn add styled-components webfontloader lodash

Here’s what we get:

  • styled-components: A flexible way to style React components with CSS. It provides out-of-the-box theming support using a wrapper component called, <ThemeProvider>. This component is responsible for providing the theme to all other React components that are wrapped within it. We will see this in action in a minute.
  • Web Font Loader: The Web Font Loader helps load fonts from various sources, like Google Fonts, Adobe Fonts, etc. We will use this library to load fonts when a theme is applied.
  • lodash: This is a JavaScript utility library for some handy little extras.

Define a theme

This is the first of our requirements. A theme should have a certain structure to define appearance, including colors, fonts, etc. For our application, we will define each theme with these properties:

  • unique identifier
  • theme name
  • color definitions
  • fonts
Screenshot of a code editor showing the organized structure of properties for a sea wave theme.
A theme is a structured group of properties that we’ll use in the application.

You may have more properties and/or different ways to structure it, but these are the things we’re going to use for our example.

Create and save multiple themes

So, we just saw how to define a theme. Now let’s create multiple themes by adding a folder in the project at src/theme and a file in it called, schema.json. Here’s what we can drop in that file to establish “light” and “sea wave” themes:

{   "data" : {     "light" : {       "id": "T_001",       "name": "Light",       "colors": {         "body": "#FFFFFF",         "text": "#000000",         "button": {           "text": "#FFFFFF",           "background": "#000000"         },         "link": {           "text": "teal",           "opacity": 1         }       },       "font": "Tinos"     },     "seaWave" : {       "id": "T_007",       "name": "Sea Wave",       "colors": {         "body": "#9be7ff",         "text": "#0d47a1",         "button": {           "text": "#ffffff",           "background": "#0d47a1"         },         "link": {           "text": "#0d47a1",           "opacity": 0.8         }       },       "font": "Ubuntu"     }   } }

The content of the schema.json file can be saved to a database so we can persist all the themes along with the theme selection. For now, we will simply store it in the browser’s localStorage. To do that, we’ll create another folder at src/utils with a new file in it called, storage.js. We only need a few lines of code in there to set up localStorage:

export const setToLS = (key, value) => {   window.localStorage.setItem(key, JSON.stringify(value)); }  export const getFromLS = key => {   const value = window.localStorage.getItem(key);    if (value) {     return JSON.parse(value);   } }

These are simple utility functions to store data to the browser’s localStorage and to retrieve from there. Now we will load the themes into the browser’s localStorage when the app comes up for the first time. To do that, open the index.js file and replace the content with the following,

import React from 'react'; import ReactDOM from 'react-dom'; import App from './App';  import * as themes from './theme/schema.json'; import { setToLS } from './utils/storage';  const Index = () => {   setToLS('all-themes', themes.default);   return(     <App />   ) }  ReactDOM.render(   <Index />   document.getElementById('root') );

Here, we are getting the theme information from the schema.json file and adding it to the localStorage using the key all-themes. If you have stopped the app running, please start it again and access the UI. You can use DevTools in the browser to see the themes are loaded into localStorage.

The theme with DevTools open and showing the theme properties in the console.
All of the theme props are properly stored in the browser’s localStorage, as seen in DevTools, under Application → Local Storage.

Select and apply a theme

We can now use the theme structure and supply the theme object to the <ThemeProvider> wrapper.

First, we will create a custom React hook. This will manage the selected theme, knowing if a theme is loaded correctly or has any issues. Let’s start with a new useTheme.js file inside the src/theme folder with this in it:

import { useEffect, useState } from 'react'; import { setToLS, getFromLS } from '../utils/storage'; import _ from 'lodash';  export const useTheme = () => {   const themes = getFromLS('all-themes');   const [theme, setTheme] = useState(themes.data.light);   const [themeLoaded, setThemeLoaded] = useState(false);    const setMode = mode => {     setToLS('theme', mode)     setTheme(mode);   };    const getFonts = () => {     const allFonts = _.values(_.mapValues(themes.data, 'font'));     return allFonts;   }    useEffect(() =>{     const localTheme = getFromLS('theme');     localTheme ? setTheme(localTheme) : setTheme(themes.data.light);     setThemeLoaded(true);   }, []);    return { theme, themeLoaded, setMode, getFonts }; };

This custom React hook returns the selected theme from localStorage and a boolean to indicate if the theme is loaded correctly from storage. It also exposes a function, setMode, to apply a theme programmatically. We will come back to that in a bit. With this, we also get a list of fonts that we can load later using a web font loader.

It would be a good idea to use global styles to control things, like the site’s background color, font, button, etc. styled-components provides a component called, createGlobalStyle that establishes theme-aware global components. Let’s set those up in a file called, GlobalStyles.js in the src/theme folder with the following code:

import { createGlobalStyle} from "styled-components";  export const GlobalStyles = createGlobalStyle`   body {     background: $ {({ theme }) => theme.colors.body};     color: $ {({ theme }) => theme.colors.text};     font-family: $ {({ theme }) => theme.font};     transition: all 0.50s linear;   }    a {     color: $ {({ theme }) => theme.colors.link.text};     cursor: pointer;   }    button {     border: 0;     display: inline-block;     padding: 12px 24px;     font-size: 14px;     border-radius: 4px;     margin-top: 5px;     cursor: pointer;     background-color: #1064EA;     color: #FFFFFF;     font-family: $ {({ theme }) => theme.font};   }    button.btn {     background-color: $ {({ theme }) => theme.colors.button.background};     color: $ {({ theme }) => theme.colors.button.text};   } `;

Just some CSS for the <body>, links and buttons, right? We can use these in the App.js file to see the theme in action by replace the content in it with this:

// 1: Import import React, { useState, useEffect } from 'react'; import styled, { ThemeProvider } from "styled-components"; import WebFont from 'webfontloader'; import { GlobalStyles } from './theme/GlobalStyles'; import {useTheme} from './theme/useTheme';  // 2: Create a cotainer const Container = styled.div`   margin: 5px auto 5px auto; `;  function App() {   // 3: Get the selected theme, font list, etc.   const {theme, themeLoaded, getFonts} = useTheme();   const [selectedTheme, setSelectedTheme] = useState(theme);    useEffect(() => {     setSelectedTheme(theme);    }, [themeLoaded]);    // 4: Load all the fonts   useEffect(() => {     WebFont.load({       google: {         families: getFonts()       }     });   });    // 5: Render if the theme is loaded.   return (     <>     {       themeLoaded && <ThemeProvider theme={ selectedTheme }>         <GlobalStyles/>         <Container style={{fontFamily: selectedTheme.font}}>           <h1>Theme Builder</h1>           <p>             This is a theming system with a Theme Switcher and Theme Builder.             Do you want to see the source code? <a href="https://github.com/atapas/theme-builder" target="_blank">Click here.</a>           </p>         </Container>       </ThemeProvider>     }     </>   ); }  export default App;

A few things are happening here:

  1. We import the useState and useEffect React hooks which will help us to keep track of any of the state variables and their changes due to any side effects. We import ThemeProvider and styled from styled-components. The WebFont is also imported to load fonts. We also import the custom theme, useTheme, and the global style component, GlobalStyles.
  2. We create a Container component using the CSS styles and styled component.
  3. We declare the state variables and look out for the changes.
  4. We load all the fonts that are required by the app.
  5. We render a bunch of text and a link. But notice that we are wrapping the entire content with the <ThemeProvider> wrapper which takes the selected theme as a prop. We also pass in the <GlobalStyles/> component.

Refresh the app and we should see the default “light” theme enabled.

The theme with a white background and black text.
Hey, look at that clean, stark design!

We should probably see if switching themes works. So, let’s open the useTheme.js file and change this line:

localTheme ? setTheme(localTheme) : setTheme(themes.data.light);

…to:

localTheme ? setTheme(localTheme) : setTheme(themes.data.seaWave);

Refresh the app again and hopefully we see the “sea wave” theme in action.

The same theme in with a blue color scheme with a light blue background and dark blue text and a blue button.
Now we’re riding the waves of this blue-dominant theme.

Switch themes

Great! We are able to correctly apply themes. How about creating a way to switch themes just with the click of a button? Of course we can do that! We can also provide some sort of theme preview as well.

A heading instructs the user to select a theme and two card components are beneath the heading, side-by-side, showing previews of the light theme and the sea wave theme.
A preview of each theme is provided in the list of options.

Let’s call each of these boxes a ThemeCard, and set them up in a way they can take its theme definition as a prop. We’ll go over all the themes, loop through them, and populate each one as a ThemeCard component.

{   themes.length > 0 &&    themes.map(theme =>(     <ThemeCard theme={data[theme]} key={data[theme].id} />   )) }

Now let’s turn to the markup for a ThemeCard. Yours may look different, but notice how we extract its own color and font properties, then apply them:

const ThemeCard = props => {   return(     <Wrapper        style={{backgroundColor: `$ {data[_.camelCase(props.theme.name)].colors.body}`, color: `$ {data[_.camelCase(props.theme.name)].colors.text}`, fontFamily: `$ {data[_.camelCase(props.theme.name)].font}`}}>       <span>Click on the button to set this theme</span>       <ThemedButton         onClick={ (theme) => themeSwitcher(props.theme) }         style={{backgroundColor: `$ {data[_.camelCase(props.theme.name)].colors.button.background}`, color: `$ {data[_.camelCase(props.theme.name)].colors.button.text}`, fontFamily: `$ {data[_.camelCase(props.theme.name)].font}`}}>         {props.theme.name}       </ThemedButton>     </Wrapper>   ) }

Next up, let’s create a file called ThemeSelector.js in our the src folder. Copy the content from here and drop it into the file to establish our theme switcher, which we need to import in App.js:

import ThemeSelector from './ThemeSelector';

Now we can use it inside the Container component:

<Container style={{fontFamily: selectedTheme.font}}>   // same as before   <ThemeSelector setter={ setSelectedTheme } /> </Container>

Let’s refresh the browser now and see how switching themes works.

An animated screenshot showing the theme changing when it is selected from the list of theme card options.

The fun part is, you can add as many as themes in the schema.json file to load them in the UI and switch. Check out this schema.json file for some more themes. Please note, we are also saving the applied theme information in localStorage, so the selection will be retained when you reopen the app next time.

Selected theme stored in the Local Storage.

Customize a theme

Maybe your users like some aspects of one theme and some aspects of another. Why make them choose between them when they can give them the ability to define the theme props themselves! We can create a simple user interface that allows users to select the appearance options they want, and even save their preferences.

Animated screenshot showing a modal opening with a list of theme options to customize the appearance, including the them name, background color, text color, button text color, link color, and font.

We will not cover the theme creation code explanation in details but, it should be easy by following the code in the GitHub Repo. The main source file is CreateThemeContent.js and it is used by App.js. We create the new theme object by gathering the value from each input element change event and add the object to the collection of theme objects. That’s all.

Before we end…

Thank you for reading! I hope you find what we covered here useful for something you’re working on. Theming systems are fun! In fact, CSS custom properties are making that more and more a thing. For example, check out this approach for color from Dieter Raber and this roundup from Chris. There’s also this setup from Michelle Barker that relies on custom properties used with Tailwind CSS. Here’s yet another way from Andrés Galente.

Where all of these are great example for creating themes, I hope this article helps take that concept to the next level by storing properties, easily switching between themes, giving users a way to customize a theme, and saving those preferences.

Let’s connect! You can DM me on Twitter with comments, or feel free to follow.


The post Theming and Theme Switching with React and styled-components appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Optimize Images According to Network and Device Constraints in React

Connectivity has evolved beyond recognition since the beginning of the internet. We are lightyears past dial up, these days, and can watch a video in high resolution on our smartphone while being connected to a mobile network. But not all mobile connections are created equal – older generation networks (3G, 2G, etc.) are still quite dominant, accounting for almost half of all connections worldwide in 2020.

Unfortunately, the phasing out process is very slow, and many people around the globe are experiencing really dragged out page loads, comparable to the very early days of home internet adoption.

Modern websites became resource-hungry, featuring lots of images and animations. For a visitor on an underpowered device and a fragile network connection, an average webpage might take a good minute to load completely. This is largely due to the fact that developers often make binary decisions when it comes to user’s hardware and network conditions: devices fall either in the desktop or smartphone category, while connectivity is a question of being on- or offline. In reality, user’s circumstances tend to be much more nuanced.

We Can Do Better?

What can be done to bridge the gap for users on modest devices and spotty connections? First, we need to do a quick evaluation of what exactly their conditions are by looking at the following two properties:

Based on that, we can decide, for instance, to adjust the quality of the images we intend to serve. There is a catch, however, with Jamstack websites and apps rendered on the server – `navigator`object, as any other browser API, isn’t available during the rendering stage. A common workaround for this issue is to add a bunch of responsive image markup, but it comes with a significant pain point – inefficient scaling. An image CDN like ImageEngine helps to avoid this and other pitfalls associated with responsive images as it handles all the heavy-lifting behind the scenes by applying automated, smart tweaks to requested resources on-the-fly.

When it comes to adapting to a user’s network constraints, one could detect connection type and instruct an image CDN to vary compression according to connection quality. Here’s how one might go about it in React:

import React, { useState, useEffect } from 'react'  const useConnectionType = (defaultConnectionType) => {    const isSupported = navigator?.connection?.effectiveType     ? true     : false    const [connectionType, setNetworkStatus] = useState(     isSupported       ? navigator.connection.effectiveType       : defaultConnectionType   )    useEffect(() => {     if (isSupported) {       const { connection } = navigator       const updateConnectionType = () => {         setNetworkStatus(connection.effectiveType)       }        connection.addEventListener('change', updateConnectionType)        return () => {         connection.removeEventListener('change', updateConnectionType)       }     }   }, [])    return [ connectionType, setNetworkStatus ] }  const imageCDNHost = 'images.foo.com  function ConnectionAwareComponent () {    const [ connectionType ] = useConnectionType()    let compressionLevel = 0    switch (connectionType) {     case 'slow-2g':       compressionLevel = 65       break     case '2g':       compressionLevel = 50       break     case '3g':       compressionLevel = 30       break     case '4g':       compressionLevel = 0       break   }    return (     <div>       {/* Apply variable compression via dedicated directive */}       <img src={`$ {imageCDNHost}/?imgeng?=cmpr_$ {compressionLevel}`} />     </div>   ) }

One can take this idea even further to accommodate those on really sluggish and wonky networks by rendering blurred images and offering an option to download a higher resolution version on demand. Or devise a performance score system and adjust what is sent based on that. 

On the other hand, the fact that the user is on a “speedy” 4G connection doesn’t necessarily mean they aren’t interested in saving data as they might be accessing a website in roaming. Enabling Client Hints on one’s website will let site owners detect the presence of a data saver flag and take necessary steps to adjust to the user’s preferences.

Reasons for Faster Images

Mediocre CPU, modest amounts of memory and a low-grade connection aren’t imaginary constraints. They pose real user experience challenges potentially affecting hundreds of millions of users worldwide. Some companies began to bake inclusive experiences into their products: streaming services like Netflix and Spotify adjust the streaming quality based on your network conditions, while many others are doing automatic image optimizations behind the scenes for users.

Developing regions, where fast networks aren’t yet accessible to everyone and everywhere, might not be one’s target market. Meanwhile, someone browsing from a rural area in a developed country will likely have a jarring experience if they are served a fully-fledged version of a website. We can be more considerate and intentional by adjusting what we send / display to our users with only a couple of small tweaks.

Using an image CDN like ImageEngine simplifies the image optimization process and automatically responds to the Client Hints for network constraints. The result is a better experience for a network-constrained visitor and an elegant workflow for developers.


The post Optimize Images According to Network and Device Constraints in React appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , ,
[Top]

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

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

Huh?

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

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

The easiest design handoff ever

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

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

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

With Anima 4.0, that process is more like this:

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

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

No more “How does this thing work?”

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

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

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

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

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

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

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

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

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

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

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

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

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

Turn anything into a functional React component

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

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

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

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

Why this is a big deal

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

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

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

Anima 4.0 is available… today

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


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

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

CSS-Tricks

, , , , ,
[Top]

Pre-Caching Image with React Suspense

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

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

Quick crash course on Suspense

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

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

Let’s preload some images!

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

OK, let’s go!

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

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

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

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

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

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

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

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

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

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

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

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

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

The implementation

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

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

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

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

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

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

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

Transition state update

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

Here’s what mine looks like:

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

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

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

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

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

Avoiding network waterfalls 

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

{images.map(img => (

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

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

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

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

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

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

Suspend late

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

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

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

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

The demo

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

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

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

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

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

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

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

Odds and ends

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

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

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

const INITIAL_TIME = +new Date();

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

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

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

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

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

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


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

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

CSS-Tricks

, , ,
[Top]

A Complete Walkthrough of GraphQL APIs with React and FaunaDB

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

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

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

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

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

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

Our Application

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

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

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

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

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

Creating the React Application

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

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

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

Deploying to Heroku

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

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

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

heroku open

Provisioning a FaunaDB Database

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

Creating an API via GraphQL Schema in FaunaDB

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

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

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

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

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

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

  • findItemByID
  • createItem
  • updateItem
  • deleteItem

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

query MyItemQuery {  allItems {    data {     name    }  } }

Then click on the play button to run it:

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

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

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

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

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

Provisioning a New Database Key

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

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

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

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

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

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

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

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

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

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

REACT_APP_FAUNA_SECRET=fnADzT7kXcACAFHdiKG-lIUWq-hfWIVxqFi4OtTv

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

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

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

Connecting to FaunaDB in React with Apollo

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

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

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

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

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

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

There are 2 important implications from this snippet of code:

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

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

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

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

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

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

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

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

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

git push heroku master heroku open

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

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

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

heroku config:set REACT_APP_FAUNA_SECRET=fnADzT7kXcACAFHdiKG-lIUWq-hfWIVxqFi4OtTv

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

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

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

Adding New To-Do Items

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

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

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

Deleting New To-Do Items

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

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

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

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

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

Marking Items as Complete

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

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

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

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

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

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

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

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

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

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

git push heroku master heroku open

Conclusion

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

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


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

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

CSS-Tricks

, , , , ,
[Top]

Going Jamstack with React, Serverless, and Airtable

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

Why these technologies, you ask?

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

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

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

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

What we’re building

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

 

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

Airtable setup

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

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

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

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

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

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

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

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

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

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

Locating Airtable credentials

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

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

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

Project setup

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

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

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

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

npm install dotenv

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

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

Now let’s write some code!

Setting up serverless functions

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

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

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

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

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

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

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

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

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

Updating the Airtable configuration file

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

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

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

Getting courses

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

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

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

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

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

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

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

npm install -g netlify-cli

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

This beautiful command does a few things for us:

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

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

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

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

Creating courses

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

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

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

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

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

Then, we can return the createdCourse:

return formattedReturn(200, createdCourse);

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

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

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

Inside of Postman, I need the following configuration.

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

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

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

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

Updating courses

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

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

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

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

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

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

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

Here’s the full file with all that together:

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

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

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

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

We can also see the change in the Airtable GUI.

Deleting courses

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

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

The final file looks like this:

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

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

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

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

Displaying a list of courses in React

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

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

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

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

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

setCourses(courses)

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

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

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

Adding courses in React

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

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

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

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

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

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

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

Updating purchased courses in React

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

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

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

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

Deleting courses in React

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

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

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

Deploying to Netlify

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

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

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

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

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

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

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

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

The site should automatically start building.

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

Welcome to the Jamstack!

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

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


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

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

CSS-Tricks

, , , ,
[Top]