I am struggling to understand how to use React modals (specifically React-Bootstrap) when asynchronous code is involved. I am learning about promises and React at the same time so I hope I am formulating the question correctly.
My goal is to launch an asynchronus function with parameters provided by a modal. This is what I am doing at the moment, it works, but I have the impression that I am not separating concerns.
When a button is clicked the async function simulateImprovedBuilding is launched.
async function simulateImprovedBuilding() {
function selectOsMeasures(measures) {
let selectedOsMeasures = measures;
let scenario = "Improved";
simulateBuilding(scenario, selectedOsMeasures).then((result) => {
console.log("Finally!!!!");
renderMonthlySimulation(result);
});
}
const osMeasuresModalRoot = createRoot(document.getElementById("react_modal"));
osMeasuresModalRoot.render(<OsMeasuresModal action={selectOsMeasures} />);
console.log("Done");
}
The modal is rendered, I make the selection and the component execute selectOsMeasures to update the parameters, launch the long running async process and, when completed renders the results on a chart. This is the component
function osMeasuresModalRoot(props) {
const [show, setShow] = useState(true);
const handleClose = () => {
setShow(false);
let selectedOsMeasures = osMeasures.filter(x => selections[osMeasures.indexOf(x)]);
console.log(selectedOsMeasures);
props.action(selectedOsMeasures);
};
const handleShow = () => setShow(true); // Not used
const [selections, setSelections] = useState([]);
const [osMeasures, setOsMeaaures] = useState([]);
useEffect(() => {
let url = `http://${osServer}/os_measures`;
fetch(url)
.then((response) => response.json())
.then((result) => {
setOsMeaaures(result);
setSelections(Array(result.length).fill(false));
});
}, []);
return (
<>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Select ECMs</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
{osMeasures.map((measure, index) => (
<div key={`${measure}`} className="mb-3">
<Form.Check
type={"checkbox"}
id={`${measure}`}
label={`${measure}`}
onChange={() => {
selections[index] = !selections[index];
console.log(selections);
}}
/>
</div>
))}
</Form>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" onClick={handleClose}>
Simulate
</Button>
</Modal.Footer>
</Modal>
</>
);
}
I think the selectOsMeasures function is doing too much. I think I should do something like:
simulateImprovedBuilding()
.then(result => renderMonthlySimulation(result));
But if I do this, the function exits immediately after the React render function and of course result is undefined.
First of all, is my concern valid? If so, how should I refactor my code?
How about increasing the scope of the function and making the result global? It's a bit hacky but is more along the lines of what React should be doing rather than having multiple rendering functions. Then it will automatically switch between the Modal and the graph rendering function when it changes:
function SimulateImprovedBuilding() {
let r_result = undefined;
function OsMeasuresModal(props) {
const [show, setShow] = useState(true);
const handleClose = () => {
setShow(false);
let selectedOsMeasures = osMeasures.filter(x => selections[osMeasures.indexOf(x)]);
console.log(selectedOsMeasures);
props.action(selectedOsMeasures);
};
const handleShow = () => setShow(true); // Not used
const [selections, setSelections] = useState([]);
const [osMeasures, setOsMeaaures] = useState([]);
useEffect(() => {
let url = `http://${osServer}/os_measures`;
fetch(url)
.then((response) => response.json())
.then((result) => {
setOsMeaaures(result);
setSelections(Array(result.length).fill(false));
});
}, []);
return (
<>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Select ECMs</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
{osMeasures.map((measure, index) => (
<div key={`${measure}`} className="mb-3">
<Form.Check
type={"checkbox"}
id={`${measure}`}
label={`${measure}`}
onChange={() => {
selections[index] = !selections[index];
console.log(selections);
}}
/>
</div>
))}
</Form>
</Modal.Body>
<Modal.Footer>
<button onClick={handleClose}>
Cancel
</button>
<button onClick={handleClose}>
Simulate
</button>
</Modal.Footer>
</Modal>
</>
);
}
function selectOsMeasures(measures) {
let selectedOsMeasures = measures;
let scenario = "Improved";
simulateBuilding(scenario, selectedOsMeasures).then(r => {
console.log("Finally!!!!");
r_result = r;
});
}
function Render() {
return (
<>
{r_result===undefined? <OsMeasuresModal action={selectOsMeasures} /> : renderMonthlySimulation(r_result)}
</>
)
}
const osMeasuresModalRoot = createRoot(document.getElementById("react_modal"));
osMeasuresModalRoot.render(<Render/>);
console.log("Done");
}
Note: the renderMonthlySimulation(r_result) function will need to become a react component that returns what you want to render
Same can be done in multiple ways, you could use a 'Context' across all the react functions, or use 'Routing' to switch between things you're rendering. It's much better to have a singular .render.
Related
I'm attempting to replace browser alerts with Dialog components from MUI in my React project. The code below is taken from a JSX component that will make an API call to delete a comment from a database.
I'm able to get the dialog to work in order to delete the comment and optimistically render the result however, in the catch block of the api call I would like to trigger a new Dialog component render should there be an error with the deletion. I'm doing this by flipping the state of "err" to true within the catch block and setting this to the open prop in the Dialog component, but this 2nd Dialog component never renders and the console.logs always show err to be false in the browser console.
What am I missing here? Thanks!
export default function DeleteCom({ commentId, setComments, setComNum }) {
const [open, setOpen] = useState(false);
const [err, setErr] = useState(false);
let comIndexToDelete = 0;
let deletedCom = {};
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
const handleClick = () => {
setComments((currentComments) => {
comIndexToDelete = currentComments.findIndex(
(comment) => comment.comment_id === commentId
);
deletedCom = currentComments[comIndexToDelete];
setComNum((currentComNum) => currentComNum - 1);
return currentComments.filter(
(comment) => comment.comment_id !== commentId
);
});
deleteComment(commentId).catch(() => {
setComNum((currentComNum) => currentComNum + 1);
// alert("There was a problem deleting your comment. Please try again.")
console.log(1, err)
setComments((currentComments) => {
const comClone = [...currentComments];
comClone.splice(comIndexToDelete, 0, deletedCom);
return comClone;
});
setErr(true);
console.log(2, err)
});
};
return (
<div className="com-delete">
{commentId && (
<IconButton
className="IconButton"
aria-label="delete"
size="large"
onClick={handleOpen}
>
<DeleteTwoToneIcon fontSize="inherit" />
</IconButton>
)}
<Dialog
open={open}
onClose={handleClose}
aria-describedby="alert-dialog-description"
>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you wish to delete this comment?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>No</Button>
<Button onClick={handleClick}>Yes</Button>
</DialogActions>
</Dialog>
<Dialog
open={err}
onClose={() => {setErr(false)}}
aria-describedby="alert-dialog-description"
>
<DialogContent>
<DialogContentText id="alert-dialog-description">
There was a problem deleting your comment. Please try again.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => {setErr(false)}}>Close</Button>
<Button onClick={handleClick}>Retry</Button>
</DialogActions>
</Dialog>
</div>
);
}
I know there are question asked before, but I have tried the solution but it's not working. I have split the component differently but then also it refresh on every single keypress.
const TenementRegistration = () => {
const [show, setShow] = useState(false);
const [name, setName] = useState("");
const [editId, setEditId] = useState("");
function Example() {
const onSubmitHandler = async () => {
const data = {
name: name
}
await services.postService("User", data).then((res) => {
onGetUserData();
});
}
return(
<Modal
show={show}
onHide={() => setShow(false)}
size="lg"
aria-labelledby="example-custom-modal-styling-title"
scrollable="true"
centered
animation="true"
>
<Modal.Header closeButton>
<Modal.Title id="example-custom-modal-styling-title">
Add User
</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="form-container">
<form>
<Row>
<div className="form-group col-12 col-md-6
center">
<label for="inputName" className="asy-
FormLabel">
Name
</label>
<input
type="text"
className="form-control asy-InputValues"
id="policyName"
placeholder="Enter Property Id"
onChange={(e) => {
setName(e.target.value);
}}
value={name}
required
/>
</div>
</Row>
</form>
</div>
</Modal.Body>
<Modal.Footer>
<button
type="button"
className="submit-button"
onClick={() => {
onSubmitHandler();
}}
>
Submit
</button>
</Modal.Footer>
</Modal>
const [data, setData] = useState([]);
useEffect(() => {
onGetUserData();
}, []);
const onGetUserData = async () => {
services.getService("User").then((res) => {
setData(res.data);
});
};
const onEditData = async (id) => {
setShow(true);
const newData = data.filter((obj) => obj.id === id)[0];
setName(newData.name);
}
//Table where we show name and pass id to update button
}
I have also tried to Split the Modal and separate the form (not in this example) but it didn't work any suggestions how to handle the modal problem
Try this
In your button onClick
<button
type="button"
className="submit-button"
onClick={(event) =>
{
onSubmitHandler(event);
}}
>
Submit
</button>
then in that function:
const onSubmitHandler = async (event) => {
event.preventDefault()
const data = {
name: name
}
await services.postService("User", data).then((res) => {
onGetUserData();
});
}
Following Situation.
I have a functional Parent Component like this:
function TestAutomationTab() {
const theme = createMuiTheme({
typography: {
htmlFontSize: 10,
useNextVariants: true,
},
});
const [szenarios, setSzenarios] = useState([]);
const [filterSzenario, setFilterSzenario] = useState('ALL');
const [data, setData] = useState([{}]);
const [runAll, setRunAll] = useState(false);
const [runAllButton, setRunAllButton] = useState('RUN ALL');
useEffect(() => {
fetchDistinctSzenarios();
fetchTestfaelle();
}, []);
async function fetchDistinctSzenarios() {
const response = await Api.getDistinctTestautoSzenarios();
setSzenarios(response.data);
setSzenarios(oldState => [...oldState, 'ALLE']);
}
function handleFilterChange(event) {
setFilterSzenario(event.target.value);
fetchTestfaelle();
}
async function fetchTestfaelle() {
const response = await Api.getAllOeTestfaelle();
response.data.forEach((e) => {
e.status = 'wait';
e.errorStatus = '';
e.statusText = '-';
});
setData(response.data);
}
function sendSingleCase(id) {
data.forEach((e) => {
if(e.id === id){
e.status = 'sending';
}
})
}
return (
<React.Fragment>
<MuiThemeProvider theme={theme}>
<div style={styles.gridContainer}>
<Upload />
<TestautomationSzenarioFilter
/>
<DocBridgePieChart />
<div style={styles.uebersicht}>
{filterSzenario.length ? <OeTestfallAccordion
choosenFilter={filterSzenario}
testData={data}
runAll={runAll}
sendSingleCase={sendSingleCase}
/> : <div>Wähle Szenario</div>}
</div>
</div>
</MuiThemeProvider>
</React.Fragment>
);
}
OeTestfallAccordion
function OeTestfallAccordion(props) {
const data = props.testData;
return (
<React.Fragment>
{data.map(e => (<OeTestfall
key={e.id}
szenario={e.szenario}
testid={e.testfallid}
json={e.json}
status={e.status}
runAll={props.runAll}
errorStatus={e.errorStatus}
statusText={e.statusText}
sendSingleCase={props.sendSingleCase}
/>))}
</React.Fragment>
);
}
OeTestfall
function OeTestfall(props) {
const { szenario, testid, json } = props;
const [open, setOpen] = useState(false);
function handleOpen(event) {
event.stopPropagation();
setOpen(true);
}
function handleClose() {
setOpen(false);
}
return (
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreOutlined />}>
<OeTestfallSummary
szenario={szenario}
testid={testid}
json={json}
status={props.status}
handleClose={handleClose}
handleOpen={handleOpen}
open={open}
statusText={props.statusText}
errorStatus={props.errorStatus}
sendSingleCase={props.sendSingleCase}
/>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<div>ForNoError</div>
</ExpansionPanelDetails>
<ExpansionPanelActions>
<Button
variant="outlined"
color="primary"
>
Bearbeiten
</Button>
<Button
variant="outlined"
color="secondary"
>
Löschen
</Button>
</ExpansionPanelActions>
</ExpansionPanel>
);
}
OeTestfallSummery
function OeTestfallSummary(props) {
const { handleOpen } = props;
const [status, setStatus] = useState('');
const [statusText, setStatusText] = useState('');
const [errorStatus, setErrorStatus] = useState('');
useEffect(() => {
setErrorStatus(props.errorStatus);
setStatusText(props.statusText);
setStatus(props.status);
}, []);
return (
<div style={styles.summaryWrapper}>
<Typography align="center" variant="subtitle1">
TestID: {props.testid}
</Typography>
<Typography align="center" variant="subtitle1" style={{ fontWeight: 'bold' }}>
{props.szenario}
</Typography>
<Button
size="small"
variant="outlined"
color="primary"
onClick={handleOpen}
>
JSON
</Button>
<Tooltip title="VorneTooltip" style={styles.lightTooltip} placement="left">
<Chip
color="secondary"
variant="outlined"
label={status}
/>
</Tooltip>
<StatusChip
status={errorStatus}
/>
<OeJsonViewer json={JSON.parse(props.json)} open={props.open} handleClose={props.handleClose} stopEventPropagation />
<Tooltip
title="ToolTipTitel"
style={styles.lightTooltip}
placement="top"
>
<Chip
color="primary"
variant="outlined"
label={statusText}
/>
</Tooltip>
<Button variant="contained" color="primary" onClick={() => props.sendSingleCase(props.testid)} >
Run
</Button>
<Button variant="contained" color="primary" onClick={() => console.log(status)} >
test
</Button>
</div>
);
}
In my OeTestfallAccordion the prop testData does not update. If i try to console.log it inside my childComponent it has the old Value like before i execute the sendSinglecase function. What do i need to do, that i update the Data correctly that my child component gets notified that the props had changed and it has to rerender.
EDIT:
I tried some new things and can narrow down the problem. In my TestAutomationTab Component i send the whole data State to the OeTestfallAccordion Child Component. In this OeTestfallAccordion Component i split up the Array of Data which consists of multiple Objects like:
0: {id: 41, testfallid: 1, json: "{\"testCaseData\":{\"baseData\":{\"Check\":\"Thing…e\":\"alle\",\"tuwid\":\"2909\"}},\"testType\":\"Test\"}}", ID: null, businessId: null, …}
1: {id: 42, testfallid: 2, json: "{\"testCaseData\":{\"baseData\":{\"testfallid\":\"1…e\":\"alle\",\"tuwid\":\"2909\"}},\"testType\":\"Test\"}}", edcomAuftragsId: null, businessId: null, …}
When i hit the function sendSingleCase in my Parent Component TestAutomationTab i just change one single Parameter of the Object. The whole construct of Data keeps the same. The Child Component doesnt recognize that i changed something in the Object of Data.
But i dont know why? I also tried to useEffect on Props change in my Child COmponent when the props are changed. But it never gets executed even tho some attributes got updated inside the props.data.
function OeTestfallAccordion(props) {
const testData = props.testData;
const [data, setData] = useState(testData);
useEffect(() => {
setData(testData);
console.log("triggered");
}, [props]);
...
}
Okay things worked out a bit.
I changed the sendSingleCase function to first Copy the whole state in a Temp variable. Change one Attribute inside an Object and then setData (inside useState) with the tempData Variable. So the whole State gets renewed and the child components recognize the change and rerender.
But it seems not to be very fast. Always to copy the whole Data in a new Variable and then reassign it is very Ressource heavy. Is there a better solution?
function sendSingleCase(id) {
const tempState = [...data];
tempState.forEach((e) => {
if (e.testfallid === id) {
e.status = "pressed";
console.log(e.status);
}
});
setData(tempState);
}
I'm trying to update the state of my component, but for some reason it keeps saying Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I'm trying to have a modal that opens onclick of a <div>. Within that modal is a text input that will update the state (notes). For some reason it's saying invalid hook call - why is that?
const openTestModal = () => {
let [notes, setNotes] = useState("");
let [openModal, setOpenModal] = useState(true);
let modalBody =
<div>
<TextInput
value={notes}
onChange={(value) => setNotes(value)}
/>
</div>
return (
<Modal
open={openModal}
onCancel={() => setOpenModal(false)}
onConfirm={() => console.log('works')}
body={modalBody}
/>
)
};
const TestHooks = () => {
return (
<div onClick={() => openTestModal()}>
Test
</div>
)
};
Seems like you tried to render testModal in react as an event, which's not a way to go, at all. Instead you must render your testModal as component, like that, so click on Test div will open your modal:
const TestModal = () => {
const [notes, setNotes] = useState("");
const modalBody = (
<div>
<TextInput
value={notes}
onChange={(value) => setNotes(value)}
/>
</div>
)
return (
<Modal
open={openModal}
onCancel={() => setOpenModal(false)}
onConfirm={() => console.log('works')}
body={modalBody}
/>
)
};
const TestHooks = () => {
const [openModal, setOpenModal] = useState(false);
return (
<React.Fragment>
<TestModal openModal={openModal} setOpenModal={setOpenModal} />
<div onClick={() => setOpenModal(true)}>
Test
</div>
<React.Fragment>
)
};
Hope it helps :)
It's not working because your testHooks is calling to setState from a separate component. Add you testHooks code into your openTestModal component. It would work as is if TestHooks was a child of openTestModal as well.
Just started to learn about Reack hooks but I cannot figure out if it is possible to write a simple hook (or should I use some other approach, e.g. useEffect along with useState) in order to control visibility of multiple elements by clicking on different buttons on page.
Let's say I have a simple app with 2 buttons and 2 "modal" windows:
const App = () => {
const [firstModalOpen, toggleFirstModal] = useState(false);
const [secondModalOpen, toggleSecondModal] = useState(false);
return (
<div>
<button onClick={() => toggleFirstModal(true)}>Open First Modal</button>
<button onClick={() => toggleSecondModal(true)}>Open Second Modal</button>
<FirstModal
{...props}
show={firstModalOpen}
toggleModal={toggleFirstModal}
/>
<SecondModal
{...props}
show={secondModalOpen}
toggleModal={toggleSecondModal}
/>
</div>
)
}
const FirstModal = (props) => {
const { toggleModal, ...rest } = props;
return (
<Modal
{ ...rest }
show={firstModalOpen}
onHide={() => props.toggleModal(false)}
>
First modal content...
</Modal>
)
}
const SecondModal = (props) => {
const { toggleModal, ...rest } = props;
return (
<Modal
{ ...rest }
show={secondModalOpen}
onHide={() => props.toggleModal(false)}
>
Second modal content...
</Modal>
)
}
// state hook attempt
const useToggleModal = () => (init) => {
const [show, setToggleModal] = useState(init);
const toggleModal = () => setToggleModal(!show);
return { show, toggleModal };
};
Since those are react-bootstrap modal windows, they use show and onHide properties to determine/handle visibility and I have to pass rest prop to avoid some side-effects.
If I'd use my hook attempt in my app, I'd handle both modals on any button click so I came up with the idea to pass a string (to both, buttons and modals) which would tell which modal exactly to handle, but that approach for some reason looked a bit wrong.
Is there a "smarter" way in React to handle this internally instead of passing strings around?
If you have multiple modals and only one of them needs to open at once, then you must use a single state which stores which modal is opened, kind of like a string having the id of the modal. However if you want to open multiple modals, you would store the isOpen prop differently
For the first case you would write your code like
const App = () => {
const [openModal, toggleModal] = useState('');
return (
<div>
<button onClick={() => toggleModal('first')}>Open First Modal</button>
<button onClick={() => toggleModal('second')}>Open Second Modal</button>
<FirstModal
{...props}
show={openModal === 'first'}
toggleModal={toggleModal}
/>
<SecondModal
{...props}
show={secondModalOpen}
toggleModal={toggleModal}
/>
</div>
)
}
const FirstModal = (props) => {
const { toggleModal, ...rest } = props;
return (
<Modal
{ ...rest }
show={firstModalOpen}
onHide={() => props.toggleModal('first')}
>
First modal content...
</Modal>
)
}
const SecondModal = (props) => {
const { toggleModal, ...rest } = props;
return (
<Modal
{ ...rest }
show={secondModalOpen}
onHide={() => props.toggleModal('second')}
>
Second modal content...
</Modal>
)
}
For the second case it would be as you have written in your example, the only optimisation you can do for the second case is to store an array of modal objects and render them dynamically or let each modal handle its own toggle states and use useImperativeHandle to provide methods which parent can call to child modals like
const App = () => {
const firstRef = useRef(null);
const secondRef = useRef(null);
return (
<div>
<button onClick={() => this.firstRef.current.toggleModal()}>Open First Modal</button>
<button onClick={() => this.secondRef.current.toggleModal()}>Open Second Modal</button>
<FirstModal
{...props}
ref={firstRef}
/>
<SecondModal
{...props}
ref={secondRef}
/>
</div>
)
}
const FirstModal = forwardRef((props, ref) => {
const { showModal, toggleModal } = useToggleModal(false, ref);
return (
<Modal
{ ...rest }
show={showModal}
onHide={toggleModal}
>
First modal content...
</Modal>
)
})
const SecondModal = forwardRef((props, ref) => {
const { showModal, toggleModal } = useToggleModal(false, ref);
return (
<Modal
{ ...props }
show={showModal}
onHide={toggleModal}
>
Second modal content...
</Modal>
)
})
// state hook attempt
const useToggleModal = (init, ref) => {
const [show, setToggleModal] = useState(init);
const toggleModal = () => setToggleModal(!show);
useImperativeHandle(ref, () => ({
toggleModal
}))
return { show, toggleModal };
};