Giter VIP home page Giter VIP logo

react-adaptive-hooks's Introduction

React Adaptive Loading Hooks & Utilities ยท Build Status npm bundle size

Deliver experiences best suited to a user's device and network constraints (experimental)

This is a suite of React Hooks and utilities for adaptive loading based on a user's:

It can be used to add patterns for adaptive resource loading, data-fetching, code-splitting and capability toggling.

Objective

Make it easier to target low-end devices while progressively adding high-end-only features on top. Using these hooks and utilities can help you give users a great experience best suited to their device and network constraints.

Installation

npm i react-adaptive-hooks --save or yarn add react-adaptive-hooks

Usage

You can import the hooks you wish to use as follows:

import { useNetworkStatus } from 'react-adaptive-hooks/network';
import { useSaveData } from 'react-adaptive-hooks/save-data';
import { useHardwareConcurrency } from 'react-adaptive-hooks/hardware-concurrency';
import { useMemoryStatus } from 'react-adaptive-hooks/memory';
import { useMediaCapabilitiesDecodingInfo } from 'react-adaptive-hooks/media-capabilities';

and then use them in your components. Examples for each hook and utility can be found below:

Network

useNetworkStatus React hook for adapting based on network status (effective connection type)

import React from 'react';

import { useNetworkStatus } from 'react-adaptive-hooks/network';

const MyComponent = () => {
  const { effectiveConnectionType } = useNetworkStatus();

  let media;
  switch(effectiveConnectionType) {
    case 'slow-2g':
      media = <img src='...' alt='low resolution' />;
      break;
    case '2g':
      media = <img src='...' alt='medium resolution' />;
      break;
    case '3g':
      media = <img src='...' alt='high resolution' />;
      break;
    case '4g':
      media = <video muted controls>...</video>;
      break;
    default:
      media = <video muted controls>...</video>;
      break;
  }
  
  return <div>{media}</div>;
};

effectiveConnectionType values can be slow-2g, 2g, 3g, or 4g.

This hook accepts an optional initialEffectiveConnectionType string argument, which can be used to provide a effectiveConnectionType state value when the user's browser does not support the relevant NetworkInformation API. Passing an initial value can also prove useful for server-side rendering, where the developer can pass an ECT Client Hint to detect the effective network connection type.

// Inside of a functional React component
const initialEffectiveConnectionType = '4g';
const { effectiveConnectionType } = useNetworkStatus(initialEffectiveConnectionType);

Save Data

useSaveData utility for adapting based on the user's browser Data Saver preferences.

import React from 'react';

import { useSaveData } from 'react-adaptive-hooks/save-data';

const MyComponent = () => {
  const { saveData } = useSaveData();
  return (
    <div>
      { saveData ? <img src='...' /> : <video muted controls>...</video> }
    </div>
  );
};

saveData values can be true or false.

This hook accepts an optional initialSaveData boolean argument, which can be used to provide a saveData state value when the user's browser does not support the relevant NetworkInformation API. Passing an initial value can also prove useful for server-side rendering, where the developer can pass a server Save-Data Client Hint that has been converted to a boolean to detect the user's data saving preference.

// Inside of a functional React component
const initialSaveData = true;
const { saveData } = useSaveData(initialSaveData);

CPU Cores / Hardware Concurrency

useHardwareConcurrency utility for adapting to the number of logical CPU processor cores on the user's device.

import React from 'react';

import { useHardwareConcurrency } from 'react-adaptive-hooks/hardware-concurrency';

const MyComponent = () => {
  const { numberOfLogicalProcessors } = useHardwareConcurrency();
  return (
    <div>
      { numberOfLogicalProcessors <= 4 ? <img src='...' /> : <video muted controls>...</video> }
    </div>
  );
};

numberOfLogicalProcessors values can be the number of logical processors available to run threads on the user's device.

Memory

useMemoryStatus utility for adapting based on the user's device memory (RAM)

import React from 'react';

import { useMemoryStatus } from 'react-adaptive-hooks/memory';

const MyComponent = () => {
  const { deviceMemory } = useMemoryStatus();
  return (
    <div>
      { deviceMemory < 4 ? <img src='...' /> : <video muted controls>...</video> }
    </div>
  );
};

