I have a website made in ReactJS. In public/index.html, I have
<head>
<script src="/lib/analyzejs-v1.js"></script>
<script src="/lib/analyzejs-v2.js"></script>
</head>
<body>
<div id="root"></div>
</body>
where analyzejs-v1.js has 6Mo, and analyzejs-v2.js has 3Mo; they are all fixed files that I could not much modify.
These two files are not modules; their functions are declared (e.g., declare function f1(address: string): string; in src/defines/analyzejs-v1.d.ts). So some components call functions of analyzejs-v1.js by using a function name like f1(...) directly without any namespace, import, or export. And the rest of the components call functions of analyzejs-v2.js by using a function name like f2(...) directly without any namespace, import, or export.
It takes time to load these two js library files. So I'm looking for a way to load either analyzejs-v1.js or analyzejs-v2.js according to the component (or URL).
So does anyone know a conventional way to load different JS library files for different components?
If you don't need two script at the same time, you can add the script tag in the runtime when necessary.
I can provide you a hook which I have used to load the script on the fly.
export function useScript(url: string, clean: boolean = false, cleanJob: () => void = () => undefined): boolean {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
let create = false;
let script = document.querySelector(`script[src="${url}"]`) as HTMLScriptElement | null;
if (!script) {
script = document.createElement('script');
script.src = url;
script.async = true;
if (type (document as any).attachEvent === 'object') {
(script as any).onreadystatechange = () => {
if ((script as any).readyState === 'loaded') {
setLoaded(true);
}
}
} else {
script.onload = () => {
setLoaded(true);
}
}
document.body.appendChild(script);
create = true;
} else {
setLoaded(true);
}
// For a special library, you can do the clean work by deleting the variable it exports.
return () => {
if (create && script && clean) {
setLoaded(false);
document.body.removeChild(script);
cleanJob && cleanJob();
}
}
}, [url]);
return loaded;
}
To use it:
export const Comp = (props: ICompProps) => {
const loaded = useScript('https://path/to/the/script.js');
// if you want to do some clean work, Suppose the external script introduces the variable A, And A can be reasigned.
// const loaded = useScript('https://path/to/the/script.js', true, () -> { A = undefined; });
useEffect(() -> {
if (loaded) {
// Suppose the external script introduces the variable A. Now it is available.
const a = new A();
// do something with a.
}
}, [loaded]);
if (loaded) {
return XXX;
} else {
return null;
}
}
If the script is not a module, just add a typescript declare file without import statements, and declare the global variable the script export. Such as:
declare interface XXX {
YYY
}
declare const ScriptValue: XXX;
When you import a script using the <script> tag, the library can only be used client side and therefore not with node. However, if you mark it as a module, another script can use it like this:
index.html:
<script src="test.mjs" type="module"></script>
<script type="module">
import {hello} from "./test.mjs"
hello()
</script>
test.mjs:
export function hello(text) {
console.log("hello from test")
}
The only thing is the communication between your react scripts and that inline script. The only way, I figured out, how to achieve this is using window.
DISCLAIMER
I'm really not sure, if anyone should use it this way. I have only tested this once and it might very well break... Maybe someone else can tell me their opinion on my approach.
index.tsx
... // imports
(window as any).importStuff = (a: any) => {
a.hello()
}
...
index.html
<script src="test.mjs" type="module"></script>
<script type="module">
import {hello} from "./test.mjs"
window.importStuff({
hello: hello
})
</script>
If you are looking for better performance of the page and not let the scripts block the DOM content loading, then you may want to look at adding defer to the script https://javascript.info/script-async-defer#defer.
If you are looking for dynamic loading of script, and load the script only when its first used, check this doc https://javascript.info/script-async-defer#dynamic-scripts
You can create <script> tag when your component is loaded.
At the first, We create a function to create the script tag
const scriptGenerator = (options = {}) => {
const s = document.createElement("script");
for (const option in options) {
s[option] = options[option]
}
document.querySelector("head").appendChild(s);
}
We can use two attributes to load scripts
defer
async
defer: The defer attribute tells the browser not to wait for the script. Instead, the browser will continue to process the HTML, build DOM. The script loads “in the background”, and then runs when the DOM is fully built.
async: The async attribute is somewhat like defer. It also makes the script non-blocking. But it has important differences in the behavior.
async & defer Docs
After all these steps, we can define our script in the head. You can use useEffect
useEffect(() => {
// V1
scriptGenerator({
src: "...",
async: 1,
});
// V2
scriptGenerator({
src: "...",
async: 1,
});
}, []);
Do not forget to delete them after exiting the component
useEffect(() => {
// Generate script
return () => {
document
.querySelector("head")
.querySelectorAll('script[src="..."]')
.remove();
}
});
If we want to reach a conclusion, it is as follows:
import { useEffect } from 'react';
const Component = () => {
const scriptGenerator = (options = {}) => {
const s = document.createElement("script");
for (const option in options) {
s[option] = options[option]
}
document.querySelector("head").appendChild(s);
}
useEffect(() => {
// V1
scriptGenerator({
src: "...",
async: 1,
});
// V2
scriptGenerator({
src: "...",
async: 1,
});
return () => {
document
.querySelector("head")
.querySelectorAll('script[src="..."]')
.remove();
}
});
}
Related
I made a custom hook that registers data into a store keyed by the components that use it, and then removes the data from the store when the components unmount. Basics are like this
const [id] = useState(Math.random());
const setData = useCallback((data) => {
if(data) {
setRegistrations({
...registrations,
[id]: data
});
} else {
const {[id]: _, ...nextRegistrations} = registrations;
setRegistrations(nextRegistrations);
}
}, [id]);
useEffect(() => {
// update the registration if the data changed
setData(data);
}, [data, setData]);
useEffect(() => {
// remove the registration on unmount
setData();
}, []);
I'm using Math.random() to generate a unique id for each component that uses my hook, and that works fine, but I was hoping there's a way to figure out the name of the component that is using my hook. It'd make debugging my application state easier if I could see which data was registered by which component, but instead all I have to look at is a random decimal.
Right now my store looks like this
{
'0.5694216823063629': { ... }
'0.002760497537984463': { ... }
}
I'd prefer if I could do something like this
{
'MyComponent-1': { ... }
'MyComponent-2': { ... }
'MyOtherComponent-1': { ... }
}
Yes, I know I could just make components pass in a key, but that's missing the point. I want the API to remain pure and not pollute it just so debugging is easier.
Is there a way to figure out which component used my hook?
Functions and the arguments object have deprecated properties that allow them to see who called them - Function.caller and arguments.callee. However, both of them are blocked in strict mode.
You can get the caller's name from the stack when throwing an error, but this is ugly, and you should only use it in development mode. This would only work for function component's, but since you're using it in a hook, it won't be a problem.
Example (taken from this article):
// taken from https://devimalplanet.com/javascript-how-to-get-the-caller-parent-functions-name
function whoIsMyDaddy() {
try {
throw new Error();
} catch (e) {
// matches this function, the caller and the parent
const allMatches = e.stack.match(/(\w+)#|at (\w+) \(/g);
// match parent function name
const parentMatches = allMatches[2].match(/(\w+)#|at (\w+) \(/);
// return only name
return parentMatches[1] || parentMatches[2];
}
}
const useDemo = () => {
const daddy = whoIsMyDaddy()
React.useEffect(() => console.log(daddy))
}
const ComponentX = () => {
useDemo()
return 'X'
}
const ComponentY = () => {
useDemo()
return 'Y'
}
ReactDOM.render(
<div>
<ComponentX />
<ComponentY />
</div>,
root
)
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Note: using Math.random() as an id is not a good idea, since Math.random() is not that random - see this answer. You can use Crypto.getRandomValues() if supported, or an external uuid library.
const importAll = require =>
require.keys().reduce((acc, next) => {
acc[next.replace("./", "")] = require(next);
return acc;
}, {});
const images = importAll(
require.context('./images', false, /\.(png|jpe?g|svg)$/)
);
console.log(images);
I have the above block of code that imports all files in a given directory given that they end in the required type however, I want to reuse this code so that I can import it into different modules across my app and specify the path for each individual module.
I've also tried the below block of code but am new to this syntax, any help is appreciated!
const importAll = require =>
require.keys().reduce((acc, next) => {
acc[next.replace("./", "")] = require(next);
return acc;
}, {});
const images = (path) => importAll(
require.context(path, false, /\.(png|jpe?g|svg)$/)
);
export default {
images,
}
Found out that It's because require.context will only take a literal regex in webpack 4 and therefore, not a variable.
See here https://webpack.docschina.org/guides/dependency-management/
However, mcm-ham found a work around which can be viewed here https://github.com/webpack/webpack/issues/4772?fbclid=IwAR3m6kmghtm4EoV8IspcqEfFItdeMnzQHGiEkXbwqgOywXSUFxcWXxbyjRI#issuecomment-344556837
I have a NextJS application and I want to add Google auto translate widget to my app.
So made a function like this:
function googleTranslateElementInit() {
if (!window['google']) {
console.log('script added');
var script = document.createElement('SCRIPT');
script.src =
'//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';
document.getElementsByTagName('HEAD')[0].appendChild(script);
}
setTimeout(() => {
console.log('translation loaded');
new window.google.translate.TranslateElement(
{
pageLanguage: 'tr',
includedLanguages: 'ar,en,es,jv,ko,pt,ru,zh-CN,tr',
//layout: google.translate.TranslateElement.InlineLayout.SIMPLE,
//autoDisplay: false,
},
'google_translate_element'
);
}, 500);
}
And I call this function in useEffect(), it loads but when I route to another page it disappers.
When I checked the console I saw translation loaded so setTimeout scope called every time even when I route to another page but translation widget is not appear, only appear when I refresh the page.
How can I solve this?
Thanks to the SILENT's answer: Google no longer support this widget.
So I'm going to configure next-i18next which is a i18n (lightweight translation module with dynamic json storage) for NextJS.
Also, I think the problem with this widget was Google's JS code is attach that widget to DOM itself so it's not attached to VirtualDOM, thats why when I route in app, React checked VirtualDOM and update DOM itself so the widget disappear because it's not on VirtualDOM. (That's just a guess)
Edit: after further testing I found that this code might still be unstable. Be careful if using it in production.
Use the code below inside your custom app and do not forget to put <div id="google_translate_element" /> inside your page or component. Based on this and this answers.
import { useEffect } from 'react'
import { useRouter } from 'next/router'
const MyApp = ({ Component, pageProps }) => {
const { isFallback, events } = useRouter()
const googleTranslateElementInit = () => {
new window.google.translate.TranslateElement({ pageLanguage: 'en' }, 'google_translate_element')
}
useEffect(() => {
const id = 'google-translate-script'
const addScript = () => {
const s = document.createElement('script')
s.setAttribute('src', '//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit')
s.setAttribute('id', id)
const q = document.getElementById(id)
if (!q) {
document.body.appendChild(s)
window.googleTranslateElementInit = googleTranslateElementInit
}
}
const removeScript = () => {
const q = document.getElementById(id)
if (q) q.remove()
const w = document.getElementById('google_translate_element')
if (w) w.innerHTML = ''
}
isFallback || addScript()
events.on('routeChangeStart', removeScript)
events.on('routeChangeComplete', addScript)
return () => {
events.off('routeChangeStart', removeScript)
events.off('routeChangeComplete', addScript)
}
}, [])
return <Component {...pageProps} />
}
export default MyApp
I am trying to create a menu component that reads the contents of the pages folder at build time. However I haven't had any success. Here is what I have tried:
import path from "path";
import * as ChangeCase from "change-case";
export default class Nav extends React.Component {
render() {
return (
<nav>
{this.props.pages.map((page) => (
<a href={page.link}>{page.name}</a>
))}
</nav>
);
}
async getStaticProps() {
let files = fs.readdirSync("../pages");
files = files.filter((file) => {
if (file == "_app.js") return false;
const stat = fs.lstatSync(file);
return stat.isFile();
});
const pages = files.map((file) => {
if (file == "index.js") {
const name = "home";
const link = "/";
} else {
const link = path.parse(file).name;
const name = ChangeCase.camelCase(link);
}
console.log(link, name);
return {
name: name,
link: link,
};
});
return {
props: {
pages: pages,
},
};
}
}
This does not work, the component does not receive the pages prop. I have tried switching to a functional component, returning a promise from getStaticProps(), switching to getServerSideProps(), and including the directory reading code into the render method.
The first two don't work because getStaticProps() and getServerSideProps() never get called unless the component is a page, and including the code in the render method fails because fs is not defined or importable since the code might run on the front end which wouldn't have fs access.
I've also tried adding the code to a getStaticProps() function inside _app.js, with the hopes of pushing the pages to the component via context, but it seems getStaticProps() doesn't get called there either.
I could run the code in the getStaticProps function of the pages that include the menu, but I would have to repeat that for every page. Even if I extract the logic into a module that gets called from the getStaticProps, so something like:
// ...
export async function getStaticProps() {
return {
props: {
pages: MenuMaker.getPages(),
// ...
}
}
}
and then pass the pages to the navigation component inside the page via the Layout component:
export default function Page(props) {
return (
<Layout pages={props.pages}></Layout>
)
}
then that's still a lot of boilerplate to add to each page on the site.
Surely there is a better way... It can't be that there is no way to add static data to the global state at build time, can it? How do I generate a dynamic menu at build time?
I managed to get this working by exporting a function from next.config.js and setting an environment variable that contains the menu structure. I abstracted the menu loading code into it's own file. After seeing the result, I understand better why I was not able to find an example of anyone doing something similar:
The menu is not ordered the way I would like. I could sort it alphabetically, or by the modification date but realistically it almost always needs to be manually sorted in relation to the subject of the pages. I could use an integer, either tacked on to the filename or somewhere in the file (perhaps in a comment line). But in retrospect I think that just hard coding the links in a component is probably the best way after all since it offers much more flexibility and probably isn't going to be much more work even in the very long run.
That being said I am sharing my solution as it is a way to initialize an app wide static state. It's not ideal, you will have to restart the dev server if you wish to recalculate the variables here, which is why I'm still interested in other possible solutions, but it does work. So here it is:
next.config.js
const menu = require("./libraries/menu.js");
module.exports = (phase, { defaultConfig }) => {
return {
// ...
env: {
// ...
menu: menu.get('pages'),
// ...
},
// ...
};
};
libraries/menu.js
const fs = require("fs");
const path = require("path");
const ccase = require("change-case");
module.exports = {
get: (pagePath) => {
if (pagePath.slice(-1) != "/") pagePath += "/";
let files = fs.readdirSync(pagePath);
files = files.filter((file) => {
if (file == "_app.js") return false;
const stat = fs.lstatSync(pagePath + file);
return stat.isFile();
});
return files.map((file) => {
if (file == "index.js") {
return {
name: "Home";
link: "/";
};
} else {
link = path.parse(file).name;
return {
link: link;
name: ccase.capitalCase(link);
};
}
});
},
};
Then the actual menu is generated from the environment variable in a component that can be included in the layout:
components/nav.js
import Link from "next/link";
export default class Nav extends React.Component {
render() {
return (
<nav>
{process.env.menu.map((item) => (
<Link key={item.link} href={item.link}>
<a href={item.link}>
{item.name}
</a>
</Link>
))}
</nav>
);
}
}
You can try this:
const fg = require('fast-glob');
const pages = await fg(['pages/**/*.js'], { dot: true });
How to script a conditional external script reference inside the public html file in reactjs.
<script>
var element=document.createElement('script');
if (env.test) {
element.setAttribute('src', 'https://pay-sandbox.gocardless.com/js/beta');
} else {
element.setAttribute('src', 'https://pay.gocardless.com/js/beta');
}
document.getElementById("...").appendChild(element);
</script>
// Was thinking something like the above but don't know how to access the environmental variable from here.
<script>
var element=document.createElement('script');
if (env.test) {
element.setAttribute('src', 'https://pay-sandbox.com/js/beta');
} else {
element.setAttribute('src', 'https://pay.com/js/beta');
}
document.getElementById("...").appendChild(element);
</script>
Instead of in the index.html file, you can add a custom hook which loads the script dynamically in your App component.
import React, { useState, useEffect } from 'react';
import { render } from 'react-dom';
interface AppProps { }
const App: React.FC<AppProps> = ({}) => {
const [hasScriptLoaded, setLoadedStatus] = useState(false);
const isDev = true; // Set this boolean by reading your env here
const src = isDev ? 'https://pay-sandbox.gocardless.com/js/beta' :
'https://pay.gocardless.com/js/beta';
useEffect(
() => {
// Create script
let script = document.createElement('script');
script.src = src;
script.async = true;
// Event listener for when script is loaded
const onScriptLoad = () => {
setLoadedStatus(true);
};
// Attach event listener to script
script.addEventListener('load', onScriptLoad);
document.body.appendChild(script);
// Clean up
return () => {
script.removeEventListener('load', onScriptLoad);
};
},[src])
return (
<div>
<p>
Start editing to see some magic happen :)
{hasScriptLoaded && <div>Script has been loaded</div>}
</p>
</div>
);
}
render(<App />, document.getElementById('root'));
You can play around in this Stackblitz. Inspect the scripts being loaded in the browser in the StackBlitz and check that script is rendered according to the isDev variable. You can set the isDev variable based on the environment instead.