How do I access the 'currentImageIdIndex' when using the stack scroll tool in Cornerstone.js in a React functional component? - reactjs

I'm currently successfully displaying a stack of images in a React component but am unsure where to place an event listener in order to access the currentImageIdIndex when scrolling.
import React, { useEffect, useRef, useCallback } from "react";
import cornerstone from "cornerstone-core";
import cornerstoneMath from "cornerstone-math";
import cornerstoneTools from "cornerstone-tools";
import cornerstoneFileImageLoader from "cornerstone-file-image-loader";
import Hammer from "hammerjs";
function StackImageViewport(props) {
const viewerRef = useRef(null);
const base64StringToArrayBuffer = useCallback((base64) => {
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}, []);
const initializeCornerstone = () => {
// Initialise cornerstone and link to DOM element
cornerstoneTools.external.cornerstone = cornerstone;
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
cornerstoneFileImageLoader.external.cornerstone = cornerstone;
cornerstoneTools.external.Hammer = Hammer;
cornerstoneTools.init();
cornerstone.enable(viewerRef.current);
};
const setCornerstoneTools = () => {
// define Cornerstone Tools
const StackScrollTool = cornerstoneTools.StackScrollTool;
const StackScrollMouseWheelTool =
cornerstoneTools.StackScrollMouseWheelTool;
const WindowingTool = cornerstoneTools.WwwcTool;
// Add tools
cornerstoneTools.addTool(StackScrollTool);
cornerstoneTools.addTool(StackScrollMouseWheelTool);
cornerstoneTools.addTool(WindowingTool);
// set tools to Active state
cornerstoneTools.setToolActive("StackScroll", { mouseButtonMask: 1 });
cornerstoneTools.setToolActive("StackScrollMouseWheel", {});
cornerstoneTools.setToolActive("Wwwc", { mouseButtonMask: 2 });
};
const displayStack = (stackMediaArray) => {
let mediaArray = [];
// 'stackMediaArray' is an array of images, each containing a buffer of the image
Promise.all(
stackMediaArray.map((mediaObject) => {
return new Promise((resolve, reject) => {
let imageBuffer = base64StringToArrayBuffer(mediaObject.buffer);
const imageId =
cornerstoneFileImageLoader.fileManager.addBuffer(imageBuffer);
mediaArray.push(imageId);
resolve(mediaObject);
}).catch(console.error);
})
);
//define the stack
const stack = {
currentImageIdIndex: 0,
imageIds: mediaArray,
};
// load images and set the stack
cornerstone.loadAndCacheImage(mediaArray[0]).then((image) => {
cornerstone.displayImage(viewerRef.current, image);
cornerstoneTools.addStackStateManager(viewerRef.current, ["stack"]);
cornerstoneTools.addToolState(viewerRef.current, "stack", stack);
});
setCornerstoneTools();
};
useEffect(() => {
if (!viewerRef.current) {
return;
}
initializeCornerstone();
displayStack(props.stackMediaArray);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewerRef]);
return (
<>
<div ref={viewerRef} id="viewer" className="flex h-1/2-screen"></div>
</>
);
}
export default StackImageViewport;
This attempts to answer the question:
https://github.com/cornerstonejs/cornerstoneTools/issues/1121
however, I don't want to access DOM elements to add the event listener to.
It's also clear that events are accessible in Cornertsone.js:
https://github.com/cornerstonejs/cornerstoneTools/blob/master/src/events.js
... but I'm still not sure where to place the event listener ?
Any help would be much appreciated.
Thanks.

Listening to events in ReactJS might be confusing for someone new, even more, when using a new tool such as CornerstoneJS. However, you can create an event listener by using the window.addEventListener method, just like you would in a Vanilla JavaScript. Note that this might change from browser to mobile environments. Moreover, your cornerstoneFileImageLoader can also be a challenge.
For this purpose, you can follow the structure:
window.addEventListener('keydown', (event) => {
...
});
But now, we need to understand "where" to place it. Imagine that all of your pages are just components, as ReactJS is a component-based system. Meaning that the event listener need to happen inside the component.
For instance, you can do like that:
import React from 'react';
const App = (props) => {
window.addEventListener('keydown', (event) => {
...
});
return (
<div className='container'>
<h1>Welcome to the Keydown Listening Component</h1>
</div>
);
};

Related

Is there any memory problem that "Too many active WebGL contents." keeps coming out in my web console

