I have a working code (Flask + plain JS) which shows the growing log file in real time.
I have a problem converting it to ReactJS.
app.py (Flask, working code, no problems)
#app.route('/stream')
def stream():
def generate():
fname="/Users/xxx/tmp/log.txt"
with open(fname) as f:
while True:
yield f.read()
sleep(1)
return
app.response_class(generate(), mimetype='text/plain')
app.run()
index.html (plain JS, no problems)
<pre id="output"></pre>
<script>
var output = document.getElementById('output');
var xhr = new XMLHttpRequest();
xhr.open('GET', '');
xhr.send();
setInterval(function() {
output.textContent = xhr.responseText;
}, 1000);
</script>
Below is my attempt to convert it to React.
On MyPage.jsx I put the button "Show Real Time Log".
When user clicks this button the Modal Dialog should pop-up and display the log.
File: MyPage.jsx
import Modal from 'react-bootstrap/Modal'
import { Button } from 'react-bootstrap'
const showLog = (e) => {
render(<ModalLog />, document.getElementById('output'));
}
const MyPage = () => {
return (
<div>
<div id="output"></div>
<button type="button" onClick={showLog}>Show Real Time Log</button>
</div
}
function ModalLog() {
const [show, setShow] = useState(true);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
return (
<>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Real Time Log</Modal.Title>
</Modal.Header>
<Modal.Body> Log file live update goes here </Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
}
Questions:
Where to put into MyPage.jsx the JS code currently sitting in my index.html?
When I press the button "Show Real Time Log" the Modal is displayed only 1st time, after I close it never displayed again. I understand what it is because handleClose() was called, but how to fix it?
(1) Where to put into MyPage.jsx the JS code currently sitting in my index.html?
Simple answer is you don't. It's directly manipulating the DOM and this is a severe react anti-pattern. You would convert it to an useEffect hook to fetch the latest log data and save it to component state, to be rendered.
const [logs, setLogs] = useState('');
useEffect(() => {
// asynchronous function to fetch logs
const fetchLogs = async () => {
try {
const logResponse = await /* logic to fetch log */
setLogs(logResponse);
} catch {
// just don't update state, or set an error message in state,
// basically anything you want to do to handle errors
}
}
// setup interval to fetch logs
const intervalTimer = setInterval(fetchLogs, 1000);
// function to clean up effect when component unmounts
return () => clearInterval(intervalTimer);
}, []); // empty dependency array to run when mounted
(2) When I press the button "Show Real Time Log" the Modal is displayed only 1st time, after I close it never displayed again. I understand what it is because handleClose() was called, but how to fix it?
You initial state to display the modal is true. I would refactor the logic a bit to have MyPage hold the state if the modal is open or not (instead of the modal). Pass show and handleClose as props to ModalLog. No need to render the modal into another DOM element, I believe the react-bootstrap will handle creating a portal for you. You can simply render it into your component.
const MyPage = () => {
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
return (
<div>
<div id="output"></div>
<button type="button" onClick={handleShow}>Show Real Time Log</button>
<ModalLog show={show} onHide={handleClose} />
</div>
);
}
function ModalLog({ show, onHide }) {
return (
<>
<Modal show={show} onHide={onHide}>
<Modal.Header closeButton>
<Modal.Title>Real Time Log</Modal.Title>
</Modal.Header>
<Modal.Body> Log file live update goes here </Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
}
Suggestion: Place the log state and logic to fetch and display the logs in the modal as it appears you only care to display them when the modal is open and there's no point in fetching them in the background while the modal is closed.
Related
I have an Antd popover, that by clicking a button inside its content, opens a modal.
I want to close the popover when the modal opens.
When I tried just passing the popover visibility state setter down to the modal as a prop, there was a problem. There was some kind of "collision" between the state of the modal and the passed down prop state of the popover:
Collision CodeSandbox example
I did find a workaround - creating the modal state variables in the parent component (the popover) and passing them down to the modal using props:
Working CodeSandbox example
First of all, you can notice that the modal isn't closing at it supposed to - there's no nice smooth animation minimizing it, it just suddenly disappears. For reference, you can look here to see how it should look like when closing.
So my question is - why did this collision happen? Is there a better way to solve it?
Thanks!
This collision happens because in show modal handler you set visibility of popover to false and hide it and ant-popover-hidden class add to it's div element so anything inside it would not display like Modal however you show modal but because of its parent it couldn't visible, so I think You must separate modal from the popover content and place it somewhere beside them like this:
const Test = () => {
const [isSharePopoverVisible, setIsSharePopoverVisible] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const handlePopoverVisibleChange = () => {
setIsSharePopoverVisible(!isSharePopoverVisible);
};
const handleOk = () => {
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
};
const showModal = () => {
setIsModalVisible(true);
setIsSharePopoverVisible(false);
};
return (
<>
<Popover
trigger="click"
title="Test"
visible={isSharePopoverVisible}
onVisibleChange={handlePopoverVisibleChange}
content={
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
}
>
<Button>Test</Button>
</Popover>
<Modal
title="Basic Modal"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
>
<p>Some contents...</p>
</Modal>
</>
);
};
I have a modal that opens on click event. It gets the event data (date and time string) and displays it inside the modal. The problem is that my state is always one step behind. The content being displayed by the modal is always in the previous state. When I close the modal and reopen it, It does render the content but It is the previous event. How to solve this issue? am I using the react hooks wrong? I cannot post the whole code as It's quite large so I am just posting the parts where I am using hooks, please let me know if you need more information I will provide it.
const [isModalVisible, setIsModalVisible] = useState(false)
const [modelContent, setModalContent] = useState('')
const [modalTitle, setModalTitle] = useState('')
const handleEventAdd = (e) => {
form.resetFields()
setModalTitle('Add an Event')
const event = _.cloneDeep(e)
form.setFieldsValue({
datePicker: dateString,
timeRangePicker: [dateString, eventEnd.date],
})
}
const content = () => (
<div>
<Form
form={form}
initialValues={{
datePicker: moment(e.start),
timeRangePicker: [moment(e.start), moment(e.end)],
}}
>
<Form.Item name="datePicker" label="Current Event Date">
<DatePicker
className="w-100"
format={preferredDateFormat}
onChange={setValueDate}
/>
</Form.Item>
<Form.Item>
<Button
onClick={() => {
form
.validateFields()
.then((values) => {
form.resetFields()
})
}}
>
Add Event
</Button>
</Form.Item>
</Form>
</div>
)
setModalContent(content)
setIsModalVisible(true)
}
const handleModalClose = () => {
setIsModalVisible(false)
form.resetFields()
setModalContent('')
}
UPDATE::
my return function consists,
<Modal visible={isModalVisible} footer={null} onCancel={handleModalClose} title={modalTitle}>
{modelContent}
</Modal>
This problem is due to the asynchronous behavior of setState. This method generates a "pending state transition" (see this for more information). In order to solve this issue, you have two options:
use ref instead of state. In contrast to state, ref has synchronous behavior. I have developed an example for myself that you can check out here. you can see that there is an asynchronous behavior in the state (dialog box vs. in webpage).
You can explicitly pass the new state value as part of the event being raised.
I have a React component with Ant Design Modal inside it and I am trying to test that modal gets opened when a button it clicked:
The component:
const ModalComponent = () => {
const [visible, setVisible] = useState(false);
return (
<>
<Button type="primary" onClick={() => setVisible(true)}>
Open Modal
</Button>
<Modal
title="Modal title"
centered
visible={visible}
onOk={() => setVisible(false)}
onCancel={() => setVisible(false)}
>
<p>some contents...</p>
<p>some contents...</p>
<p>some contents...</p>
</Modal>
</>
);
};
Test file:
test('modal opening', async () => {
const { queryByText } = render(<ModalComponent />);
fireEvent.click(queryByText('Open Modal'));
await waitFor(() => expect(queryByText('Modal title')).toBeInTheDocument());
});
The problem is that the modal DOM is never rendered in the test when I try to debug, so the test fails. It could be happening because the modal content is created outside of the component DOM tree right inside the body tag?
There is no test failure that you have given from our side.
A little information from my side on Antd modal component.
Antd Modal during testing renders outside the container. This is because Antd uses the rc-dialog component and that component uses react portal to show modal which is always render outside the root div. In the same way, during testing modal will not render in the container but outside of it.
The test that you have given will pass(modal is present) because the queryByText will search the element in document.body not inside the container.
test('modal opening', async () => {
const { baseElement, queryByText } = render(<ModalComponent />);
fireEvent.click(queryByText('Open Modal'));
expect(baseElement).toMatchSnapshot(); // added snapshot
await waitFor(() => expect(queryByText('Modal title')).toBeInTheDocument());
});
baseElement will show all the elements that are present in the document.body.
I had this exact same use case, with a very similar component.
For some reason, if I triggered the event inside act, then the state of the component wouldn't be updated and the modal would never come up (even if a console.log in the event handler does show up in the console).
The solution was to do userEvent.click outside the act
I have a form within a modal and I want to, on Update Info button click, take the data from that form and have them in a list as the value on a hidden input that is outside the modal so that I can save the info together with the rest of form inputs. Here's the code I have so far for the modal section, I would really appreciate it if someone would point me to how I can go about this.
const modalFormInput = ({id, value}) =>
<div className='col-sm-3'>
<Button onClick={handleShow}>
Edit Personal Info
</Button>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>EDIT PERSONAL INFO</Modal.Title>
</Modal.Header>
<Modal.Body>
<Row key={id} className={'form-group'}>
<FormText model={model}
path={path(id, 'name')}
label={Official Name}
value={value}
error={state.nameErrors[id]}
/>
</Row>
</Modal.Body>
<Modal.Footer>
<Button>
Update Information
</Button>
</Modal.Footer>
</Modal>
</div>
I used React Docs to do the modal, I'm very new to React so any help will be greatly appreciated.
I'm not sure what follows is helpful.
I'd need a bit more surrounding context to provide a complete answer (and your example seems to be missing some critical information, e.g. where handleShow and handleClose come from), but I can try to illuminate a common pattern for this kind of thing.
Summarized: Pass an event handler function as a prop and have the component call that function to communicate information back to the parent component.
Here's a contrived example of the basic pattern.
It renders an input with an onChange handler and some text displaying the current value.
When the user types in the input, the input invokes the handler with an event object. (This is built-in behavior)
The onNameChange handler updates the component's state with the value from the event.
The new value is reflected in the text on the subsequent render. (Updating state triggers a re-render, so this happens automatically.)
function SomeComponent () {
// keep track of the name in state so we can use it for other stuff
const [name, setName] = React.useState('');
// define a change event handler for the input
const onNameChange = event => {
// event.target is the input
// event.target.value is the new value
const newValue = event.target.value;
// update state with the new value
setName(newValue);
}
return (
<div>
<input value={name} onChange={onNameChange} />
<span>Hello, {name}</span> {/* updates as the user types */}
</div>
)
}
Extending this to more closely resemble your example, SomeComponent could accept an event handling prop of its own to be invoked when a button is clicked:
function SomeComponent ({onButtonClick}) { // accepts an 'onButtonClick' prop
// same as before
const [name, setName] = React.useState('');
// same as before
const onNameChange = event => {
const newValue = event.target.value;
setName(newValue);
}
return (
<div>
<input value={name} onChange={onNameChange} />
{/* call the handler with the current value of name */}
<button onClick={() => onButtonClick(name)}>Click Me</button>
</div>
)
}
With that in place, a different component can now use SomeComponent to do something with name when the button is clicked:
function SomeOtherComponent () {
const buttonClickHandler = (name) => alert(name); // or whatever
return (
<SomeComponent onButtonClick={buttonClickHandler} />
);
}
I am noticing that my React Bootstrap modal wont open when I put the following condition : onHide={props.setShowModal(false)}. When I change it to onHide={props.setShowModal(true)} then it always stays open.
What is going wrong ?
PARENT JS --------
const [showModal, setShowModal] = useState(false);
<AddModal showModal={showModal} setShowModal={setShowModal} />
MODAL -------
import React, { useState, useEffect } from 'react';
import { Button, Form, FormControl, Modal } from "react-bootstrap";
const AddModal = (props) => {
const handleClose = () => {
props.setShowModal(false);
}
return (
<Modal show={props.showModal} animation={false}
// onHide={props.setShowModal(false)}
>
<Modal.Header closeButton>
<Modal.Title>Enter Item</Modal.Title>
</Modal.Header>
{
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}
>
Cancel
</Button>
<Button variant="primary" onClick={handleClose}
>
Save Changes
</Button>
</Modal.Footer>}
</Modal>
);
}
export default AddModal;
This happens because props.setShowModal(false) is immideatly called when your modal is rendered. You can fix it by changing your code to onHide={() => props.setShowModal(false)}.
This is a common trap. onHide (as well as onClick and any other event handlers) expects a function that will be called when event is fired. props.setShowModal is a function, props.setShowModal(false) is not. So your options are either pass a lamda function as I suggested, or create a function
hideModal() {
props.setShowModal(false)
}
and pass it to onHide.