deviceMemory values can be the approximate amount of device memory in gigabytes.

This hook accepts an optional initialMemoryStatus object argument, which can be used to provide a deviceMemory state value when the user's browser does not support the relevant DeviceMemory API. Passing an initial value can also prove useful for server-side rendering, where the developer can pass a server Device-Memory Client Hint to detect the memory capacity of the user's device.

// Inside of a functional React component
const initialMemoryStatus = { deviceMemory: 4 };
const { deviceMemory } = useMemoryStatus(initialMemoryStatus);

Media Capabilities

useMediaCapabilitiesDecodingInfo utility for adapting based on the user's device media capabilities.

Use case: this hook can be used to check if we can play a certain content type. For example, Safari does not support WebM so we want to fallback to MP4 but if Safari at some point does support WebM it will automatically load WebM videos.

import React from 'react';

import { useMediaCapabilitiesDecodingInfo } from 'react-adaptive-hooks/media-capabilities';

const webmMediaDecodingConfig = {
  type: 'file', // 'record', 'transmission', or 'media-source'
  video: {
    contentType: 'video/webm;codecs=vp8', // valid content type
    width: 800, // width of the video
    height: 600, // height of the video
    bitrate: 10000, // number of bits used to encode 1s of video
    framerate: 30 // number of frames making up that 1s.
  }
};

const initialMediaCapabilitiesInfo = { powerEfficient: true };

const MyComponent = ({ videoSources }) => {
  const { mediaCapabilitiesInfo } = useMediaCapabilitiesDecodingInfo(webmMediaDecodingConfig, initialMediaCapabilitiesInfo);

  return (
    <div>
      <video src={mediaCapabilitiesInfo.supported ? videoSources.webm : videoSources.mp4} controls>...</video>
    </div>
  );
};

mediaCapabilitiesInfo value contains the three Boolean properties supported, smooth, and powerEfficient, which describe whether decoding the media described would be supported, smooth, and powerEfficient.

This utility accepts a MediaDecodingConfiguration object argument and an optional initialMediaCapabilitiesInfo object argument, which can be used to provide a mediaCapabilitiesInfo state value when the user's browser does not support the relevant Media Capabilities API or no media configuration was given.

Adaptive Code-loading & Code-splitting

Code-loading

Deliver a light, interactive core experience to users and progressively add high-end-only features on top, if a user's hardware can handle it. Below is an example using the Network Status hook:

import React, { Suspense, lazy } from 'react';

import { useNetworkStatus } from 'react-adaptive-hooks/network';

const Full = lazy(() => import(/* webpackChunkName: "full" */ './Full.js'));
const Light = lazy(() => import(/* webpackChunkName: "light" */ './Light.js'));

const MyComponent = () => {
  const { effectiveConnectionType } = useNetworkStatus();
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        { effectiveConnectionType === '4g' ? <Full /> : <Light /> }
      </Suspense>
    </div>
  );
};

export default MyComponent;

Light.js:

import React from 'react';

const Light = ({ imageUrl, ...rest }) => (
  <img src={imageUrl} {...rest} />
);

export default Light;

Full.js:

import React from 'react';
import Magnifier from 'react-magnifier';

const Full = ({ imageUrl, ...rest }) => (
  <Magnifier src={imageUrl} {...rest} />
);

export default Full;

Code-splitting

We can extend React.lazy() by incorporating a check for a device or network signal. Below is an example of network-aware code-splitting. This allows us to conditionally load a light core experience or full-fat experience depending on the user's effective connection speed (via navigator.connection.effectiveType).

import React, { Suspense } from 'react';

const Component = React.lazy(() => {
  const effectiveType = navigator.connection ? navigator.connection.effectiveType : null

  let module;
  switch (effectiveType) {
    case '3g':
      module = import(/* webpackChunkName: "light" */ './Light.js');
      break;
    case '4g':
      module = import(/* webpackChunkName: "full" */ './Full.js');
      break;
    default:
      module = import(/* webpackChunkName: "full" */ './Full.js');
      break;
  }

  return module;
});

