GitXplorerGitXplorer
i

hidden-attribute

public
9 stars
0 forks
2 issues

Commits

List of commits on branch master.
Unverified
0405b79004a53556a25e0c2b649ebad479d51aa9

feat: Switch to cryptocompare API

iicyJoseph committed 2 years ago
Unverified
006fb581e4224aca3395d9ae4a859f5470f9bf59

style: Import ness css first

iicyJoseph committed 3 years ago
Unverified
f8a005e343670291706449d393fde0560aa53948

chore: Use parcels `process` suggestion

iicyJoseph committed 3 years ago
Unverified
d3524511e18b6330e4797db86ebed4c360ec6bcc

chore: Upgrade browserlist

iicyJoseph committed 3 years ago
Unverified
4ec735f12bf63a79ceadc2560abfe2a82a7fdb06

fix: Bitcoin price labels

iicyJoseph committed 3 years ago
Unverified
97113d310461c82285692e80d93294ba3b13e44a

Merge branch 'master' of github.com:icyJoseph/hidden-attribute

iicyJoseph committed 3 years ago

README

The README file for this repository.

Hidden Attribute

Demonstrates how to take advantage of the HTML attribute hidden.

Demo

Demo available here.

About

Article.

This repository exists for educational purposes only.

It uses nes.css as styling library.

It uses Parcel to bundle the application.

Consumes data from CoinDesk and PokeApi.

Explanation

Every time a React component mounts, updates or is unmounted, React executes two phases:

  • A render phase 📖
  • And a commit phase 📝

There is a cause-effect relation between these, however, when Concurrent Mode comes out, you'll be able to opt-in to have many render phases before a commit phase actually kicks in. The actual details of this are not clear, so do not think about them just yet.

During the commit phase React makes changes to the DOM, after this, the Browser's job is to apply CSS styles, layout calculations, paint pixels and compose your elements into layers.

That's a lot of work!

Luckily, there is a DOM attribute called hidden, which you can use to get React to commit changes to the DOM, while also telling the browser not to run its own work, just yet.

It is equivalent to display none, in fact, setting a display property on hidden elements overrides the attribute!

Even though MDN recommends not to use hidden to hide panels, it is a great example to show how to take advantage of this attribute.

In this demo, there are three tabs:

  • The Landing tab, simply displays a message.
  • The Bitcoin tab, loads data from CoinDesk and shows it in a Victory Chart.
  • The Pokémon tab, loads Pokémon from PokeApi and shows their sprites.

Each tab is a separate JavaScript bundle thanks to React's lazy. Since the Bitcoin tab uses Victory Charts, this bundle is bound to be heavy and will most likely load last. The demo also uses Parcel to bundle the whole application with zero configuration.

As soon as the page loads, the three bundles are fetched. Because the demo uses lazy + Suspense, at this point, all three tabs do a fallback. As far as the Tabs handler is concerned, the job is done, React is now handling the asynchronous loading.

After each bundle is loaded, it is parsed, then the actual component is mounted and its own life cycles methods are executed. On mount, the landing page does nothing special, however, in the Pokemon tab, fire type Pokémons are queried, and in the bitcoin tab a request for the BTC/SEK rate during the last month is created.

All of this is done before the user has switched tabs. Most likely, it all happens as they read the landing page message.

To see that the browser is not actually spending resources in loading hidden elements, in the Pokémon tab, elements with an even index order load their Pokémon sprite as CSS background, while the ones with an odd index load the sprite as an img HTML element.

Now one can observe that React commits its work to the DOM, and Pokémon with odd indeces fetch the src attribute in the img element, but Pokémon with even indeces do not, because it is CSS and the hidden attribute tells the browser to not run it's work chain, just yet.

Open the network tab in your browser dev tools to see for yourself, as soon as you switch to the Pokémon tab, the even indeces images are fetched. Notice that the Bitcoin data is also fetched upon page loading.

What if you switch tabs quickly? You'll either see the fallback, or simply see your rendered component.

Implementation

At the root of the React three in the demo we have:

function Tab({ title, children }) {
  return (
    <>
      <h3>{title}</h3>
      <div>{children}</div>
    </>
  );
}

function App() {
  return (
    <Tabs>
      <Tab label={<LandingLabel />} title="Landing">
        <SuspenseLanding />
      </Tab>
      <Tab label={<BitCoinLabel />} title="Bitcoin">
        <SuspenseBitcoin />
      </Tab>
      <Tab label={<PokemonLabel />} title="Pokemon">
        <SuspensePokemon />
      </Tab>
    </Tabs>
  );
}

Where the lazy + Suspense are defined like so:

const LazyLanding = React.lazy(() => import("./containers/Landing"));

function SuspenseLanding({ ...props }) {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <LazyLanding {...props} />
    </React.Suspense>
  );
}

To implement the desired behavior, write the Tab handler as such:

function Tabs({ children }) {
  const [current, setCurrent] = React.useState(0);

  const controls = React.useMemo(
    () =>
      React.Children.map(children, ({ props: { label } }, index) => (
        <button key={index} onClick={() => setCurrent(index)}>
          {label}
        </button>
      )),
    []
  );

  return (
    <>
      <div>{controls}</div>
      {React.Children.toArray(children).map((child, index) => (
        <div key={index} hidden={index !== current}>
          {child}
        </div>
      ))}
    </>
  );
}

This approach, begins the async lazy + Suspense loading of each tab, and then renders and commits each children to the DOM. However, those not currently selected children are hidden in the DOM, only the current is displayed.

Another approach is to:

function Tabs({ children }) {
  const [current, setCurrent] = React.useState(0);

  const controls = React.useMemo(
    () =>
      React.Children.map(children, ({ props: { label } }, index) => (
        <button key={index} onClick={() => setCurrent(index)}>
          {label}
        </button>
      )),
    []
  );

  return (
    <>
      <div>{controls}</div>
      {React.Children.toArray(children).map(
        (child, index) => index === current && <div key={index}>{child}</div>
      )}
    </>
  );
}

Unfortunately, this actually prevents the tabs from being lazy + Suspense loaded, rendered and commited to the DOM by React. With this implementation, switching tabs triggers the fallback on Suspense, and will make users wait until it resolves, renders and commits to the DOM, after which data is fetched and updates the component, triggering render and commit once again.

The code for this demo lives in this repository.

Enjoy!