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.
Related
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.
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.
Admit it. Being new to JavaScript in 2018 is difficult. Coming from languages like C#, Java and Typescript(yeah subset of js..) where type safety is key, Javascript just keep f***** me over. I struggle with something simple like updating an array..
So I have this React component, where the state is defined like this:
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
show: false,
shoes: []
};
}
....
...
}
The shoes is an array of undefined(?)
This array is passed to a stateless component which looks like this
const Shoelist = props => {
return (
<Wrapper>
{props.shoes.map((shoe, i) => (
<div key={i}>
<Shoe shoe={shoe} />
<Separator />
</div>
))}
</Wrapper>
);
};
I in my Form component, I have a method which is supposed to react(doh) on onClick methods. In this method I get a parameter with a new shoe to add in this list. This is very it stops for me in javascript - something which is faaaaairly easy in all other languages that we've being using for the past years..
I've tried several ways:
1#
addShoe(shoe) {
this.setState(state => {
const list = state.shoes.push(shoe);
return {
list
};
});
}
This results in an error: Uncaught TypeError: Cannot read property 'push' of undefined Do I need to define shoes as an Array? I thought the [] was enough
2#
I googled, I do that alot. I found one blog post saying something about react-addons-update. I installed this by running yarn add and code looks like this:
addShoe(shoe) {
this.setState(update(this.state, { shoes: { $push: [shoe] } }));
}
which results in Uncaught Error: update(): expected target of $push to be an array; got undefined.
Help! How difficult can this be?
EDIT
I pass this method into another component like this:
<ShoeModal onClose={this.addShoe} />
in the ShoeModal component this is bound to a onClick method:
<FinishModalButton
onClick={this.props.onClose.bind(this, this.state.shoe)}>
....
</FinishModalButton>
ShoeModal.propTypes = {
onClose: PropTypes.func.isRequired
};
You can do it this way:
this.setState({
shoes: [...this.state.shoes, newShoe]
})
... adds all elements from this.state.shoes
With your updates we can see that the issue is the way the addShoe callback is passed. It's being invoked as a function instead of a method of an object, so it loses context.
Change it to:
<ShoeModal onClose={this.addShoe.bind(this)} />
or
<ShoeModal onClose={shoe => this.addShoe(shoe)} />
In addition, .push returns the count of the array, so the following line won't give you what you expect:
const list = state.shoes.push(shoe);
See #merko's answer for a solution.
Firstly, your addShoe method is not an arrow function.
Using arrow functions because the context this is of the component.
Second, you are returning the object {list}. This sets the variable list in state.
Also push to the new list variable instead of mutating state.
Change your function to
addShoe = (shoe) => {
this.setState(state => {
let list = state.shoes;
list.push(shoe);
return {
shoes : list
};
});
}
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>
);
}
}
I am coming from Angular 1.x and looking to update an unordered list with React / Redux.
In console.log, I am seeing the array being updated, but it doesn't seem to bind to the DOM. I have the following --
onKeyPress of an input, I have a function that pushes to messages array.
<ul className="list-inline">
{messages.map(function(message, key){
return (
<li key={key} message={message}>{message}</li>
);
})}
</ul>
Update
I have the following (but no luck yet) Some notes. I am using Firebase to listen for events, and add to an array. Wondering if its a bind issue? --
class Comments extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {messages: this.props.messages};
}
componentDidMount() {
const path = '/comments/all';
// Firebase watches for new comments
firebase
.database()
.ref(path)
.on('child_added', (dataSnapshot) => {
this.state.messages.push(dataSnapshot.val());
this.setState({
messages: this.state.messages
});
//console.log(dataSnapshot.val());
});
}
render() {
const messages = this.state.messages;
return (
<ul className="list-inline">
{messages.map(function(message, key){
<li key={key}>{message}</li>
})}
</ul>
);
}
}
You need messages to be set in the components state.
getInitialState() {
return {
messages: []
}
}
Then in your function, set the state:
this.setState({messages: updatedMessages})
and then map over the messages state, or a messages variable in render:
const messages = this.state.messages;
<ul className="list-inline">
{messages.map(function(message, key){
etc...
put messages array and set state change to render DOM. You should read https://facebook.github.io/react/docs/component-specs.html
Two issues:
You mustn't directly mutate the state object in React (see: Do Not Directly Modify State). Instead, provide a new array via setState with the new entry in it.
When updating state based on existing state, you must use the function callback version of setState, not the version accepting an object, because state updates are asynchronous and may be merged (see: State Updates May Be Asynchronous, though it's really "will," not "may"). Using the object version often happens to work, but it isn't guaranteed to; indeed, it's guaranteed not to, at some point.
Let's look at various ways to update an array:
Adding to the end (appending):
this.setState(({messages}) => ({
messages: [...messages, newValue]
}));
In your case, newValue would be dataSnapshot.val().
(We need the () around the object initializer because otherwise the { would seem to start a full function body instead of a concise expression body.)
Adding to the beginning (prepending):
Largely the same, we just insert the new element in a different place:
this.setState(({messages}) => ({
messages: [newValue, ...messages]
}));
(We need the () around the object initializer because otherwise the { would seem to start a full function body instead of a concise expression body.)
Updating an existing item in the array
Suppose you have an array of objects and want to update one you have in the variable targetElement:
this.setState(({messages}) => {
messages = messages.map(element => {
return element === targetElement
? {...element, newValue: "new value"}
: element;
});
return {messages};
}));
Removing an existing item in the array
Suppose you have an array of objects with id values and want to remove the one with targetId:
this.setState(({messages}) => {
messages = messages.filter(element => element.id !== targetId);
return {messages};
}));
By Index
Warning: Updating arrays in React by index is generally not best practice, because state updates are batched together and indexes can be out of date. Instead, work based on an identifying property or similar.
But if you have to use an index and you know it won't be out of date:
this.setState(({messages}) => {
messages = messages.filter((e, index) => index !== targetindex);
return {messages};
}));