const App = () => {
  return (
    <div className='App'>
      <Suspense fallback={<div>Loading...</div>}>
        <Component />
      </Suspense>
    </div>
  );
};

export default App;

Server-side rendering support

The built version of this package uses ESM (native JS modules) by default, but is not supported on the server-side. When using this package in a web framework like Next.js with server-rendering, we recommend you

import {
  useNetworkStatus,
  useSaveData,
  useHardwareConcurrency,
  useMemoryStatus,
  useMediaCapabilitiesDecodingInfo
} from 'react-adaptive-hooks/dist/index.umd.js';

Browser Support

Demos

Network

Save Data

CPU Cores / Hardware Concurrency

Memory

Hybrid

References

License

Licensed under the Apache-2.0 license.

Team

This project is brought to you by Addy Osmani and Anton Karlovskiy.

react-adaptive-hooks's People

Contributors

addyosmani avatar anton-karlovskiy avatar eugeneglova avatar jdanil avatar midudev avatar mlampedx avatar mobily avatar mudssrali avatar stramel avatar wingleung avatar wmertens avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-adaptive-hooks's Issues

SSR; statics

I noticed that none of the x in navigator statements are guarded, so they will all break when rendering server-side. I propose adding typeof navigator !== 'undefined' in front of all those.

Furthermore, with the exception of the networking, the hooks return static data and even use useState to store the static data, causing extra work and memory use for something that's basically a single static value that can be determined at module load time. This confuses me, I created #1 to explain what I mean.

Ambient Light Events (hear me out...)

OK, just hear me out on this...

Let's say you have a cosy evening at home, your SO is sitting next to you on the couch watching The Crown on Netflix and the lights are turned off for extra dramatic effects.

However, suddenly you want to check something on your device for whatever reason. Maybe you're frustrated they switched actors in season 3 of The Crown or you want to check something you spent the entire day thinking of.

You turn your device on which is in dark mode to save your eyes (and arguments with you SO) and if you're lucky the site you're visiting automatically picks up the prefers-color-scheme: dark media query and sets the background to a nice and gentle dark tone for smoother reading in dark environments.

And then, you want to play a video and what happens...
alt text

But, what if the web app is intelligent enough to pick up light intensity and stream a more suited video source for dark environment. In comes the useAmbientLightEvents() hook allowing developers to adapt the media assets and others based on surrounding light intensity. You don't want to use dark mode for this because then you'd get the "dark" streams on broad daylight...

dark stream in broad daylight

Let's just say, Ambient Light Events could offer users better experiences than dark mode depending on the use case. Even though support is somewhat limited, only Edge supports it ๐Ÿ™„, I think we could let the big browsers know we might be interested in seeing this in future versions of their browsers. Unless there's a good reason they're not supporting it.

Well at the very least, thanks for hearing me out ๐Ÿ˜€

disclaimer: this ticket will self-close if it's not gaining traction in 1 week

SyntaxError: Unexpected token import

i am trying to implement the react adaptive hooks on my project but i get the following error.
Screenshot 2020-01-28 at 5 47 49 PM

i am using Next.js
my test page code is as follows

import React from 'react';
import Layout from '../components/template/Layout';
import { useNetworkStatus } from 'react-adaptive-hooks/network';

function Test() {
  const { effectiveConnectionType } = useNetworkStatus();
  return (
    <Layout>
      <p>test</p>
    </Layout>
  );
}

export default Test;

Misleading

From the title and description I understood that this library should be a React hooks collection. However, when reviewing the source code I found only the Network hook to internally use React hooks APIs, the other three hooks seem to be plain JS functions that memoize the data in module scope at boot time, hence they are not really React hooks.

My understanding of a React hook is something that internally itself uses some other React hook, which means React will schedule calls to those hooks on its hook-stack. If a JS function internally does not use any of React hooks, it is not really a hook, it can be called anywhere-anytime, and hook "rules" don't apply to it. For example, in react-use we have a rule about this in CONTRIBUTING.md.

React hooks are useful for two things: (1) store state at a specific node in React's rendering tree; (2) force a React component to re-render. For example, if you take useHardwareConcurrency hook, it reports number of CPUs on user's device. But that is not state, as number of CPUs is constant and you will never need to re-render a React component because of that data as it will never change.

