How to use getElementsByClassName on dynamic html - javascript

What I'm trying to do is to get HTML tag by className on dynamic HTML that I fetched,
but it returns undefined. It works if I try to getElementByClassName("main-page") because that class isn't dynamic
const HomePage = () => {
const [pageData, setPageData] = useContext(PageContext)
useEffect(() => {
const allImages = document.getElementsByClassName("wp-block-column")
console.log([...allImages])
}, [])
//render fronpage
const renderMainPage = () => {
//map the data and check for the site url (www.siteurl.com = front page)
if (pageData) {
return pageData.map(page => {
if (window.location.origin + "/" === page.link) {
return <section dangerouslySetInnerHTML={{ __html: page.content.rendered }}></section>
}
})
}
}
return (
<h1 className="main-page"">{renderMainPage()}</h1>
)
}
export default HomePage

From what I see the problem in pageData in context, probably your default value to context's pageData is false and renderMainPage() is not going further then first "if" statement
But its not preferred to use document selectors, use refs in React instead.
Also from naming I see that you are trying to get images and not dom nodes, but with this logic you are going to get dom nodes. I'm sure there is better way/flow to access images you need than looking for them in nodes.

const elementsRef = useRef(data.map(() => createRef()));
you can you dynamic create ref to access or create random iniNumber Array then assing to you elements when select them

You can use this function that useLayoutEffect, you can modify your code like this,
useLayoutEffect(()=> {
document.getElementsByClassName("yourclassname")
})

Related

React, component not re-rendering after change in an array state (not the same as others)

