How can React useScript hook handles multiple calls - javascript

I'm using the React useScript hook (from useHooks website). It allows to easily load external scripts and cache them once loaded.
It works fine however I found an edge-case causing me some issues..
The problem is with the caching of scripts.
If I have a component loaded 2 times in a page using useScript as below:
const ScriptDemo = src => {
const [loaded, error] = useScript("https://hzl7l.codesandbox.io/test-external-script.js");
return (
<div>
<div>
Script loaded: <b>{loaded.toString()}</b>
</div>
<br />
{loaded && !error && (
<div>
Script function call response: <b>{TEST_SCRIPT.start()}</b>
</div>
)}
</div>
);
};
function App() {
return (
<div>
<ScriptDemo />
<ScriptDemo />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can see and reproduce here: https://codesandbox.io/s/usescript-hzl7l
If my App only have one ScriptDemo it's fine, however having two or more would make it fails.
Indeed the flow will be:
ScriptDemo -> is script cached ? no -> add script to cache -> fetch it -> render
ScriptDemo2 -> is script cached ? yes -> render (but it's not finish loading
yet ..)
One way to fix it is to change the useScript hook to only cache the script after a successful onScriptLoad callback.
The issue with this approach is that the external script will be called twice.
See here: https://codesandbox.io/s/usescript-0yior
ScriptDemo -> is script cached ? no -> fetch it -> add script to cache -> render
ScriptDemo -> is script cached ? no -> fetch it -> add script to cache -> render
I thought about caching the script src AND a loading boolean but then it implies setting up a timeout handling and it gets very complex in my opinion.
So, what is the best way to update the hook in order to load the external script only once but ensuring it's correctly loaded?

In useScript module, we will need to keep track of status of loading the script.
So instead of cachedScripts being simple array of strings, we now need to keep an object representing the status of loading.
This modified implementation of useScript will address the issue:
import { useState, useEffect } from 'react';
let cachedScripts = {};
export function useScript(src) {
// Keeping track of script loaded and error state
const [state, setState] = useState({
loaded: false,
error: false
});
useEffect(
() => {
const onScriptLoad = () => {
cachedScripts[src].loaded = true;
setState({
loaded: true,
error: false
});
};
const onScriptError = () => {
// Remove it from cache, so that it can be re-attempted if someone tries to load it again
delete cachedScripts[src];
setState({
loaded: true,
error: true
});
};
let scriptLoader = cachedScripts[src];
if(scriptLoader) { // Loading was attempted earlier
if(scriptLoader.loaded) { // Script was successfully loaded
setState({
loaded: true,
error: false
});
} else { //Script is still loading
let script = scriptLoader.script;
script.addEventListener('load', onScriptLoad);
script.addEventListener('error', onScriptError);
return () => {
script.removeEventListener('load', onScriptLoad);
script.removeEventListener('error', onScriptError);
};
}
} else {
// Create script
let script = document.createElement('script');
script.src = src;
script.async = true;
// Script event listener callbacks for load and error
script.addEventListener('load', onScriptLoad);
script.addEventListener('error', onScriptError);
// Add script to document body
document.body.appendChild(script);
cachedScripts[src] = {loaded:false, script};
// Remove event listeners on cleanup
return () => {
script.removeEventListener('load', onScriptLoad);
script.removeEventListener('error', onScriptError);
};
}
},
[src] // Only re-run effect if script src changes
);
return [state.loaded, state.error];
}
Edit:
Went to GitHub page of useHooks to suggest this improvement and found some one has already posted similar fix:
https://gist.github.com/gragland/929e42759c0051ff596bc961fb13cd93#gistcomment-2975113

Related

ReactJS - Adding (Mollie) script to React page

I'm trying to import a script called Mollie (used for payments), but I'm not sure how to do it in React.
Normally in Javascript you would do something like this:
<html>
<head>
<title>My Checkout</title>
</head>
<body>
<script src="https://js.mollie.com/v1/mollie.js"></script>
</body>
</html>
I've tried this (according to other Stackoverflow posts)
useEffect(() => {
const script = document.createElement("script");
script.src = "https://js.mollie.com/v1/mollie.js";
script.addEventListener("load", () => setScriptLoaded(true));
document.body.appendChild(script);
}, []);
const mollie = Mollie(); // Mollie is not defined
But then Mollie is undefined. Can anyone point in the right direction on how to import Mollie in React?
I'm following this guide (but it's for standard Javascript)
You can easily install this package from npmjs.com where you can find necessary documentations and examples to get started. Installation:
npm i #mollie/api-client
the point here is that the effect is being invoked after the react component mounted and rendered to the user.
The next line where you are trying to call Mollie in fact running earlier when component is being constructed but not rendered yet.
There are multiple options what you can do about it:
Import script in the index.html file as you do for standard non-React solution. There should be a "public" folder containing this file in case of create-react-app usage or other place in case of custom project setup. The HTML should exist in any form even in case it's being generated on the server side dynamically. A mollie instance can later be created in the component or globally.
Use multiple effects in the React component: one to load Mollie and another one to use it when loaded:
// MollieExample.jsx
const MollieExample = () => {
const [mollie, setMollie] = useState();
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://js.mollie.com/v1/mollie.js';
script.addEventListener("load", () => {
setMollie(window.Mollie(/* Mollie arguments */);
});
document.body.appendChild(script);
}, []);
useEffect(() => {
if (mollie) {
console.log('Mollie exists here');
console.log(typeof mollie);
}
} , [mollie]);
return <p>typeof mollie: {typeof mollie}</p>;
};
Use dynamic script loading in case it is required with globally shared Mollie instance via custom hook:
// useMollie.js
let molliePromise;
const useMollie = (effect, deps) => {
const [mollie, setMollie] = useState();
const mollieCb = useCallback((mollie) => effect(mollie), deps);
useEffect(() => {
if (!molliePromise) {
molliePromise = new Promise((resolve) => {
const script = document.createElement("script");
script.src = "https://js.mollie.com/v1/mollie.js";
script.addEventListener("load", () => {
resolve(window.Mollie(/* Mollie arguments */);
});
document.body.appendChild(script);
});
}
molliePromise.then((mollie) => setMollie(mollie));
}, []);
useEffect(() => {
if (mollie) {
mollieCb(mollie);
}
}, [mollie, mollieCb]);
};
// MollieConsumer.jsx
const MollieConsumer = () => {
useMollie((mollie) => {
console.log('Mollie exists here');
console.log(typeof mollie);
}, [/* useMollie effect callback dependencies array */]);
return (
<p>Mollie consumer</p>
);
};
// App.jsx
function App() {
/* Both consumers use the same instance of Mollie */
return (
<div>
<MollieConsumer/>
<MollieConsumer/>
</div>
);
}
I assume you will end up with using some middle option. For instance, with importing script in the index.html (or any other sort of the HTML page you have containing the React application host element) and global hook.

ReactJS: Data not binding from continuous response

While integrating the aws ivs streaming channel with quiz related metadata, at that time getting the console.log of the metadata records and while passing those records into another component it is not handling any how.
A playground that i have created into codesandobx
PlayerComponent
function PlayerComponent(options) {
useEffect(() => {
const script = document.createElement("script");
script.src = "https://player.live-video.net/1.0.0/amazon-ivs-player.min.js";
script.async = true;
document.body.appendChild(script);
script.onload = (IVSPlayer) => {
if (IVSPlayer.isPlayerSupported) {
const player = IVSPlayer.create();
player.attachHTMLVideoElement(document.getElementById("video-player"));
player.load(
"https://fcc3ddae59ed.us-west-2.playback.live-video.net/api/video/v1/us-west-2.893648527354.channel.xhP3ExfcX8ON.m3u8"
);
player.play();
player.addEventListener(
IVSPlayer.PlayerEventType.TEXT_METADATA_CUE,
(cue) => {
const metadataText = cue.text;
setMetaData(metadataText);
console.log("PlayerEvent - METADATA: ", metadataText);
}
);
}
};
return () => {
document.body.removeChild(script);
};
}, []);
return (
<div ref={divEl}>
<video id="video-player" ref={videoEl} autoPlay controls></video>
{metaData !== undefined ? <QuizComponent metadata={metaData} /> : ""}
</div>
);
}
On QuizComponent would like to render those metadata
export default function QuizComponent(props) {
const questionData = props;
return (
<React.Fragment>
<h2>{questionData.metadata.question}</h2>
</React.Fragment>
);
}
But any how not able to render the data into component.
Ref example of what I am going to implement.
https://codepen.io/amazon-ivs/pen/XWmjEKN?editors=0011
I found the problem. Basically you are referring IVSPlayer as if it was the argument of the arrow function you passed to script onload, while the argument instead is an event (the onload event).
Solution: const {IVSPlayer} = window;. Infact docs say
Once amazon-ivs-player.min.js is loaded, it adds an IVSPlayer variable to the global context.
Docs also explain how to setup with NPM which you may be interested in.
I updated my playground here.
I also suggest you to edit the version of the player as the last one is 1.2.0.

Change state dynamically based on the external Internet connectivity - React (offline/online)

I have a React component, that includes the availability flag of Internet connectivity. UI elements have to be dynamically changed according to state real-time. Also, functions behave differently with the changes of the flag.
My current implementation polls remote API using Axios in every second using interval and updates state accordingly. I am looking for a more granular and efficient way to do this task to remove the 1-second error of state with the minimum computational cost. Considered online if and only if device has an external Internet connection
Current implementation :
class Container extends Component {
constructor(props) {
super(props)
this.state = {
isOnline: false
};
this.webAPI = new WebAPI(); //Axios wrapper
}
componentDidMount() {
setInterval(() => {
this.webAPI.poll(success => this.setState({ isOnline: success });
}, 1000);
}
render() {
return <ChildComponent isOnline={this.state.isOnline} />;
}
}
Edited:
Looking for a solution capable of detecting external Internet connectivity. The device can connect to a LAN which doesn't have an external connection. So, it is considered offline. Considers online if and only if device has access to external Internet resources.
You can use https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event
window.addEventListener('offline', (event) => {
console.log("The network connection has been lost.");
});
and https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event
for checking when you're back online
window.addEventListener('online', (event) => {
console.log("You are now connected to the network.");
});
Method one: Using legacy browser API - Navigator.onLine
Returns the online status of the browser. The property returns a boolean value, with true meaning online and false meaning offline. The property sends updates whenever the browser's ability to connect to the network changes. The update occurs when the user follows links or when a script requests a remote page. For example, the property should return false when users click links soon after they lose internet connection.
You can add it to your component lifecycle:
Play with the code below using Chrome dev tools - switch "Online" to "Offline" under the Network tab.
class App extends React.PureComponent {
state = { online: window.navigator.onLine }
componentDidMount() {
window.addEventListener('offline', this.handleNetworkChange);
window.addEventListener('online', this.handleNetworkChange);
}
componentWillUnmount() {
window.removeEventListener('offline', this.handleNetworkChange);
window.removeEventListener('online', this.handleNetworkChange);
}
handleNetworkChange = () => {
this.setState({ online: window.navigator.onLine });
}
render() {
return (
<div>
{ this.state.online ? 'you\'re online' : 'you\'re offline' }
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
However, I think this isn't what you want, you wanted a real-time connection validator.
Method two: Checking internet connection by using it
The only solid confirmation you can get if the external internet connectivity is working is by using it. The question is which server you should call to minimize the cost?
There are many solutions on the internet for this, any endpoint that responds with a quick 204 status is perfect, e.g.:
calling to Google server (for it being the most battle-tested (?) )
calling its cached JQuery script endpoint (so even if the server is down, you should still be able to get the script as long as you have a connection)
try fetching an image from a stable server (e.g.: https://ssl.gstatic.com/gb/images/v1_76783e20.png + date timestamp to prevent caching)
IMO, if you are running this React app on a server, it makes the most sense to call to your own server, you can call a request to load your /favicon.ico to check the connection.
This idea (of calling your own server) has been implemented by many libraries, such as Offline, is-reachable, and is widely used across the community. You can use them if you don't want to write everything by yourself. (Personally I like the NPM package is-reachable for being simple.)
Example:
import React from 'react';
import isReachable from 'is-reachable';
const URL = 'google.com:443';
const EVERY_SECOND = 1000;
export default class App extends React.PureComponent {
_isMounted = true;
state = { online: false }
componentDidMount() {
setInterval(async () => {
const online = await isReachable(URL);
if (this._isMounted) {
this.setState({ online });
}
}, EVERY_SECOND);
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
return (
<div>
{ this.state.online ? 'you\'re online' : 'you\'re offline' }
</div>
);
}
}
I believe what you have currently is already fine, just make sure that it is calling the right endpoint.
Similar SO questions:
Detect the Internet connection is offline?
Detect network connection in React Redux app - if offline, hide component from user
https://stackoverflow.com/Questions/3181080/How-To-Detect-Online-Offline-Event-Cross-Browser
Setup a custom hook
Setup a hook with the online, offline events. then update a state and return it. This way you can use it anywhere in your app with an import. Make sure you clean up with the return function. If you don't you will add more and more event listeners each time a component using the hook mounts.
const onlineHook = () => {
const {isOnline, setOnline} = React.useState();
React.useEffect(() => {
const goOnline = function(event){
setOnline(true);
});
const goOffline = function(event){
setOnline(false);
});
window.addEventListener('offline', goOffline);
window.addEventListener('online', goOnline);
return () => {
window.removeEventListener('offline', goOffline);
window.removeEventListener('online', goOnline);
}
}, [])
return isOnline
}
To use this just import the above hook and call it like this.
const isOnline = onlineHook(); // true if online, false if not
You can create a component to share between all subcomponents
used:
import React, { useState, useEffect } from "react";
export default function NetworkChecker() {
const [networkStatus, setNetworkStatus] = useState(true)
useEffect(() => {
window.addEventListener('offline', (event) => {
setNetworkStatus(false)
});
window.addEventListener('online', (event) => {
setNetworkStatus(true)
});
return function cleanupListener() {
window.removeEventListener('online', setNetworkStatus(true))
window.removeEventListener('offline', setNetworkStatus(false))
}
},[])
if (networkStatus) {
return <div className={"alert-success"}>Online</div>
} else {
return <div className={"alert-danger"}>Offline</div>
}
}

Jest: Testing API Methods from Intercom

I am having trouble understanding what the testing flow would be for testing functions which use functions loaded from a JavaScript library from Intercom.
My method looks like this:
export const generateButton = (handleOnClick) => {
case "moo":
return <button onClick={() => Intercom('show')}>Sign Up</button>
default:
return 'moo'
The error I get when running this is:
ReferenceError: Intercom is not defined
So I figured it out, I needed to add a new file and point jest set up on package.json to it like so (the file added is mockObject)
"setupFiles": [
"./config/jest/setupJest.js",
"./config/jest/mockObject.js"
],
then in the file itself has this in it
global.Intercom = () => {
console.log('Intercom called')
}
If I understand what you're trying to do then create a dummyFunction to replace Intercom in your tests. Something like this...
const Intercom = jest.fn();
describe('button click', () => {
it('Intercom is called correctly', () => {
// whatever component contains the button should be mounted
const wrapper = mount(<YourComponentHere />);
// you may need to add a class to target the specific button
wrapper.find('button').simulate('click');
expect(dummyFunction).toHaveBeenCalledWith('show');
expect(dummyFunction).toHaveBeenCalledTimes(1);
});
});

react-http-request does not change loading state after second request

I am now using react-http-request in my React.js component to send request and process the response. The URL parameter passed is relevant to the component state such that when the state changes, the component will be re-rendered and change the component display.
This works on the first request. However, I found that the component does not return a {load: true} after the second request, and I wonder how to solve this.
I tried to call the onRequest method and set the loading state for the component, but I cannot change the loading state after the request is finished (as render function cannot change the state).
react-http-request: https://github.com/mbasso/react-http-request
My Code is like below:
var FilmList = React.createClass({
getInitialState: function(){
return {
queryType: this.props.queryType
}
},
// ... details emitted.
render: function(){
return (<Request
url={config.url.api + "/" + this.state.queryType}
method="get"
accept="application/json"
query={{ several parameter }}
>
{
({error, result, loading}) => {
if (loading || error) {
return <Loading />
}
else {
// process the result here.
}
}
}
</Request>)
}
So, my initial recommendation would be that you use some state management library (redux, mobx, etc) but it is not necessary to get a working example of your code, so:
import fetch from 'whatwg-fetch'; // gives compatibility with older browsers
var FilmList = React.createClass({
getInitialState: function(){
return {
queryType: this.props.queryType,
content: null
}
},
componentWillMount: function() {
this.fetchContent();
},
fetchContent: function() {
const uri = config.url.api + "/" + this.state.queryType;
// You can use w/e you want here (request.js, etc), but fetch is the latest standard in the js world
fetch(uri, {
method: 'GET',
// More properties as you see fit
})
.then(response => response.json()) // might need to do this ;)
.then(response => {
this.setState({
content: response
})
})
},
// ...
render: function(){
const content = this.state.content? (
// render your content based on this.state.content
): (
<Loading />
)
return content;
}
});
Haven't tested this code, but there are some nice benefits to it:
The http request is not dependant on React, which should (in theory) be for UI components.
The fetching mechanism is decoupled, and can be re-used at any point in the component lifecycle
In my opinion easier to read, divided into smaller logical chunks
I would recommend reading the React Component Lifecycle.
In this case, I read the source code of the react-http-request, and found that there is a weakness that after accepting and sending the second request, the component failed to update the state of "loading" returns.
// starts from Line 49
value: function componentWillReceiveProps(nextProps) {
if (JSON.stringify(this.props) === JSON.stringify(nextProps)) {
return;
}
this.request.abort();
this.performRequest(nextProps);
}
Manually changed the state of loading here can help reset the loading after each request received.
I changed the source code of this lib, and sent the pull request to the repo. It's now merged into master and ejected a new release.
See: https://github.com/mbasso/react-http-request/pull/3
Thus, this problem can be solved by keeping the lib update to the release (currently it is 1.0.3).

Categories

Resources