I hope I'm missing something, would be happy to learn that.

Add support for Vue (composition api)

Vue 3 will have an equivalent to react hooks called Composition API.

Given the fact that there are already some hooks that can be used in Vue 3 like the save data hook and memory hook. This project could facilitate both libraries, centralizing the communication with the native browser api as much as possible and providing an lightweight implementation api for both react and vue.

mjs file extension breaking CRA

After the build scripts update, it is breaking CRA apps.

Failed to compile.

./node_modules/react-adaptive-hooks/dist/index.mjs
Can't import the named export 'useEffect' from non EcmaScript module (only default export is available)

CRA doesn't support .mjs files and requires a good amount of effort just to use that. From my understanding there is no reason that .mjs extension is needed to indicate a ESM file.

Related issues:

@midudev and @addyosmani

Feature - add encoding hook for media capabilities

Currently, useMediaCapabilitiesDecodingInfo will check if the current device can play a certain media file.

Would be nice if we could add support for encoding information, this will allow developers to check if the device can record to a specific media type.

If feasible, we could either...

  • add another hook within the media-capabilities folder called useMediaCapabilitiesEncodingInfo
  • refactor the current hook to support both API.

For now, the browser api is still in development and not available in chrome but we can already think about the use cases and wether or not we want to implement this.

Specs: https://w3c.github.io/media-capabilities/#dom-mediacapabilities-encodinginfo

[docs] How to avoid fetching different assets during fluctuations

Consider this scenario:

My phone is connected to WiFi(4g), the power goes off, and my mobile data(2g) kicks into action. If there were multiple assets fetched for 4g, all their low quality equivalents will now be fetched.
This will also happen when I am travelling between places as my data speed keeps fluctuating.

I am not sure what the solution to this problem is but I believe we shouldn't be replacing assets that are already fully loaded.

Improvement

Hi guys, first of all your lib is awesome. And what if it had a interface that automatically inject the right resource? Like in android, you have different paths for different assets,
IMG-20191112-WA0016

[bug] navigator.mediaCapabilities.decodingInfo returns a promise but is treated as a synchronous method in useMediaCapabilities

@wingleung cc @addyosmani

const useMediaCapabilities = (mediaConfig, initialMediaCapabilities = {}) => {
  let mediaCapabilities = {
    supported,
    hasMediaConfig: !!mediaConfig
  };

  mediaCapabilities = (mediaCapabilities.supported && mediaCapabilities.hasMediaConfig)
    ? navigator.mediaCapabilities.decodingInfo(mediaConfig)
    : {
      ...mediaCapabilities,
      ...initialMediaCapabilities
    };

  return {mediaCapabilities};
};

I think navigator.mediaCapabilities.decodingInfo(mediaConfig) was considered synchronous method in above implementation but according to MediaCapabilities.decodingInfo() MDN doc, it's asynchronous method that returns a promise.

So I wonder if this hook returns false for supported property all the time.
And this highlights an issue with the tests which should be catching such problems.

@wingleung Would you mind addressing this issue? Thank you.

React Adaptive Hooks will cause attribute mismatch during server side rendering

Hi. I saw a recent issue #2 regarding server side rendering. However, this behaviour simply hides the attribute mismatch that can happen due to the SSR phase and the client phase rendering differently.

I have built an example repo that showcases this mismatch. https://github.com/gja/react-adaptive-hooks-ssr

Apologies for just copying the relevant file (react-adaptive-hooks/network), but I didn't have time to configure babel and whatnot for server side rendering.

Usage to start repo:

npm install
# npx webpack (if you want to recompile main.js, though it's checked in)
node .

As you can see from https://github.com/gja/react-adaptive-hooks-ssr/blob/master/src/ReactAdaptiveHooksExample.js, it should render a paragraph whose text and class are matching (ie: <p class="4g">Your Network Status Is 4g</p>). However, the ssr hydration will not resolve the classname (ie: <p class="unsupported">Your Network Status Is 4g</p>).