I'm trying to make a page that gets picture from a server and once all pictures are downloaded display them, but for some reason the page doesn't re-render when I update the state.
I've seen the other answers to this question that you have to pass a fresh array to the setImages function and not an updated version of the previous array, I'm doing that but it still doesn't work.
(the interesting thing is that if I put a console.log in an useEffect it does log the text when the array is re-rendered, but the page does not show the updated information)
If anyone can help out would be greatly appreciated!
Here is my code.
export function Profile() {
const user = JSON.parse(window.localStorage.getItem("user"));
const [imgs, setImages] = useState([]);
const [num, setNum] = useState(0);
const [finish, setFinish] = useState(false);
const getImages = async () => {
if (finish) return;
let imgarr = [];
let temp = num;
let filename = "";
let local = false;
while(temp < num+30) {
fetch("/get-my-images?id=" + user.id + "&logged=" + user.loggonToken + "&num=" + temp)
.then(response => {
if(response.status !== 200) {
setFinish(true);
temp = num+30;
local = true;
}
filename = response.headers.get("File-Name");
return response.blob()
})
.then(function(imageBlob) {
if(local) return;
const imageObjectURL = URL.createObjectURL(imageBlob);
imgarr[temp - num] = <img name={filename} alt="shot" className="img" src={imageObjectURL} key={temp} />
temp++;
});
}
setNum(temp)
setImages(prev => [...prev, ...imgarr]);
}
async function handleClick() {
await getImages();
}
return (
<div>
<div className="img-container">
{imgs.map(i => {
return (
i.props.name && <div className="img-card">
<div className="img-tag-container" onClick={(e) => handleView(i.props.name)}>{i}</div>
<div className="img-info">
<h3 className="title" onClick={() => handleView(i.props.name)}>{i.props.name.substr(i.props.name.lastIndexOf("\\")+1)}<span>{i.props.isFlagged ? "Flagged" : ""}</span></h3>
</div>
</div>
)
})}
</div>
<div className="btn-container"><button className="load-btn" disabled={finish} onClick={handleClick}>{imgs.length === 0 ? "Load Images" : "Load More"}</button></div>
</div>
)
}
I think your method of creating the new array is correct. You are passing an updater callback to the useState() updater function which returns a concatenation of the previous images and the new images, which should return a fresh array.
When using collection-based state variables, I highly recommend setting the key property of rendered children. Have you tried assigning a unique key to <div className="img-card">?. It appears that i.props.name is unique enough to work as a key.
Keys are how React associates individual items in a collection to their corresponding rendered DOM elements. They are especially important if you modify that collection. Whenever there's an issue with rendering collections, I always make sure the keys are valid and unique. Even if adding a key doesn't fix your issue, I would still highly recommend keeping it for performance reasons.
It is related to Array characteristics of javascript.
And the reason of the console log is related with console log print moment.
So it should be shown later updated for you.
There are several approaches.
const getImages = async () => {
... ...
setNum(temp)
const newImage = [...prev, ...imgarr];
setImages(prev => newImage);
}
const getImages = async () => {
... ...
setNum(temp)
setImages(prev => JOSN.parse(JSON.object([...prev, ...imgarr]);
}
const getImages = async () => {
... ...
setNum(temp)
setImages(prev => [...prev, ...imgarr].slice(0));
}
Maybe it could work.
Hope it will be helpful for you.
Ok the problem for me was the server was not sending a proper filename header so it was always null so the condition i.props.name was never true... lol sorry for the confusion.
So the moral of this story is, always make sure that it's not something else in your code that causes the bad behavior before starting to look for other solutions...

Replace href link with a JS function

In my react electron app, that it is working with an API, I receive JSON values to display data into the components. So for example I have a Features component:
const Features = () => {
const { title } = useSelector(({ titles }) => titles);
let string = title.features;
// the string can contain some html tags. Example bellow:
// sting = 'This is a string containing a href to Google';
string = string.replace(/href="(.*?)"/g, function() {
return `onClick="${() => shell.openExternal('www.google.com')}"`;
});
return (
<>
<Heading>Features</Heading>
<Text content={parsedHTML} />
</>
);
};
What I want is to replace the href attribute with onClick and assign Electron's shell.openExternal() function.
The string.replace() callback function does that, but when I click on the <a> element, the app throws next error:
error: uncaughtException: Expected onClick listener to be a
function, instead got a value of string type.
UPDATE
Also tried this logic and the same error occurs:
global.openInBrowser = openInBrowser; // openInBrowser is basically a function that calls shell.openExternal(url)
const re = new RegExp('<a([^>]* )href="([^"]+)"', 'g');
string = string.replace(re, '<a$1href="#" onClick="openInBrowser(\'$2\')"');
Here's a link to Sandbox rep.
How do I do this correctly?
The onclick not being set on the React element is actually expected behavior.
Because there's an XSS security risk when evaling the onclick string. The recommended solution is to use the replace option in html-react-parser.
you can also use dangerouslySetInnerHTML which involves security risk.
Sandbox Demo
export default function App() {
let string =
'This is a string containing html link to Google';
const re = new RegExp('<a([^>]* )href="([^"]+)"', "g");
let replaced = string.replace(re, "<a onclick=\"alert('$2')\"");
return (
<div className="App">
<p dangerouslySetInnerHTML={{__html: replaced}}></p>
<p>{parse(string, {
replace: domNode => {
if (domNode.name === 'a') {
let href = domNode.attribs.href
domNode.attribs.onClick = () => { alert(href) }
delete domNode.attribs.href
}
}
})}</p>
</div>
);
}
Not sure on specifics of electron, but passing a (function(){functionName()})() would not work in html if there is no functionName variable available on window scope. Being there is a global environment in electron this might answer your question:
const Features = () => {
const { title } = useSelector(({ titles }) => titles);
let string = title.features;
// the string can contain some html tags. Example bellow:
// sting = 'This is a string containing a href to Google';
function runOpen(href){
shell.openExternal(href)
}
global.runOpen = runOpen;
string = string.replace(/href="(.*?)"/g, function() {
return `onClick="runOpen(${'www.google.com'})"`;
});
return (
<>
<Heading>Features</Heading>
<Text content={parsedHTML} />
</>
);
};
if it doesnt you can use something like onclick="console.log(this)" to find out what is the scope the onclick runs in and futher assign your runOpen variable there.

How can I make some text from a json string bold?

I want to concatenate two strings in react such that the first displays bold and the second does not. I have the string I want bolded in a JSON file and I have the string I want to concatenate coming from backend API call. Here's my setup:
This is in a JSON file:
{ stuff: {
stuffIWantBolded: "bold text"
}
}
And my frontend looks like this:
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
const textIWantToDisplay = `${stuff.stuffIWantBolded} ${theStringFromBackend}`;
return (
<SomeComponent someProp={textIWantToDisplay} />
);
};
That concatenates the two strings successfully. I've tried using .bold() at the end of stuff.stuffIWantBolded, but that apparently doesn't work, because then the string renders as <b>bold text</b> the string from backend (assuming that's what the string from backend says), with the HTML tags written out explicitly instead of rendering actual bold text. Is there something I'm missing? I don't think one can just make the string bold in the JSON...perhaps I need to use a regex? Any help would be greatly appreciated.
How about this:
return (
<>
<span style={{fontWeight: "bold"}}>{stuff.stuffIWantBolded}</span>
<span>{theStringFromBackend}</span>
</>
);
The <> and </> effectively allow you to return multiple items from one render function.
I would do it by using composition (I prefer the strong tag to b to make text bold):
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
return (
<SomeComponent>
<p><strong>{stuff.stuffIWantBolded}</strong> {theStringFromBackend}</p>
</SomeComponent>
);
};
And in SomeComponent you will find the nested html inside props.children
for more info check here: https://reactjs.org/docs/composition-vs-inheritance.html
It turns out you CAN pass in a prop within a self-closing tag component the way I want to. Not sure if it's conventionally sound/overly bloated but it looks like this:
render() {
// props for this component in which I'm rendering SomeComponent (see below)
const { data } = this.props
const { theStringFromBackend } = data
// a method to get the string that is working for me
const stuff = this.getTheStringFromTheJSON();
const theBoldedText = `${stuff.stuffIWantBolded}`;
return (
<SomeComponent someProp={<span><b>{theBoldedText}</b> <span>{theStringFromBackend}</span></span>} />
);
};

Replace img tags with Gallery in React

There is problem with render gallery component:
I get string with html from server
let serverResponse = `
<h3>Some title</h3>
<p>Some text</p>
<p>
<img src="">
<img src="">
<img src="">
<br>
</p>
...
`
Now I render this response with dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: serverResponse }} />
But when I got 2 or more repeating <img> tags I want to replace them with component.
How can I do that? I tried to do it with Regex and replace them with <Gallery/> but it doesn't work. I think that I need split string in array of tags and then replace images with <Gallery/> component.
I tried do it with renderToString
...
getGallery = images => {
// Loop throw images and get sources
let sources = [];
if (images) {
images.map(img => {
let separatedImages = img.match(/<img (.*?)>/g);
separatedImages.map(item => sources.push(...item.match(/(https?:\/\/.*\.(?:png|jpg))/)));
});
}
if (sources.length) {
return <Gallery items={sources}>
}
return <div/>
};
...
<div dangerouslySetInnerHTML={{__html: serverResponse.replace(/(<img (.*?)>){2,}/g,
renderToString(this.getGallery(serverResponse.match(/(<img (.*?)>){2,}/g))))}}/>}
And this doesn't work, because I get just html without logic :(
Fist of all, dangerouslySetInnerHTML is not the way to go, you can't insert gallery into in it and have it processed by React. What you need to do is multistep procedure.
1. Parse HTML into document. In this stage you will convert string to valid DOM document. This is very easy to do with DOMParser:
function getDOM (html) {
const parser = new DOMParser()
const doc = parser.parseFromString(`<div class="container">${html}</div>`, 'text/html')
return doc.querySelector('.container')
}
I make this helper function to return container with your HTML nodes. It will be need in the next step.
2. Transform DOM document into React JSX tree. Now that you have DOM tree it's very easy to convert it to JSX by creating individual React elements out of corresponding DOM nodes. This function needs to be recursive to process all levels of the DOM tree. Something like this will do:
function getJSX(root) {
return [...root.children].map(element => {
const children = element.children.length ? getJSX(element) : element.textContent
const props = [...element.attributes].reduce((prev, curr) => ({
...prev,
[curr.name]: curr.value
}), {})
return React.createElement(element.tagName, props, children)
})
}
This is enough to create JSX out of DOM. It could be used like this:
const JSX = getJSX(getDOM(htmlString))
3. Inject Gallery. Now you can improve JSX creation to inject Gallery into created JSX if element contains more then 1 image tag. I would pass inject function into getJSX as the second parameter. The only difference from above version would be is how children is calculated in gallery case:
if (element.querySelector('img + img') && injectGallery) {
const imageSources = [...element.querySelectorAll('img')].map(img => img.src)
children = injectGallery(imageSources)
} else {
children = element.children.length ? getJSX(element) : element.textContent
}
4. Create Gallery component. Now it's time to create Gallery component itself. This component will look like this:
import React from 'react'
import { func, string } from 'prop-types'
function getDOM (html) {
const parser = new DOMParser()
const doc = parser.parseFromString(`<div class="container">${html}</div>`, 'text/html')
return doc.querySelector('.container')
}
function getJSX(root, injectGallery) {
return [...root.children].map(element => {
let children
if (element.querySelector('img + img') && injectGallery) {
const imageSources = [...element.querySelectorAll('img')].map(img => img.src)
children = injectGallery(imageSources)
} else {
children = element.children.length ? getJSX(element) : element.textContent
}
const props = [...element.attributes].reduce((prev, curr) => ({
...prev,
[curr.name]: curr.value
}), {})
return React.createElement(element.tagName, props, children)
})
}
const HTMLContent = ({ content, injectGallery }) => getJSX(getDOM(content), injectGallery)
HTMLContent.propTypes = {
content: string.isRequired,
injectGallery: func.isRequired,
}
export default HTMLContent
5. Use it! Here is how you would use all together:
<HTMLContent
content={serverResponse}
injectGallery={(images) => (
<Gallery images={images} />
)}
/>
Here is the demo of this code above.
Demo: https://codesandbox.io/s/2w436j98n
TLDR: You can use React HTML Parser or similar libraries.
While it looks very alike, JSX get parsed into a bunch of React.createElement so interpolate React component into HTML string will not work. renderToString won't do too because it is used to server-side render React page and will not work in your case.
To replace HTML tags with React component, you need a parser to parse the HTMl string to nodes, map the nodes to React elements and render them. Lucky for you, there are some libraries out there that do just that, like React HTML Parser for example.