Now I'm using RTSP stream with jsmpeg, ffmpeg, node-rtsp-stream in React.js
I set multiple streaming in react component about 1 to maximum 25
and when I change route to many , It apeard my console
I'm a little afraid of if it caused by memory leak problem
I cannot figure out it is okay or critical problem
Could you tell me how to solve this problem or is it fine
// out of component
const streams: Streams = {};
...
const { preset } = props;
const elementsRef = useRef<any>(new Array(preset?.partition).fill(0).map((v) => createRef()));
// Mount
useEffect(() => {
const { JSMpeg } = window;
const sortedStream = _.sortBy(preset?.stream, ["position"]);
console.log(sortedStream);
sortedStream.forEach((v, i: number) => {
const player = new JSMpeg.Player(v.camera_monitoring.ws, {
canvas: elementsRef?.current[v.position]?.current, // Canvas should be a canvas DOM element
});
console.dir(elementsRef?.current[v.position]?.current);
streams[v.id] = player;
});
}, []);
// UnMount
useEffect(() => {
return () => {
Object.keys(streams).forEach((v) => {
console.log("unmount key:", v);
if (streams[v] !== null) {
streams[v].destroy();
streams[v] = null;
}
});
};
}, []);
...
https://github.com/phoboslab/jsmpeg
above library `jsmpeg.min.js` is set by global ( in public directory and set in my html )
actually my code are so many antipattern in contrast react style, To make an excuse, It is my limits of one's ability

Execute Function when a State Variable Changes inside of a useEffect() Hook

so I am trying to create a graph visualization front-end using Antv's G6 and React. I have this useState() variable and function as shown below:
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graph.findById(hideNode);
node.hide();
};
The function is in charge of hiding the selected node. However, the problem with running this function as it is, is that it will raise the error TypeError: Cannot read properties of null (reading 'findById') because graph is assigned inside of the useEffect() hook, as shown below:
useEffect(() => {
if (!graph) {
graph = new G6.Graph();
graph.data(data);
graph.render();
hideN();
}
}, []);
It only works as intended if I call the function hideN() inside of the useEffect() hook, otherwise outside of the useEffect() if I console.log(graph) the result would be undefined.
So I wanted to ask, is there a way I could have this function run when the state changes while inside of the useEffect(), or is there a better way to go about this. I'm sorry I am super new to React so still learning the best way to go about doing something. I'd appreciate any help you guys can provide.
Full code:
import G6 from "#antv/g6";
import React, { useEffect, useState, useRef } from "react";
import { data } from "./Data";
import { NodeContextMenu } from "./NodeContextMenu";
const maxWidth = 1300;
const maxHeight = 600;
export default function G1() {
let graph = null;
const ref = useRef(null);
//Hide Node State
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graph.findById(hideNode);
node.hide();
};
useEffect(() => {
if (!graph) {
graph = new G6.Graph(cfg);
graph.data(data);
graph.render();
hideN();
}
}, []);
return (
<div>
<div ref={ref}>
{showNodeContextMenu && (
<NodeContextMenu
x={nodeContextMenuX}
y={nodeContextMenuY}
node={nodeInfo}
setShowNodeContextMenu={setShowNodeContextMenu}
sethideNode={sethideNode}
/>
)}
</div>
</div>
);
}
export { G1 };
Store graph in a React ref so it persists through rerenders. In hideN use an Optional Chaining operator on graphRef.current to call the findById function.
Add hideNode state as a dependency to the useEffect hook and move the hideN call out of the conditional block that is only instantiating a graph value to store in the ref.
const graphRef = useRef(null);
const ref = useRef(null);
//Hide Node State
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graphRef.current?.findById(hideNode);
node.hide();
};
useEffect(() => {
if (!graphRef.current) {
graphRef.current = new G6.Graph(cfg);
graphRef.current.data(data);
graphRef.current.render();
}
hideN();
}, [hideNode]);

Lazy Hydrate + Code Splitting on NextJS app