However, react hydrate does not go through element attributes to look for mismatches, hence these attributes will never be caught. (worse, react believes that the classname is set to 4g, while the dom has a different value, so this will not be rectified in future renders as well).

I'm not sure what the supported model is here. In the old class world, i'd have only called out to the new functions during componentDidMount, and change the behaviour that way. I'm not sure what the equivalent is in the react hooks world.

Maybe return undefined when loading, then useEffect to set loading to false, and return the correct value on the next render.

Human readable usage

For jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize it could be nice to have a human readable property like deviceMemory is somewhat of a human readable property (in gigabytes). We could add a human readable property alongside machine value.

{
	supported: true,
	deviceMemory: 8,
	jsHeapSizeLimit: 4345298944
	jsHeapSizeLimit-human: "4.05 G",
	totalJSHeapSize: 47665242, 
	totalJSHeapSize-human:"45.46 MB",
	usedJSHeapSize: 38501994, 
	usedJSHeapSize-human: "36.72 MB",
}

Yarn or NPM?

@addyosmani I have spotted your Travis configuration is using Yarn, and there's no yarn.lock in the repo (there's package-lock.json file instead). I don't know whether it's done intentionally or it's just a simple mistake :) In order to get consistent installs across machines, it would be good to pick one of them.

WDYT?

Alternative to Core count

The basic idea behind the useHardwareConcurrency hook is great, but I fear that it is not sufficient to identify slow devices.
Even the moto G of the first generation (with a Snapdragon 400 CPU) has 4 cores. On the other hand, older iPhones only have 2 cores while the single cores are much faster and are able to run expensive animations more easily compared to low end android devices with more cores.

Could we maybe implement a "mini benchmark" to identify the JS execution speed of the device?
Maybe something along the lines of this:

function measure() {
  let start = Date.now();
  let index = 0;
  while (start + 8 >= Date.now()) index++;
  return index;
}

This would however block the mainthread for 8ms (duration could be changed obviously).

Get your score here: https://ogb5i.csb.app/

My Laptop (i7 8565U) manages something around 30.000 while my s10 (Exynos variant) only gets 3.000 - 10.000, the s7 of a friend manages ~1000 and the iPhone 6s of a friend gets 8.000

One would need to source the scores for different devices and implement categories.

Another added benefit would be, that the JS execution speed of the browser is factored in. That would mean, that IE gets fewer heavy animations because it is slower.

Too many re-renders. React limits the number of renders to prevent an infinite loop

I Implemented effectiveConnectionType to detect the network signal strength. When I call the function may be its going in infinite loop and returning following error in console:

image

Its going multiple times in switch cases. This is my function:


 const MeasureConnectionSpeed = () => {
     const { effectiveConnectionType } = useNetworkStatus();
      switch(effectiveConnectionType) {
        case 'slow-2g':
              setNetworkStrength("WEAK");
          break;
        case '2g':
              setNetworkStrength("OKAY");
          break;
        case '3g':
              setNetworkStrength("GREAT");
          break;
        case '4g':
              setNetworkStrength("EXCELLENT");
          break;
        default:
          break;
      }
  }
  MeasureConnectionSpeed();

Code styling

As of now, the code is very verbose and easy to read but I'm wondering if we can optimize readability by tweaking some code styles and namings to prevent double negatives.

For example, the following code...

let unsupported;

const useSaveData = (initialSaveDataStatus = null) => {
  if ('connection' in navigator && 'saveData' in navigator.connection) {
    unsupported = false;
  } else {
    unsupported = true;
  }

  return {
    unsupported,
    saveData: unsupported
      ? initialSaveDataStatus
      : navigator.connection.saveData === true
  };
};

export { useSaveData };

could become...

const useSaveData = (initialSaveDataStatus = null) => {
  const supported = ('connection' in navigator && 'saveData' in navigator.connection)

  return {
    unsupported: !supported,
    saveData: supported
		? navigator.connection.saveData === true
		: initialSaveDataStatus
  };
};

export { useSaveData };

maybe even use a supported property in the return value, I'm not sure if it's deliberate that we use negative naming for this.

Offline Detection?

is there a way to detect offline state in the network, so we can serve different content, using the hooks?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.