I'm currently having a problem that I can't explain. I work with React, typescript and react query.
The code on which I work is a double modal and when clicking on the refuse button of the second one launches a call via react query then we execute an onSuccess.
If I move the hook call into the first modal the onSuccess fires.
If I move the OnSuccess from the second modal into the hook it works too.
But I don't understand why in this case it doesn't work...
Does anyone have an idea/explanation please?
Thanks in advance.
Here is the code below of the first modal
import React from 'react'
import Button from '../Button'
import SecondModal from '../SecondModal'
interface FirstModalProps {
isOpen: boolean
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
id?: string
}
const FirstModal = ({ id, isOpen, setIsOpen }:
FirstModalProps) => {
const [openRefuse, setOpenRefuse] = React.useState<boolean>(false)
return (
<>
<SecondModal isOpen={openRefuse} setIsOpen={setOpenRefuse} id={id} />
<Button
onClick={() => {
setIsOpen(false)
setOpenRefuse(true)
}}
text="refuse"
/>
</>
)}
export default FirstModal
Then the code of the second modal
import React from 'react'
import ConfirmModal from '../../../../../shared/styles/modal/confirm'
import useUpdate from '../../hooks/use-update'
interface SecondModalProps {
isOpen: boolean
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
id?: string
}
const SecondModal = ({ isOpen, setIsOpen, id }: SecondModalProps) => {
const { mutate: update } = useUpdate()
const updateT = () => {
update(
{
id
},
{
onSuccess: () => {
console.log('OnSuccess trigger')
}
}
)
}
return (
<ConfirmModal
close={() => {
setIsOpen(false)
}}
isOpen={isOpen}
validate={updateT}
/>
)}
export default SecondModal
Then the hook
import { useMutation } from 'react-query'
interface hookProps {
id?: string
}
const useUpdate = () => {
const query = async ({ id }: hookProps) => {
if (!id) return
return (await myApi.updateTr(id))()
}
return useMutation(query, {
onError: () => {
console.log('error')
}
})}
export default useUpdate
callbacks passed to the .mutate function only execute if the component is still mounted when the request completes. On the contrary, callbacks on useMutation are always executed. This is documented here, and I've also written about it in my blog here.
Related
I am trying to open a modal by updating the state of the component. The component is wrapped in a Context Provider.
Although the button seems to be clicking successfully, the Modal will not open
Here is the code with the container which contains the "Open Modal Button"
import { type FC, useRef } from 'react'
import infomation from '#/assets/icons/infomation.svg'
import { useModal } from '#/providers/ModalProvider'
import styles from './Instructions.module.scss'
const Instructions: FC = () => {
const card = useRef<HTMLDivElement>(null)
const { openDemoModal } = useModal()
const onOpenModalClick = () => {
openDemoModal()
console.log(openDemoModal)
}
return (
<section ref={card} className={`card ${styles.card}`}>
<div className={styles.background} />
<div>OPEN THE MODAL DOWN BELOW</div>
<button variant="outlined" fullWidth onClick={onOpenModalClick}>
Open Modal
</button>
</section>
)
}
export default Instructions
Here is the file which contains the Context for the Modal, I have tried setting up the context in INITIAL_STATE and tried updating it using the "onOpenModalClick" function - but it doesn't seem to be able to update the ShowModal.current value below.
import { type FC, type PropsWithChildren, createContext, useContext, useRef } from 'react'
import Modal from '#/components/modal/Modal'
type ContextValue = {
showModal: boolean
openDemoModal: () => void
}
const INITIAL_STATE: ContextValue = {
showModal: false,
openDemoModal: () => {},
}
export const ModalContext = createContext(INITIAL_STATE)
export const ModalProvider: FC<PropsWithChildren> = ({ children }) => {
const showModal = useRef(INITIAL_STATE.showModal)
const openDemoModal = () => {
showModal.current = true
}
console.log(showModal.current)
return (
<ModalContext.Provider value={{ showModal: showModal.current, openDemoModal }}>
{children}
<Modal show={showModal.current} setShow={(shouldShow: boolean) => (showModal.current = shouldShow)} />
</ModalContext.Provider>
)
}
export function useModal() {
const context = useContext(ModalContext)
if (!context) {
throw new Error('useModal must be used within a ModalProvider')
}
return context
}
Is there any way to update the onOpenModalClick button to make it change the value of showModal.current in the Provider file?
Sorry if the post is unclear, this is my first Stack Overflow post. Let me know if I need to post anymore of the components.
I tried to add a button to the component which updates the Context, however the state failed to update
The useRef does not trigger a re-render when the value changes. Instead you can use a useState hook. Which would look something like this.
type ContextValue = {
showModal: boolean;
openDemoModal: () => void;
};
const INITIAL_STATE: ContextValue = {
showModal: false,
openDemoModal: () => console.warn('No ModalProvider'),
};
export const ModalContext = createContext(INITIAL_STATE);
export const ModalProvider: FC<PropsWithChildren> = ({ children }) => {
const [showModal, setShowModal] = useState(false);
const openDemoModal = () => {
setShowModal(true)
};
console.log(showModal);
return (
<ModalContext.Provider
value={{ showModal, openDemoModal }}
>
{children}
<Modal
show={showModal}
setShow={setShowModal}
/>
</ModalContext.Provider>
);
};
I am writing a small UiKit for pop-ups and i've run up into a problem. My code structure is looking somewhere like this:
<MainPopup>
<Popover />
<MainPopup />
And pop-ups structure can be presented like this:
<KeyboardListener>
<Portal>
// some logic here...
<Portal />
<KeyboardListener/>
<Portal /> component opens pop-up in the React.createPortal(...) and <KeyboardListener /> component looks like this (it basically adds event listener to track when user presses Escape button):
import { FC, useEffect } from "react"
interface KeyboardListenerProps {
onClose: () => void
children: React.ReactNode
}
const KeyboardListener: FC<KeyboardListenerProps> = ({onClose, children}) => {
useEffect(() => {
const closeOnEscapeKey = (e: KeyboardEvent) => {
e.stopImmediatePropagation()
if (e.key === 'Escape') onClose()
}
document.body.addEventListener('keydown', closeOnEscapeKey)
return () => document.body.removeEventListener('keydown', closeOnEscapeKey)
}, [onClose])
return (
<>
{children}
</>
)
}
export default KeyboardListener
But there is a problem! When I am opening pop-over and want to close it by pressing Escape the main pop-up is also getting closed.
Question: Is there a way to close a specific (last-opened) portal with pop-up with an Escape key?
Forgot to mention - every new portal is appended to the document after the previous one, here is the code:
import { FC, useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
interface PortalProps {
children: React.ReactNode
}
const Portal: FC<PortalProps> = ({ children }) => {
const [container] = useState(() => document.createElement('div'))
useEffect(() => {
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [container])
return ReactDOM.createPortal(children, container)
}
export default Portal
let every popup have some id, everytime you open popup add this id to some array, let onClose take last id from array and unmount only this popup
I solved this issue in kind of imperative way. I've added <div id="portals" /> to index.html file and now I am putting all my portals here. Also, now, when a new <Portal /> is created it must have a unique id, portal component looks like this:
import { FC, useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
import KeyboardListener from '../../KeyboardListener/KeyboardListener'
interface PortalProps {
children: React.ReactNode
onClose: () => void
id: string
}
const Portal: FC<PortalProps> = ({ children, onClose, id }) => {
const [container] = useState(() => document.createElement('div'))
container.id = id
const portals = document.getElementById('portals')!
useEffect(() => {
portals.appendChild(container)
return () => {
portals.removeChild(container)
}
}, [container, id, onClose, portals])
return ReactDOM.createPortal(
<KeyboardListener id={id} onClose={onClose} portals={portals}>
{children}
</KeyboardListener>, container)
}
export default Portal
Then I've created <KeyboardListener /> component which is listening Escape button press. Every new pop-up onClose() is closured in different <Portal />, so I am taking the last element from every time button is clicked and calling specific onClose for portal with appropriate id:
import React, { FC, useEffect } from 'react'
interface KeyboardListenerProps {
portals: HTMLElement
onClose: () => void
id: string
children: React.ReactNode
}
const KeyboardListener: FC<KeyboardListenerProps> = ({
portals,
onClose,
id,
children
}) => {
useEffect(() => {
const closeOnEscapeKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
const portals = document.getElementById('portals')!
if (
portals.children &&
portals.children[portals.children.length - 1]?.id === id
) {
onClose()
}
}
}
document.body.addEventListener('keydown', closeOnEscapeKey)
return () => document.body.removeEventListener('keydown', closeOnEscapeKey)
}, [id, portals, onClose])
return <>{children}</>
}
export default KeyboardListener
Bonus:
I also know more elegant way to solve this issue, but I haven't realized it yet. You need a store in which you will put onClose() every time a new <Portal /> is created. Then every time Escape button is pressed you will call the last onClose() in this store.
The onClickHandler in the following code, in this component, 'SearchResult', sometimes work and sometimes not.
I can't figure out any logic that can explain why it works when it works, and why it's not working, when it's not working.
I've put a debugger inside the onClickHandler, at the beginning of it, and when it's not working, it doesn't get to the debugger at all - what indicates that the function sometimes isn't even called, and I can't figure out why.
Furthermore, I've tried to move all the code in function to the onClick, inline, but then, it's not working at all.
In addition, I've tried to use a function declaration instead of an arrow function, and it still behaves the same - sometimes it works, and sometimes it's not...
This is the site, you can see the behavior for yourself, in the search box.
This is the GitHub repository
Here you can see a video demonstrating how it's not working, except for one time it did work
Please help.
The problematic component:
import { useDispatch } from 'react-redux'
import { Col } from 'react-bootstrap'
import { getWeatherRequest } from '../redux/weather/weatherActions'
import { GENERAL_RESET } from '../redux/general/generalConstants'
const SearchResult = ({ Key, LocalizedName, setText }) => {
const dispatch = useDispatch()
const onClickHandler = () => {
dispatch({ type: GENERAL_RESET })
dispatch(
getWeatherRequest({
location: Key,
cityName: LocalizedName,
})
)
setText('')
}
return (
<Col className='suggestion' onClick={onClickHandler}>
{LocalizedName}
</Col>
)
}
export default SearchResult
This is the parent component:
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Form } from 'react-bootstrap'
import { getAutoCompleteResultsRequest } from '../redux/autoComplete/autoCompleteActions'
import { AUTO_COMPLETE_RESET } from '../redux/autoComplete/autoCompleteConstants'
import SearchResult from './SearchResult'
const SearchBox = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()
const autoComplete = useSelector((state) => state.autoComplete)
const { results } = autoComplete
const onChangeHandler = (e) => {
if (e.target.value === '') {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}
setText(e.target.value)
dispatch(getAutoCompleteResultsRequest(e.target.value))
}
const onBlurHandler = () => {
setTimeout(() => {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}, 100)
}
return (
<div className='search-box'>
<Form inline>
<div className='input-group search-md search-sm'>
<input
type='search'
name='q'
value={text}
onChange={onChangeHandler}
onBlur={onBlurHandler}
placeholder='Search Location...'
className='mr-sm-2 ml-sm-3 form-control'
/>
</div>
</Form>
<div className='search-results'>
{results &&
results.map((result) => {
return (
<SearchResult key={result.Key} {...result} setText={setText} />
)
})}
</div>
</div>
)
}
export default SearchBox
I played a bit with your code and it looks like a possible solution may be the following addition in the SearchResult.js:
const onClickHandler = (e) => {
e.preventDefault();
...
After some tests
Please remove the onBlurHandler. It seams to fire ahaed of the onClickHandler of the result.
Can you put console.log(e.target.value) inside the onChangeHandler,
press again search results and make sure that one of it doesn't working and show us the console.
In searchResult component print to the console LocalizedName as well
I have a Parent component and couple of child components. I need to disable or enable the button in the parent based on the ErrorComponent. If there is an error then I disable the button or else I enable it. I believe we can pass callbacks from the child to parent and let the parent know and update the button property. I need to know how to do the same using react hooks? I tried few examples but in vain. There is no event on error component. If there is an error (props.errorMessage) then I need to pass some data to parent so that I can disable the button. Any help is highly appreciated
export const Parent: React.FC<Props> = (props) => {
....
const createContent = (): JSX.Element => {
return (
{<ErrorPanel message={props.errorMessage}/>}
<AnotherComponent/>
);
}
return (
<Button onClick={onSubmit} disabled={}>My Button</Button>
{createContent()}
);
};
export const ErrorPanel: React.FC<Props> = (props) => {
if (props.message) {
return (
<div>{props.message}</div>
);
}
return null;
};
I'd use useEffect hook in this case, to set the disabled state depending on the message props. You can see the whole working app here: codesandbox
ErrorPanel component will look like this:
import React, { useEffect } from "react";
interface IPropTypes {
setDisabled(disabled:boolean): void;
message?: string;
}
const ErrorPanel = ({ setDisabled, message }: IPropTypes) => {
useEffect(() => {
if (message) {
setDisabled(true);
} else {
setDisabled(false);
}
}, [message, setDisabled]);
if (message) {
return <div>Error: {message}</div>;
}
return null;
};
export default ErrorPanel;
So depending on the message prop, whenever it 'exists', I set the disabled prop to true by manipulating the setDisabled function passed by the prop.
And to make this work, Parent component looks like this:
import React, { MouseEvent, useState } from "react";
import ErrorPanel from "./ErrorPanel";
interface IPropTypes {
errorMessage?: string;
}
const Parent = ({ errorMessage }: IPropTypes) => {
const [disabled, setDisabled] = useState(false);
const createContent = () => {
return <ErrorPanel setDisabled={setDisabled} message={errorMessage} />;
};
const handleSubmit = (e: MouseEvent) => {
e.preventDefault();
alert("Submit");
};
return (
<>
<button onClick={handleSubmit} disabled={disabled}>
My Button
</button>
<br />
<br />
{createContent()}
</>
);
};
export default Parent;
I want to display a snackBar whenever a user signIn successfully or something wrong was happen.
MyComponent.jsx:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import SnackBar from '../../components/SnackBar';
import SignInForm from './SignInForm';
const SingInContainer = ({ message, variant}) => {
const [open, setSnackBarState] = useState(!!variant);
const handleClose = () => {
setSnackBarState(false)
};
return (
<div>
<SnackBar
open={open}
handleClose={handleClose}
variant={variant}
message={message}
/>
<SignInForm/>
</div>
)
}
SingInContainer.propTypes = {
variant: PropTypes.string.isRequired,
message: PropTypes.string.isRequired
}
const mapStateToProps = (state) => {
const {variant, message } = state.snackBar;
return {
variant,
message
}
}
export default connect(mapStateToProps)(SingInContainer);
The open prop always set to false, however variant is updated after the component was rerenderd by connect HOC. I didn't found what's my mistake?
The parameter to useState is only used once on initial update, if you want to perform an update to state when the variant prop is updated you can use useEffect hook
const SingInContainer = ({ message, variant}) => {
const [open, setSnackBarState] = useState(!!variant);
useEffect(() => {
setSnackBarState(!!variant);
}, [variant])
const handleClose = () => {
setSnackBarState(false)
};
return (
<div>
<SnackBar
open={open}
handleClose={handleClose}
variant={variant}
message={message}
/>
<SignInForm/>
</div>
)
}