Why does the state switch while the function is running? - javascript

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.

Related

Capture this.name of a button in React onClick [duplicate]

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

React setting the state of all rendered items with .map in parent component

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

Way to render a new component onClick in react js

Am trying to render a new component onclick a button in react js. Am using functional components and I can't handle it. Eg: am in the UserManagement component and on a button click I need to render another component named employee management.
You can conditionally render your component.
Example :
EmployeeManagement.js
const EmployeeManagement = () => {
....
return (
<div>
EmployeeManagement
</div>
);
}
UserManagement.js
const UserManagement = () => {
const [hasRender, setRender] = useState(false);
const onShow = React.useCallback(() => setRender(true), []);
return (
<>
<button onClick={onShow}>Show Employee Management</button>
{hasRender && <EmployeeManagement />}
</>
)
}
One way to do this would be to add a local state in UserManagement,
that holds a boolean value indication whether the component should be hidden or shown.
Then you will have something like:
function UserManagement() {
const [compIsShown, setCompIsShown] = useState(false);
return (
// Whatever else you're rendering.
<button onClick={() => setCompIsShown(true)}>...</button>
{compIsShown && <OtherComp />}
)
}
What will happen is that compIsShown will initialize as false,
so this condition compIsShown && <OtherComp /> will prevent it from rendering.
Then, when you click the button, the state will set, causing a re-render, except now the condition will be true, so <OtherComp> will be rendered.
There are other ways to go about this.
Depends mostly on the use-case.
use a visible state & toggle it in onClick:
const [visible, setVisible] = useState(false)
onClick = () => {setVisible(true)}
then render it like this:
{visible && <EmployeeManagement onClick={onClick} />}

How to remain state when page refresh in react?

I use react in the project. The modal closes when the user refreshes, but I want to modify it so that it doesn't close. I found 'beforeunload' event, but I want to prevent 'alert' from appearing.
And when I refresh, I want to keep the data I received from the API and maintain the model.
I want to make the modal close only when I press the close button, what methods can I use?
This is not a real code. Just a example...
function Modal() {
const [open, setOpen] = useState(false)
const handleOpen = () => {
setOpen(!open);
}
return (
<Modal>
<button onClick={handleOpen}>
modal
</button>
(some data from API)
</Modal>
)
}
Read localStorage value first, if it exists use it, and then everytime you set a new value update it in local storage as well.
Remember localstorage only saves values as a string; and when it doesn't exist it will come back as a null.
function Modal() {
const [open, setOpen] = useState(JSON.parse(localStorage.getItem("open")) || false)
const handleOpen = () => {
setOpen((prevState) => {
localStorage.setItem("open", !prevState);
return !prevState;
});
}
return (
<Modal>
<button onClick={handleOpen}>
modal
</button>
</Modal>
)
}

Props not being passed down to Material UI button text

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

Categories