React createProtal called outsite a JSX component not updating the DOM - reactjs

I am trying to render a dynamically generated react component in a react app using createProtal.
When I call createProtal from a class the component is not rendered.
Handler.ts the class the contains the business logic
export class Handler {
private element: HTMLElement | null;
constructor(selector: string) {
this.element = document.getElementById(selector);
}
attachedEvent() {
this.element?.addEventListener("mouseenter", () => {
let cancel = setTimeout(() => {
if (this.element != null)
this.attachUi(this.element)
}, 1000)
this.element?.addEventListener('mouseleave', () => {
clearTimeout(cancel)
})
})
}
attachUi(domNode: HTMLElement) {
createPortal(createElement(
'h1',
{className: 'greeting'},
'Hello'
), domNode);
}
}
Main.tsx the react component that uses Handler.ts
const handler = new Handler("test_comp");
export default function Main() {
useEffect(() => {
// #ts-ignore
handler.useAddEventListeners();
});
return (
<>
<div id="test_comp">
<p>Detect Mouse</p>
</div>
</>
)
}
However when I repleace attachUi function with the function below it works
attachUi(domNode: HTMLElement) {
const root = createRoot(domNode);
root.render(createElement(
'h1',
{className: 'greeting'},
'Hello'
));
}
What am I missing?

React uses something called Virtual DOM. Only components that are included in that VDOM are displayed to the screen. A component returns something that React understands and includes to the VDOM.
createPortal(...) returns exactly the same as <SomeComponent ... />
So if you just do: const something = <SomeComponent /> and you don't use that variable anywhere, you can not display it. The same is with createPortal. const something = createPortal(...). Just use that variable somewhere if you want to display it. Add it to VDOM, let some of your components return it.
Your structure is
App
-children
-grand children
-children2
And your portal is somewhere else, that is not attached to that VDOM. You have to include it there, if you want to be displayed.
In your next example using root.render you create new VDOM. It is separated from your main one. This is why it is displayed

Related

How do you rerender a React component when an object's property is updated?

I have an object in my React application which is getting updated from another system. I'd like to display the properties of this object in a component, such that it updates in real time.
The object is tracked in my state manager, Jotai. I know that Jotai will only re-render my component when the actual object itself changes, not its properties. I'm not sure if that is possible.
Here is a sample that demonstrates my issue:
import React from "react";
import { Provider, atom, useAtom } from "jotai";
const myObject = { number: 0 };
const myObjectAtom = atom(myObject);
const myObjectPropertyAtom = atom((get) => {
const obj = get(myObjectAtom)
return obj.number
});
const ObjectDisplay = () => {
const [myObject] = useAtom(myObjectAtom);
const [myObjectProperty] = useAtom(myObjectPropertyAtom);
const forceUpdate = React.useState()[1].bind(null, {});
return (
<div>
{/* This doesn't update when the object updates */}
<p>{myObject.number}</p>
{/* This doesn't seem to work at all. */}
<p>{myObjectProperty}</p>
{/* I know you shouldn't do this, its just for demo */}
<button onClick={forceUpdate}>Force Update</button>
</div>
);
};
const App = () => {
// Update the object's property
setInterval(() => {
myObject.number += 0.1;
}, 100);
return (
<Provider>
<ObjectDisplay />
</Provider>
);
};
export default App;
Sandbox
you can use useEffect for this.
useEffect(()=> {
// code
}, [myObject.number])

How should I update individual items' className onClick in a list in a React functional component?

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

Create Dynamic Components

I want to dynamically create a component, when I implement something like this:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = () => {
const Comp = gen_Comp("Hello");
return (
<Comp txt="World" />
);
}
Something goes wrong (what exactly goes wrong is hard to explain because it's specific to my app, point is that I must be doing something wrong, because I seem to be losing state as my component gets rerendered). I also tried this with React.createElement, but the problem remains.
So, what is the proper way to create components at runtime?
The main way that react tells whether it needs to mount/unmount components is by the component type (the second way is keys). Every time App renders, you call gen_Comp and create a new type of component. It may have the same functionality as the previous one, but it's a new component and so react is forced to unmount the instance of the old component type and mount one of the new type.
You need to create your component types just once. If you can, i recommend you use your factory outside of rendering, so it runs just when the module loads:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const Comp = gen_Comp("Hello");
const App = () => {
return (
<Comp txt="World" />
);
}
If it absolutely needs to be done inside the rendering of a component (say, it depends on props), then you will need to memoize it:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = ({ spec }) => {
const Comp = useMemo(() => {
return gen_Comp(spec);
}, [spec]);
return (
<Comp txt="World" />
);
}

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>
)
}

Resources