How to fast render >10000 items using React + Flux?

I would like to ask what is the correct way to fast render > 10000 items in React.
Suppose I want to make a checkboxList which contain over dynamic 10000 checkbox items.
I make a store which contain all the items and it will be used as state of checkbox list.
When I click on any checkbox item, it will update the corresponding item by action and so the store is changed.
Since store is changed so it trigger the checkbox list update.
The checkbox list update its state and render again.
The problem here is if I click on any checkbox item, I have to wait > 3 seconds to see the checkbox is ticked. I don't expect this as only 1 checkbox item need to be re-rendered.
I try to find the root cause. The most time-consuming part is inside the checkbox list render method, related to .map which create the Checkbox component to form componentList.. But actually only 1 checkbox have to re-render.
The following is my codes.
I use ReFlux for the flux architecture.
CheckboxListStore
The Store store all the checkbox item as map. (name as key, state (true/false) as value)
const Reflux = require('reflux');
const Immutable = require('immutable');
const checkboxListAction = require('./CheckboxListAction');
let storage = Immutable.OrderedMap();
const CheckboxListStore = Reflux.createStore({
listenables: checkboxListAction,
onCreate: function (name) {
if (!storage.has(name)) {
storage = storage.set(name, false);
this.trigger(storage);
}
},
onCheck: function (name) {
if (storage.has(name)) {
storage = storage.set(name, true);
this.trigger(storage);
}
},
onUncheck: function (name) {
if (storage.has(name)) {
storage = storage.set(name, false);
this.trigger(storage);
}
},
getStorage: function () {
return storage;
}
});
module.exports = CheckboxListStore;
CheckboxListAction
The action, create, check and uncheck any checkbox item with name provided.
const Reflux = require('reflux');
const CheckboxListAction = Reflux.createActions([
'create',
'check',
'uncheck'
]);
module.exports = CheckboxListAction;
CheckboxList
const React = require('react');
const Reflux = require('reflux');
const $ = require('jquery');
const CheckboxItem = require('./CheckboxItem');
const checkboxListAction = require('./CheckboxListAction');
const checkboxListStore = require('./CheckboxListStore');
const CheckboxList = React.createClass({
mixins: [Reflux.listenTo(checkboxListStore, 'onStoreChange')],
getInitialState: function () {
return {
storage: checkboxListStore.getStorage()
};
},
render: function () {
const {storage} = this.state;
const LiComponents = storage.map((state, name) => {
return (
<li key = {name}>
<CheckboxItem name = {name} />
</li>
);
}).toArray();
return (
<div className = 'checkbox-list'>
<div>
CheckBox List
</div>
<ul>
{LiComponents}
</ul>
</div>
);
},
onStoreChange: function (storage) {
this.setState({storage: storage});
}
});
module.exports = CheckboxList;
CheckboxItem
Inside onChange callback, I call the action to update the item.
const React = require('react');
const Reflux = require('reflux');
const $ = require('jquery');
const checkboxListAction = require('./CheckboxListAction');
const checkboxListStore = require('./CheckboxListStore');
const CheckboxItem = React.createClass({
mixins: [Reflux.listenTo(checkboxListStore, 'onStoreChange')],
propTypes: {
name: React.PropTypes.string.isRequired
},
getInitialState: function () {
const {name} = this.props;
return {
checked: checkboxListStore.getStorage().get(name)
};
},
onStoreChange: function (storage) {
const {name} = this.props;
this.setState({
checked: storage.get(name)
});
},
render: function () {
const {name} = this.props;
const {checked} = this.state;
return (
<div className = 'checkbox' style = {{background: checked ? 'green' : 'white'}} >
<span>{name}</span>
<input ref = 'checkboxElement' type = 'checkbox'
onChange = {this.handleChange}
checked = {checked}/>
</div>
);
},
handleChange: function () {
const {name} = this.props;
const checked = $(this.refs.checkboxElement).is(':checked');
if (checked) {
checkboxListAction.check(name);
} else {
checkboxListAction.uncheck(name);
}
}
});
module.exports = CheckboxItem;
There are a few approaches you can take:
Don't render all 10,000 - just render the visible check boxes (+ a few more) based on panel size and scroll position, and handle scroll events to update the visible subset (use component state for this, rather than flux). You'll need to handle the scroll bar in some way, either by rendering one manually, or easier by using the normal browser scroll bar by adding huge empty divs at the top and bottom to replace the checkboxes you aren't rendering, so that the scroll bar sits at the correct position. This approach allows you to handle 100,000 checkboxes or even a million, and the first render is fast as well as updates. Probably the preferred solution. There are lots of examples of this kind of approach here: http://react.rocks/tag/InfiniteScroll
Micro-optimize - you could do storage.toArray().map(...) (so that you aren't creating an intermediate map), or even better, make and empty array and then do storage.forEach(...) adding the elements with push - much faster. But the React diffing algorithm is still going to have to diff 10000 elements, which is never going to be fast, however fast you make the code that generates the elements.
Split your huge Map into chunks in some way, so that only 1 chunk changes when you check a chechbox. Also split up the React components in the same way (into CheckboxListChunks) or similar. This way, you'll only need to re-render the changed chunk, as long as you have a PureComponent type componentShouldUpdate function for each chunk (possibly Reflux does this for you?).
Move away from ImmutableJS-based flux, so you have better control over what changes when (e.g. you don't have to update the parent checkbox map just because one of the children has changed).
Add a custom shouldComponentUpdate to CheckboxList:
shouldComponentUpdate:function(nextProps, nextState) {
var storage = this.state.storage;
var nextStorage = nextState.storage;
if (storage.size !== nextStorage.size) return true;
// check item names match for each index:
return !storage.keySeq().equals(nextStorage.keySeq());
}
Beyond the initial render, you can significantly increase rendering speed of large collections by using Mobservable. It avoids re-rendering the parent component that maps over the 10.000 items unnecessarily when a child changes by automatically applying side-ways loading. See this blog for an in-depth explanation.
Btw, I give up flux...
I finally decided to use mobservable to solve my problem.
I have made an example https://github.com/raymondsze/react-example
see the https://github.com/raymondsze/react-example/tree/master/src/mobservable for the coding.
Your render function looks somewhat more complicated then it needs to be:
it first generates an array of JSX components
then converts applies a (jQuery?) .toArray()
then returns this newly generated array.
Maybe simplifying your render function to something like this would help?
render: function () {
return (
<div className = 'checkbox-list'>
<div>
CheckBox List
</div>
<ul>
{this.state.storage.map((state, name) => {
return (
<li key = {name}>
<CheckboxItem name = {name} />
</li>
);
})}
</ul>
</div>
);
},
Do you really need to save the check status in you store every time check/uncheck?
I recently meet similar problem like you. Just save a checkedList array [name1,name2 ...] in the CheckboxList component's state, and change this checkedList every time you check/uncheck an item. When you want to save check status to data storage, call an Action.save() and pass the checkedList to store.
But if you really need to save to data storage every time check/uncheck, this solution won't help -_-.

Categories

Resources