Dynamically created element is not firing click event in React [closed] - javascript

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I have many element called infoopts which are just a html i tags, which i attach an event listener to on click so that i can change their style (background color). These are being generated dynamically when i click on a button. I've tried a million things but for some reason none of them will fire the click event.
Here's what i tried:
useEffect(() => {
const infoopts = document.querySelectorAll('.infoopts')
infoopts.forEach(el => {
el.addEventListener('click', function(e) {
console.log('clicked') //never logs this
el.style.background = 'red'
})
})
},[])
console is not even logging the click event. This is supposed to be an amateur issue. Why isn't it creating the click event?
2 Reasons why i am not using React:
I want to style also its neighbor element, onClick will only style that element
If i use state it will change ALL my elements that has infoopts class, i just want to style the clicked one and its neighbor (state is global)

The problem with the code in your question is that .infoopts might not even exist when this component is mounted. In that case, querySelectorAll would find nothing and your event handler would never fire because it was never attached to anything.
Without knowing the source of your data, or what your component hierarchy looks like, below is one possible example of a naive implementation.
Hold onto some local component state to determine which item in the list is selected, and then render the UI from that state.
The rendered DOM nodes are simply a reflection of the component's state. To update any of the DOM nodes, simply update that state.
import React, { useState } from "react";
export default function App() {
const [items, setItems] = useState([
{ title: "foo", selected: false },
{ title: "bar", selected: false },
{ title: "buzz", selected: false },
{ title: "foo", selected: false },
{ title: "bar", selected: false },
{ title: "buzz", selected: false },
{ title: "foo", selected: false },
{ title: "bar", selected: false },
{ title: "buzz", selected: false }
]);
const handleClick = (i) => {
const updated = [...items];
updated[i].selected = !items[i].selected;
setItems(updated);
};
const determineBgColor = (i) => {
const itemIsSelected = items[i].selected;
const neighbourIsSelected =
items[i - 1]?.selected || items[i + 1]?.selected;
if (itemIsSelected) {
return "darkred";
} else if (neighbourIsSelected) {
return "bisque";
}
return "darksalmon";
};
return (
<div className="App">
<ul>
{items.map((item, i) => (
<li key={i}>
<button
style={{ background: determineBgColor(i) }}
onClick={() => handleClick(i)}
>
{item.title}
</button>
</li>
))}
</ul>
</div>
);
}
Alternatively, the state might already be kept in a parent component. In that case it might be passed into this component as a prop, and this component is simply rendering based on the value of the data in that prop.
Then you would need to find a way to update the state in the parent component, and the changes will flow from the parent, to the child, and the rendered DOM will update accordingly.
Either way, the same principles apply. When using React you should aim to be writing declarative components that react to their state & props, as opposed to attempting to imperatively affect the DOM yourself.
I highly recommend that you read https://reactjs.org/docs/thinking-in-react.html if you haven't already.

Related

Using react components to render data controlled by third party library

I'm building a React component that needs to consume data from a tree library built in vanilla JS. This library holds and manages data for all "tree nodes" and they're state - expanded/collapsed, selected, hidden, etc.
I'm unsure how to approach building react components because they ideally control their own state or use a store designed for use in react.
Here's a super simple example of data that might be loaded into the tree library.
[{
id: 1,
text: 'Node 1'
}, {
id: 2
text: 'Node 2',
state: {
selected: true
}
}]
It gets loaded into the tree lib via the constructor new Tree(nodes); and the tree lib provides a ton of methods to work with it: tree.deselect(2) and tree.selected() // -> []
I've toyed around with some basic components to render this example:
I start with <TreeNodes nodes={tree.nodes()} />
const TreeNodes = ({ nodes }: TreeNodesProps) => {
return (<>{ nodes.map(node => <TreeNode key={node.id} node={node} />) }</>);
}
const TreeNode = ({ node }: TreeNodeProps) => {
const onClick = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
node.toggleSelect();
}
return <div className={clsx({ selected: node.selected()})} onClick={onClick}>{node.text}</div>
}
The tree library fires events like node.selected to let me know when something has changed in the data.
My question is, what's the best/proper way to then sync my data to react components?
I was debating listening for all tree events and updating a state object in the root component but that feels wrong:
const [nodes, setNodes] = useState(tree.nodes());
this.tree.on('node.selected', () => {
setNodes(tree.nodes())
});
I honestly don't feel adding a listener is wrong as long as it works fine. Also I think you should wrap it up in a useEffect this way:
function MyApp() {
const [nodes, setNodes] = useState(tree.nodes());
useEffect(() => {
tree.on("node.selected", () => {
setNodes(tree.nodes());
});
return () => {/* remove listener here */}
}, []);
}
You either have this solution or you will have to make all changes to the data by yourself.

Reducing renders in child components of a context