I know how to do Lazy Hydration and I know how to do Code Splitting, but how can I make the splitted chunck download only when the component is hydrating?
My code looks like this
import React from 'react';
import dynamic from 'next/dynamic';
import ReactLazyHydrate from 'react-lazy-hydration';
const MyComponent = dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));
export const PageComponent = () => {
return (
...
<ReactLazyHydrate whenVisible>
<MyComponent/>
</ReactLazyHydrate>
...
);
};
MyComponent is rendered below the fold, which means that it is only gonna hydrate when the user scrolls. The problem is that the JS chunck for MyComponent will be downloaded right away when the page loads.
I was able to hack it by using the dynamic import only on client but this makes the component disappear for a second when it hydrates, because the html rendered on server will not be used by react. It will recreate the DOM element and it will be empty until the JS chunck loads.
When the element disappear for a sec it increases the page CLS and that's the main reason why I can not use this hack.
Here is the code for this hack
const MyComponent = typeof window === 'undefined'
? require('components/my-component').MyComponent
: dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));
Note that I want to render the component's HTML on the server render. That't why I don't want to Lazy Load it. I want to Lazy Hydrate so I can have the component's HTML rendered on server but only download
and execute it's JS when it is visible.
Update:
In document:
// stops preloading of code-split chunks
class LazyHead extends Head {
getDynamicChunks(files) {
const dynamicScripts = super.getDynamicChunks(files);
try {
// get chunk manifest from loadable
const loadableManifest = __non_webpack_require__(
'../../react-loadable-manifest.json',
);
// search and filter modules based on marker ID
const chunksToExclude = Object.values(loadableManifest).filter(
manifestModule => manifestModule?.id?.startsWith?.('lazy') || false,
);
const excludeMap = chunksToExclude?.reduce?.((acc, chunks) => {
if (chunks.files) {
acc.push(...chunks.files);
}
return acc;
}, []);
const filteredChunks = dynamicScripts?.filter?.(
script => !excludeMap?.includes(script?.key),
);
return filteredChunks;
} catch (e) {
// if it fails, return the dynamic scripts that were originally sent in
return dynamicScripts;
}
}
}
const backupScript = NextScript.getInlineScriptSource;
NextScript.getInlineScriptSource = (props) => {
// dont let next load all dynamic IDS, let webpack manage it
if (props?.__NEXT_DATA__?.dynamicIds) {
const filteredDynamicModuleIds = props?.__NEXT_DATA__?.dynamicIds?.filter?.(
moduleID => !moduleID?.startsWith?.('lazy'),
);
if (filteredDynamicModuleIds) {
// mutate dynamicIds from next data
props.__NEXT_DATA__.dynamicIds = filteredDynamicModuleIds;
}
}
return backupScript(props);
};
in next config
const mapModuleIds = fn => (compiler) => {
const { context } = compiler.options;
compiler.hooks.compilation.tap('ChangeModuleIdsPlugin', (compilation) => {
compilation.hooks.beforeModuleIds.tap('ChangeModuleIdsPlugin', (modules) => {
const { chunkGraph } = compilation;
for (const module of modules) {
if (module.libIdent) {
const origId = module.libIdent({ context });
// eslint-disable-next-line
if (!origId) continue;
const namedModuleId = fn(origId, module);
if (namedModuleId) {
chunkGraph.setModuleId(module, namedModuleId);
}
}
}
});
});
};
const withNamedLazyChunks = (nextConfig = {}) => Object.assign({}, nextConfig, {
webpack: (config, options) => {
config.plugins.push(
mapModuleIds((id, module) => {
if (
id.includes('/global-brand-statement.js')
|| id.includes('signposting/signposting.js')
|| id.includes('reviews-container/index.js')
|| id.includes('why-we-made-this/why-we-made-this.js')
) {
return `lazy-${module.debugId}`;
}
return false;
}),
);
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
return config;
},
});
In file, using next/dynamic
<LazyHydrate whenVisible style={null} className="col-xs-12">
<GlobalBrandStatement data={globalBrandData} />
</LazyHydrate>
Not sure if this is what you’re after, but I use lazy hydration mixed with webpack plugin and custom next head to preserve the html but strip out below the fold dynamic imported scripts. So I only download the JS and hydrate the component just before the user scrolls into view. Regardless of it the component was used during render - I don’t need the runtime unless a user is going to see it.
Currently in production and has reduced initial page load by 50%. No impact to SEO
Get me on twitter #scriptedAlchemy if you need the implementation, I’ve not yet written a post about it - but I can tell you it’s totally possible to achieve this “download as you scroll” design with very little effort.
import { useState } from "react";
import dynamic from "next/dynamic";
const MyComponent = dynamic(() => import("components/my-component"));
export const PageComponent = () => {
const [downloadComp, setDownloadComp] = useState(false);
return (
<>
<div className="some-class-name">
<button onClick={() => setDownloadComp(true)}>
Download the component
</button>
{downloadComp && <MyComponent />}
</div>
</>
);
};
The above code will download the once you hit the button. If you want it to download if your scroll to position in that case you can use something like react-intersection-observer to set the setDownloadComp. I don't have experience using react-lazy-hydration but I have been using react-intersection-observer and nextjs dynamic import to lazy load components that depends on user scroll.
I have made a library to make this thing simple. And you can benefit with:
Fully HTML page render (Better for SEO)
Only load JS and Hydrate when needed (Better for PageSpeed)
How to use it
import lazyHydrate from 'next-lazy-hydrate';
// Lazy hydrate when scroll into view
const WhyUs = lazyHydrate(() => import('../components/whyus'));
// Lazy hydrate when users hover into the view
const Footer = lazyHydrate(
() => import('../components/footer', { on: ['hover'] })
);
const HomePage = () => {
return (
<div>
<AboveTheFoldComponent />
{/* ----The Fold---- */}
<WhyUs />
<Footer />
</div>
);
};
Read more: https://github.com/thanhlmm/next-lazy-hydrate

