I have a scenario in my app where I use data attributes on buttons to capture certain contextual information in the modal that is launched by pressing the button. This relies on the data attributes being available on e.target.dataset. The problem is if I click on the part of the button with text, the target is a that appears to be generated by Material-UI but which doesn't contain the props I passed down to the button.
Here's my button implementation (Button.js) [SmallButton and LargeButton are styled versions of the Material Button]:
const Button = ({ buttonText, buttonSize, handleClick, pink, ...props }) => {
return (
<>
{buttonSize === 'large' ? (
<LargeButton
variant="contained"
fullWidth={true}
size="large"
onClick={handleClick}
pink={pink}
{...props}
>
{buttonText}
</LargeButton>
) : (
<SmallButton
variant="contained"
size="small"
onClick={handleClick}
pink={pink}
{...props}
>
{buttonText}
</SmallButton>
)}
</>
);
};
This is where the button is used:
<AddContainer flexDirection="column">
<Label {...props}>Add {type}</Label>
<Button
buttonSize="small"
buttonText="+ Add"
pink="true"
data-show-only={lowerCase(type)} // this needs to come through in e.target.dataset when button clicked
onClick={launchModal}
/>
</AddContainer>
<ModalContainer
open={open}
context={context}
handleClose={closeModal}
initialScreen="Add Exercise"
addWorkout={addWorkout}
/>
The context object above should include a property called showOnly. Here is the custom hook where we create the context object.
const useModal = (initialValues = {}) => {
const [open, setOpen] = useState(initialValues);
const [context, setContext] = useState({});
const launchModal = e => {
const targetContext = { ...e.target.dataset };
setContext(targetContext);
setOpen(true);
};
const closeModal = e => {
setOpen(false);
setContext({});
};
return {
open,
context,
launchModal,
closeModal,
};
};
If I click on the outside of the button, this works as designed, with e.target.dataset containing a showOnly property that is passed on to the context object. If you click on the text, e.target is a and e.target.dataset is empty.
You want to use currentTarget instead of target.
From https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget:
The currentTarget read-only property of the Event interface identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to Event.target, which identifies the element on which the event occurred and which may be its descendant.
Here's a little example that you can use to see the difference in the console logs:
import React from "react";
import Button from "#material-ui/core/Button";
export default function App() {
return (
<div className="App">
<Button
data-test="1"
onClick={e => {
console.log("target", e.target.dataset);
console.log("currentTarget", e.currentTarget.dataset);
}}
>
Hello
</Button>
</div>
);
}
Related
This question already has an answer here:
ReactJS, event.currentTarget doesn't have the same behavior as Vanilla Javascript
(1 answer)
Closed last month.
I want to capture the name attribute of a button on click in React.
I tried the following code block:
export function TestButton(props){
function logName() {
console.log(this.name)
}
return(
<button name={props.name} onClick={event => logName(event.currentTarget.getAttribute("name"))} type='button'>{props.text}</button>
)
}
My expectation was that this code would allow me to create a button that displays the name in the console log:
<TestButton name='helloWorld' text='Click Me'/>
Instead I get an alert that this is undefined. This is in spite of my ability to see the name when I inspect the element.
I have also tried target instead of currentTarget with no luck. I also tried event.currentTarget.name without the results I desire.
What did i miss?
In react, I believe this is reserved for classes, whereas you are defining a functional component. In a functional component, the comparable state value would be stored with useState(). That being said, I'm not sure I see the need for that here, since this button is getting its props from somewhere and the value of name and text are not changing in this component. I would code it this way:
export const TestButton = ({props}) => {
return(
<button name={props.name} onClick={() => console.log(props.name)}>
{props.text}
</button>
)
}
Now to go a bit further, maybe you want to use state wherever this button is being rendered. That could look like this:
import {TestButton} from "./someFile";
const [name, setName] = useState("some-button");
const [text, setText] = useState("click me!");
// now there could be some code here that decides what the name or text would be
// and updates the values of each with setName("name") and setText("text")
const Page = () => (
<>
<TestButton props={{name: name, text: text}} />
</>
)
This is all building off your current code, but now I will combine everything in a way that makes sense to me:
import {useState} from "react";
const [name, setName] = useState("some-button");
const [text, setText] = useState("click me!");
// some code to determine/change the value of the state vars if necessary
const TestButton = ({name, text}) => {
return(
<button name={name} onClick={() => console.log(name)}>
{text}
</button>
)
}
export const Page = () => (
<>
<TestButton name={name} text={text} />
</>
)
Pleas try as follows:
export function TestButton(props){
function logName() {
console.log(props.name)
}
return(
<button name={props.name} onClick={() => logName()} type='button'>{props.text}</button>
)
}
Try this
export function TestButton(props){
const logName = (e, name) => {
console.log("name attribute ->", name)
}
return(
<button name={props.name} onClick={ (e) => logName(e, props.name)} type='button'>{props.text}</button>
)
}
I have a parent component with a handler function:
const folderRef = useRef();
const handleCollapseAllFolders = () => {
folderRef.current.handleCloseAllFolders();
};
In the parent, I'm rendering multiple items (folders):
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
name={folder.name}
content={folder.content}
id={folder.id}
ref={folderRef}
/>
))}
In the child component I'm using the useImperativeHandle hook to be able to access the child function in the parent:
const [isFolderOpen, setIsFolderOpen] = useState(false);
// Collapse all
useImperativeHandle(ref, () => ({
handleCloseAllFolders: () => setIsFolderOpen(false),
}));
The problem is, when clicking the button in the parent, it only collapses the last opened folder and not all of them.
Clicking this:
<IconButton
onClick={handleCollapseAllFolders}
>
<UnfoldLessIcon />
</IconButton>
Only collapses the last opened folder.
When clicking the button, I want to set the state of ALL opened folders to false not just the last opened one.
Any way to solve this problem?
You could create a "multi-ref" - ref object that stores an array of every rendered Folder component. Then, just iterate over every element and call the closing function.
export default function App() {
const ref = useRef([]);
const content = data.map(({ id }, idx) => (
<Folder key={id} ref={(el) => (ref.current[idx] = el)} />
));
return (
<div className="App">
<button
onClick={() => {
ref.current.forEach((el) => el.handleClose());
}}
>
Close all
</button>
{content}
</div>
);
}
Codesandbox: https://codesandbox.io/s/magical-cray-9ylred?file=/src/App.js
For each map you generate new object, they do not seem to share state. Try using context
You are only updating the state in one child component. You need to lift up the state.
Additionally, using the useImperativeHandle hook is a bit unnecessary here. Instead, you can simply pass a handler function to the child component.
In the parent:
const [isAllOpen, setAllOpen] = useState(false);
return (
// ...
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
isOpen={isAllOpen}
toggleAll={setAllOpen(!isAllOpen)}
// ...
/>
))}
)
In the child component:
const Child = ({ isOpen, toggleAll }) => {
const [isFolderOpen, setIsFolderOpen] = useState(false);
useEffect(() => {
setIsFolderOpen(isOpen);
}, [isOpen]);
return (
// ...
<IconButton
onClick={toggleAll}
>
<UnfoldLessIcon />
</IconButton>
)
}
The site has a button for deleting records (DeleteForeverIcon in the code). When you click on this button, a window opens (made according to the documentation using Dialog Mui).
When you click on the "Confirm" button, the handleDeleteItem() function is launched, which deletes the entry. But I can’t understand why the window closes while this function is running, because I don’t switch the state anywhere
Looking for a solution on my own, I added console.log() to my code (below is the same code, only with console.log()) and came up with the following conclusion: when I run the handleDeleteItem() function, the open state switches to false and so the window closes. But why does it switch to false?
export function DeleteButton({ item }) {
const { urlParams } = useContext(PageContext)
const { firestore } = useContext(AppContext)
const [open, setOpen] = useState(false);
console.log(open, "window close") // console.log here
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleDeleteItem = async () => {
console.log("start") // console.log here
await deleteItem()
console.log("finish") // console.log here
}
return (
<ButtonGroup>
<DeleteForeverIcon onClick={handleClickOpen}/>
<Dialog
open={open}
onClose={handleClose}>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleDeleteItem}>Confirm</Button>
</DialogActions>
</Dialog>
</ButtonGroup >
)
}
That is, summing up the question, I would like the window to close only after the deleteItem () function has completed, and not during its execution
Based on some further clarification in the comments, it seems as though your issue is to do with the fact that calling deleteItem(...) causes your state to update in your parent components (due to an onSnapshot firebase listener). Your parent components are responsible for rendering the children components. When your state updates, the item/row that you deleted won't be in the new state value, and so the component that was rendering your Dialog previously won't be rendered (because it has been deleted).
Here is a minified example of your issue:
const { useState } = React;
const List = () => {
const [items, setItems] = useState(["a", "b", "c", "d", "e"]);
const handleDelete = (charToDel) => {
setItems(items => items.filter(char => char !== charToDel));
}
return <ul>
{items.map(char =>
<li key={char}>{char} - <DeleteButton value={char} onDelete={handleDelete}/></li>
)}
</ul>
}
const DeleteButton = ({value, onDelete}) => {
const [open, setOpen] = useState(false);
return <React.Fragment>
<button onClick={() => setOpen(true)}>×</button>
<dialog open={open}>
<p>Delete {value}?</p>
<button onClick={() => onDelete(value)}>Confirm</button>
</dialog>
</React.Fragment>
}
ReactDOM.createRoot(document.body).render(<List />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
So since you're not rendering the component that renders the Dialog once you remove the item, you won't be rendering the <Dialog> anymore and so it disappears.
One way to fix this is to lift the <Dialog> component up to a component that doesn't get removed when you remove an item from your state. By the looks of things, the closest parent component that has this property is DevicesTable. In there you can render your dialog and keep track of a selectedItem to determine which item that should be deleted, which you can set based on the item you press (see code comments below):
// DevicesTable component
const [selectedItem, setSelectedItem] = useState();
const handleClose = () => {
setSelectedItem(null);
}
const handleDeleteItem = () => { // this doesn't need to be `async` if not using `await`
deleteItem(selectedItem, firestore, urlParams); // this doesn't need to be `await`ed if you don't have any code following it
}
return (
<>
{/* vvvvvv -- move dialog here -- vvvvvv */}
<Dialog open={!!selectedItem} onClose={handleClose}>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleDeleteItem}>Confirm</Button>
</DialogActions>
</Dialog>
{/* ^^^^^^ -- move dialog here -- ^^^^^^ */}
<TableContainer className="TableContainerGridStyle">
<Table className="TableStyle">
<DevicesTableHeader />
<TableBody className="TableBodyStyle">
{devices.map((device) =>
<DevicesTableCell
device={device}
onDeleteButtonPress={() => setSelectedItem(device)} /* <--- set the selected item */
key={device.description.id}
/>
)}
</TableBody>
</Table>
</TableContainer>
</>
);
For brevity, I've removed the open state and instead used the presence of the selectedItem to determine if the modal should be open or not, but you can of course add that back in if you wish and set both the selectedItem and the open state when opening and closing the modal.
Within DevicesTableCell, you would then grab the onDeleteButtonPress prop, and then pass it to DeleteButton like so:
// v-- grab the function
function DevicesTableCell({ device, onDeleteButtonPress }) {
...
<DeleteButton item={device} onDeleteButtonPress={onDeleteButtonPress}/> {/* pass it to the componennt */}
...
}
Within DeleteButton you should then invoke the onDeleteButtonPress function:
<DeleteForeverIcon onClick={onDeleteButtonPress} />
If you don't like the idea of passing callbacks down through multiple components like this, you can avoid that by using useReducer with a context, as described here.
This is the function where I am passing the onClick prop (setShowModal is setState() from the useState hook):
<MyFunctionalComponent
onClick={() => setShowModal(true)}
...other props here
/>
This is the functional component that receives the prop:
export const MyFunctionalComponent = ({ onClick }) => {
return (
<section>
...other code here
{onClick && (<Button>{ctaText}</Button>)}
</section>
);
};
But the Button component never appears, because the prop onClick is undefined. When I console.log the prop inside the functional component, it initially prints the function in the console, but then prints two more times as undefined. Could someone explain why that would be? I got it to work by spreading ...props instead. But the console.log remains the same? I don't understand why. This is my first question on Stack Overflow, so feel free to give me feedback on how to ask better questions :)
The reason why you are receiving an 'undefined' response is because as #Zrogua mentioned, onClick is an event listener function rather than a persistent value (like state you define).
import React from "react";
const YourButton = ({ onClick }) => {
console.log(onClick);
return <section>{onClick && <button>here</button>}</section>;
};
const ParentDiv = () => {
return (
<div>
<h1>Button Props</h1>
<h2>Start editing to see some magic happen!</h2>
<YourButton onClick={() => console.log("CLICK")} />
</div>
);
};
export default ParentDiv;
Result of console.log():
function onClick() // index.js:27:25
The reason why this is because props are read-only. From the React Docs:
Whether you declare a component as a function or a class, it must never modify its own props ... Such functions are called “pure” because they do not attempt to change their inputs, and always return the same result for the same inputs.
Therefore your button will only show if the onClick function is defined. For example, if you did not give onClick a function or value, the button will not appear:
import React, { useState } from "react";
const YourButton = ({ onClick }) => {
console.log(onClick);
return (
<section>
{onClick && <button>This button is shown if a button is defined.</button>}
</section>
);
};
const ParentDiv = () => {
return (
<div>
<h1>Button Props</h1>
<YourButton onClick={() => console.log("CLICK")} />
<YourButton /> {/* You won't see this button because the function is not defined. */}
</div>
);
};
export default ParentDiv;
The button appears because the prop has a value that is not undefined (your onClick function), and because it is read-only, you cannot access that function in your child component.
Instead, (1) define the modal state in the parent component and (2) pass the state through props to the button like so:
import React, { useState } from "react";
const YourButton = ({ onClick }) => {
console.log(onClick);
return (
<section>
{onClick && <button>This button is shown if a button is defined.</button>}
</section>
);
};
const AltButton = ({ modal }) => {
return (
<section>
{modal && (
<button>This button is shown the modal state is passed.</button>
)}
</section>
);
};
const ParentDiv = () => {
const [modal, setModal] = useState(false);
return (
<div>
<h1>Button Props</h1>
<YourButton onClick={() => console.log("CLICK")} />
<YourButton />{" "}
{/* You won't see this button because the function is not defined. */}
<section>
<button onClick={() => setModal(!modal)}>OPEN MODAL</button>
</section>
{modal && <p>this is dependent on state</p>}
<AltButton modal={modal} />
</div>
);
};
export default ParentDiv;
Working CodeSandbox: https://codesandbox.io/s/stack-66715327-passingfunctions-92pzr
Finally, if I am reading between the lines and understanding correctly that you are looking to hide a button when a modal is open, here is a little modal wrapper trick I use for buttons that open modals: https://codesandbox.io/s/stack-66715327-modalwrapper-wvl54
You can't pass onClick, onClick is just an event listener. You should pass the state
<MyFunctionalComponent onClick={() => setShowModal(!showModal)}
showModal={showModal}
...other props here />
/>
export const MyFunctionalComponent = ({ showModal }) => {
return (
<section>
...other code here
{showModal && (<Button>{ctaText}</Button>)}
</section>
);
};
I believe this should work. Let me know if this is what you were looking for.
I think that rather then passing callback you should pass variable which decide if component should show or not. Check this example.
export const MyFunctionalComponent = ({ isShow, onClick }) => {
return (
<section>
...other code here
{isShow && <div>something</div>}
</section>
);
};
export default function App() {
const [showModal, setShowModal] = useState(false);
return (
<MyFunctionalComponent
isShow={showModal}
onClick={() => setShowModal(true)}
/>
);
}
I also suppose that you can make mistake and have something other on mind .. like this:
<section>
...other code here
<button onClick={ onClick }>something</button>}
</section>
I have a Dropdown made with React Hooks. The button should have Arrow, which rotate. My first Version works fine:
const DropdownMenu= (props) => {
const [open, setOpen] = useState(false);
const openDropdown = (): void => setOpen(prevState => !prevState);
return (
<div>
<Button
onClick={openDropdown}
dropdownIsOpen={open}
>
Text </Button>
<DropdownContent isOpen={isOpen} />
</div>
);
};
export default DropdownMenu;
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
export default Button;
But now I have multiple Dropdowns and want to use them with the same component. I gave the toggleNode as prop. Now the Arrow rotate but without the transition:
const DropdownButton = (props) => {
const [isOpen, setOpen] = useState(false);
const onToggle = (): void => setOpen(prevState => !prevState);
return (
<Dropdown
isOpen={props.isOpen}
onToggle={onToggle}
toggleNode={
<Button dropdownIsOpen={isOpen}>
Text
</Button>
}
/>
);
};
export default DropdownButton;
Have someboy an idea?
Thank you!
my guess is, you are exporting different Button
How can you use Button inside Button, possibly by importing Button from somewhere else. And then you are again exporting Button. This will be confuse for transpiler, as which Button to be exported.
You might want to rename your custom Button to something else
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
export default Button;
Given your Button component takes an onClick prop:
const Button = (props) => {
return (
<Button Click={props.onClick}>
Text
<Arrow rotate={props.dropdownIsOpen} color={designTheme.color.primary} />
</Button>
);
};
NOTE: As #SagarMore points out, there my also be a naming collision between some imported Button component and your Button component.
You may just need to pass a callback to Click (terrible name, BTW, should be onClick, hopefully it was just a typo):
const DropdownButton = (props) => {
const [isOpen, setOpen] = useState(false);
const onToggle = (): void => setOpen(prevState => !prevState);
return (
<Dropdown
isOpen={props.isOpen}
onToggle={onToggle}
toggleNode={
<Button onClick={onToggle} dropdownIsOpen={isOpen}>
Text
</Button>
}
/>
);
};
Passing onToggle to the inner button's onClick handler should now toggle the isOpen state of the DropDown.
This was my fault.
In my Dropdown component I render two different states conditional. So it renders the start or the end state and don't use the animation. I have to render a animation and don't render new when I click so the animation works.