I'm trying to delve deeper than the basics into React and am rebuilding a Tree library I had written in plain JS years back. I need to expose an API to users so they can programmatically add/remove nodes, select nodes, etc.
From what I've learned, a ref and context is a good approach. I've built a basic demo following the examples (without ref, for now) but I'm seeing every single tree node re-render when a selection is made, even though no props have changed for all but one.
I've tried a few things like memoizing my tree node component, etc but I feel like I'm failing to understand what's causing the re-render.
I'm using the react dev tools to highlight renders.
Here's a codesandbox demo.
My basic tree node component. I essentially map this for every node I need to show. On click, this calls select() from my context API. The rerender doesn't happen if that select() call is disabled.
const TreeNodeComponent = ({ id, text, children }) => {
console.log(`rendering ${id}`);
const { select } = useTreeContext();
const onClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
select([id]);
},
[select, id]
);
return (
<div className="tree-node" onClick={onClick}>
{text}
{children ? <TreeNodesComponent nodes={children} /> : ""}
</div>
);
};
The important part of my context is the provider:
const TreeContextProvider = ({ children, nodes = [], selectedNodes }) => {
const [allNodes] = useState(nodes);
const [allSelectedNodes, setSelectedNodes] = useState(selectedNodes || []);
const api = useMemo(
() => ({
selected: () => allSelectedNodes,
select: (nodes) => {
setSelectedNodes(Array.from(new Set(allSelectedNodes.concat(nodes))));
}
}),
[allSelectedNodes]
);
const value = useMemo(
() => ({
nodes: allNodes,
selectedNodes: allSelectedNodes,
...api
}),
[allNodes, allSelectedNodes, api]
);
return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
};
Again, when the call to setSelectedNodes is disabled, the rerenders don't happen. So the entire state update is triggering the render, yet individual props to all but one tree component do not change.
Is there something I can to improve this? Imagine I have 1000 nodes, I can't rerender all of them just to mark one as selected etc.

React Stale State with Custom Hook Event Handling

It seems I just can't get my head around stale state issues in React as it relates to event handlers and hooks. I conceptually understand what is happening–there is a closure that is capturing the starting value of a state value, and isn't updating when I expect it to.
I am creating a NavBar component onto which I want to add keyboard controls to allow accessibility for sub menus and so forth. I will show the code and then describe what is happening / not happening. I'll also link to a codesandbox for easier forking and debugging.
CodeSandbox
NavBar
const NavBar: React.FC<Props> = ({ children, label }) => {
const {
actions: { createNavItemRef },
state: { activeSubMenuIndex, navBarRef },
} = useNav();
console.log('NAV.BAR', { activeSubMenuIndex });
return (
<nav aria-label={label} ref={navBarRef}>
<NavList aria-label={label} role="menubar">
{children} // NavItems
</NavList>
</nav>
);
};
NavItem
const NavItem: React.FC<Props> = ({ children, hasSubMenu, to }) => {
const ChildrenArray = React.Children.toArray(children);
const {
actions: { handleSelectSubMenu },
state: { activeSubMenuIndex },
} = useNav();
const handleSubMenuToggle = () => {
handleSelectSubMenu(index);
};
return (
<li ref={ref} role="none">
<>
<ParentButton
aria-expanded={activeSubMenuIndex === index}
aria-haspopup="true"
onClick={handleSubMenuToggle}
role="menuitem"
tabIndex={index === 0 ? 0 : -1}
>
{ChildrenArray.shift()}
</ParentButton>
{ChildrenArray.pop()}
</>
</li>
);
};
UseNav
function useNav() {
const navBarRef = useRef<HTMLUListElement>(null);
const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number | undefined>();
const handleSelectSubMenu = (index?: number) => {
if (!index || activeSubMenuIndex === index) {
setActiveSubMenuIndex(undefined);
} else {
setActiveSubMenuIndex(index);
}
};
useEffect(() => {
const navbar = navBarRef?.current;
navbar?.addEventListener('keydown', () => {
console.log("UseNav", { activeSubMenuIndex });
});
// return () => remove event listener
}, [navBarRef, activeSubMenuIndex]);
return {
actions: {
createNavItemRef,
handleSelectSubMenu,
},
state: {
activeSubMenuIndex,
navBarRef,
},
};
}
This is a somewhat stripped down version of my set up. Ultimately, here's what's going on.
Expectation
I tab onto the first NavItem and it becomes focused. I hit an arrow key (for example) and the log UseNav { activeSubMenuIndex }) logs out correctly as undefined.
Then I click on the NavItem which contains a sub menu. The activeSubMenuIndex updates and in the NavItem the correct sub menu is displayed (based on the activeSubMenuIndex === index conditional).
However, I would expect the NavBar { activeSubMenuIndex }) to log out as well when this NavItem is clicked. But it doesn't.
With the sub menu visible, I hit another arrow key and when the UseNav log is displayed, I would expect it to contain the correct activeSubMenuIndex value, but it is still undefined.
Ultimately, I will need to addEventListeners for keyPress on the NavBar in order to assign keyboard navigation throughout. But if I can't even get the state values updating correctly at this MVP level, then I can't really move forward without making this more cumbersome to work with and debug later.
I know this is an issue of stale state, but I can't find any good articles on this topic that isn't just incrementing a number within the same file. So any help in finally cracking through this wall would be amazing.
Thank you!
Looks like this issue stemmed from the use of the useNav() hook. When I call this hook inside of NavBar references and values are instantiated once. When I call the hook again in NavItem those same refs and values are instantiated again.
In this case, instead of a hook, it would make more sense to wrap this in a context in order to keep the logic out from the UI but keep the components consistent in their data sources.