How to call react function from external JavaScript file

I have read this post [ https://brettdewoody.com/accessing-component-methods-and-state-from-outside-react/ ]
But I don't understand.
that is not working on my source code.
it's my tsx file
declare global {
interface Window {
fn_test(): void;
childComponent: HTMLDivElement; <-- what the... ref type????
}
}
export default function Contact(): React.ReactElement {
....
function file_input_html( i:number ): React.ReactElement {
return (
<form id={`frm_write_file_${i}`} .... </form>
)
}
....
return (
<div ref={(childComponent) => {window.childComponent = childComponent}}>
....
)
it's my external javascript file
function fn_test(){
window.childComponent.file_input_html(3)
var element = document.getElementById("ifrm_write_file");
// element.value = "mystyle";
}
How can i call file_input_html function?
plase help me ...
You have some logic here that doesn't completely make sense.
In your class, you define file_input_html, which returns a component.
Then, in fn_test, you call attempt to call that function (which doesn't work -- I'll address that in a minute), but you don't do anything with the output.
The article that you linked to tells you how to get a ref to a component (eg the div in this case) -- not the actual Contact, which doesn't have a property named file_input_html anyway -- that's just a function you declared inside its scope.
What I'm assuming you want to happen (based on the code you shared) is for your external javascript file to be able to tell your component to render a form with a certain ID and then be able to get a reference to it. Here's an example of how to do this (it's a little convoluted, but it's a funny situation):
const { useState } = React
const App = (props) => {
const [formId, setFormId] = useState(2)
useEffect(() => {
window.alterFormId = setFormId
},[])
return (<div id={"form" + formId} ref={(ourComponent) => {window.ourComponent = ourComponent}}>
Text {formId}
</div>);
}
setTimeout(() => {
window.alterFormId(8);
setTimeout(() => {
console.log(window.ourComponent)
window.ourComponent.innerText = "Test"
}, 20)
}, 1000)
ReactDOM.render(<App />,
document.getElementById("root"))
What's happening here:
In useEffect, I set alterFormId on window so that it can be used outside of the React files
Using the technique you linked to, I get a ref to the div that's created. Note that I'm setting the ID here as well, based on the state of formId
The setTimeout function at the end tests all this:
a) It waits until the first render (the first setTimeout) and then calls alterFormId
b) Then, it waits again (just 20ms) so that the next run loop has happened and the React component has re-rendered, with the new formId and reference
c) From there, it calls a method on the div just to prove that the reference works.
I'm not exactly sure of your use case for all this and there are probably easier ways to architect things to avoid these issues, but this should get you started.
안녕하세요. 자바스크립트로 흐름만 알려드리겠습니다
아래 코드들을 참고해보세요.
iframe간 통신은
window.postMessage API와
window.addEventListener('message', handler) 메시지 수신 이벤트 리스너 로 구현할 수있습니다. 보안관련해서도 방어로직이 몇줄 필요합니다(origin 체크 등)
in parent
import React from 'react';
export function Parent () {
const childRef = React.useRef(null);
const handleMessage = (ev) => {
// 방어로직들
if (check ev.origin, check ev.source, ....) {
return false;
}
console.log('handleMessage(parent)', ev)
}
React.useEffect(() => {
window.addEventListener('message', handleMessage);
// clean memory
return () => {
window.removeEventListener('message', handleMessage);
}
})
return (
<div>
<iframe ref="childRef" src="child_src" id="iframe"></iframe>
</div>
)
}
in child
import React from 'react';
export function Iframe () {
const handleMessage = (ev) => {
console.log('handleMessage(child)', ev)
}
const messagePush = () => {
window.parent.postMessage({ foo: 'bar' }, 'host:port')
}
React.useEffect(() => {
window.addEventListener('message', handleMessage);
// clean memory
return () => {
window.removeEventListener('message', handleMessage);
}
})
return (
<div>
<button onClick={messagePush}>Push message</button>
</div>
)
}

NextJS Google Translate Widget

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

Resources