React How to fire onClick each from map function

I have some lists which are mapped and I'd like to fire onClick when I click an element(which is one of objects from the array).
Let's say there are 3 lists and when I click one of them, I'd like to change "the element that i clicked"'s class to 'open' from 'close'.
state= {
open: false
}
handleClick = () => {
this.setState({
open: !this.state.open
})
}
Array.map((list, index) => {
<div key={index} onClick={this.handleClick}>
<p className={this.state.open? 'open' : 'close'}>{list.title}</p>
</div>
})
.
Array = [{
title: 'a',
link: '/service/wallet'
},{
title: 'b',
link: '/service/home'
}]
I have a value of this.props.location.pathname and I think I can compare this to Array[i].link.
something like this?
if(this.props.location.pathname === Array[0].link){
}
However, I don't know how to complete the code for this issue. please tell if my idea is right and some hints.
You'll need to keep the "is it clicked?" information in this.state. Because it's a list of things you need to keep track of you can't store the state in a single variable, you'll need a map of them.
state= {
open: {}
}
handleClick = (link) => {
let linkOpenState = false;
if (this.state.open.hasOwnProperty(link)) {
linkOpenState = this.state.open[link];
}
this.setState({ open: { [link]: linkOpenState } })
}
Array.map((list, index) => {
<div key={index} onClick={this.handleClick.bind(this, list.link)}>
<p className={this.state.open[list.link]? 'open' : 'close'}>{list.title}</p>
</div>
})
You need to get used to thinking "the React way". State needs to be kept in component state (or if complexity becomes a problem then look at Redux). You don't imperatively manipulate the DOM and you don't "ask" the DOM for information, you tell it how it should look based on the state.
Please have a look at this code mapping data with react elements
To solve this problem, you can have a state which tracks the interaction for a group of elements, in your case it is list of elements.
here you can use a map in a state variable and have {key,value} pair associated with each element to track it.
Hope this helps.
Thanks.

How to get dynamic prop from clicked element in React

I have a React "tree" menu component which has main links with submenus which are dynamically generated by a JSON get call. In the React Inspector I can see that each element on the tree has several props but when I click on each element the only one I can access is value. Here is the props list:
{
"checked": 0,
"className": null,
"label": "192.168.1.71",
"isLeaf": false,
"isParent": true,
"title": null,
"treeId": "rct-~xDYGzs",
"value": "5bd350bf-8515-4dc2-9b12-16b221505593",
}
Here is the code for accessing the value (which was provided in the component API):
onClick(clicked) {
this.setState({ clicked });
console.log('You clicked on ' + clicked.value);
}
If I substitute any other prop name (like "treeId") for clicked.value I get "undefined". I've tried every variation of e.target and getAttributes but nothing is working. Hopefully this is enough info to get some advice but I can definitely add more if needed. Thanks in advance.
Addendum: This is all being scripted into a pre-existing component called react-checkbox-tree so putting together a Codesandbox would be kind of difficult. I did a console.log(clicked) as suggested and got the following:
{value: "5bd81d67-3fd5-464a-b115-161ce291c2d8", checked: false}
For whatever reason the remaining props besides value and checked are not reporting and I can't access them and I have tried everything imaginable.
this.setState({ clicked }) is shorthand for this.setState({ clicked: clicked }). This is called Object Destructuring. If you change that to anything else, then it will rewrite it to (In the case of treeId): treeId: treeId (The variable being passed in to the onClick function is named clicked, so treeId will be undefined.)
If you want to set treeId to clicked.value, simply use:
this.setState({
treeId: clicked.value
});
You can still use the object destructing in the parameter, if value is on the clicked object, and that's all you care about:
onClick({ value }) {
this.setState({ treeId: value });
console.log(`You clicked on ${value}`);
}
The reason you can only get the [value] prop from your onClick is because other data doesn't get passed along in a mouse event. If you want to return all of the props from your subcomponent you can adjust the way you're writing your click handler to return a function like this:
class MainComponent extends Component {
handleClick(props) {
console.log(props); /* whatever you sent from the sub component */
}
render() {
return (
<SubComponent
subComponentClicked={this.handleClick}
/>
);
}
}
class SubComponent extends Component {
handleClick = () => {
this.props.subComponentClicked(this.props);
}
render() {
return (
<div onClick={this.handleClick}></div>
);
}
}

Categories