I have a test site HERE
Please do:
Visit the site and click on the hamburger icon in the top right. The
side nav should open.
Perform the same action again and you will see the problem I am
currently having.
The side nav does not close properly because I have two conflicting functions operating.
The first conflicting function is an onClick toggle function within the actual hamburger component which toggles an associated context state.
The second conflicting function is used by the side nav panel component. It is bound to a hook which uses its ref to check whether the user has clicked inside or outside of the component.
These functions work up until the user clicks on the hamburger menu whilst the side nav is open. The hamburger is technically outside of the side nav component but overlayed on top of it, so the click does not register as outside. This results in both functions firing one after the other. The "click outside" hook fires and sets the side nav context to false, closing it. The hamburger toggle function then fires, finds the context set to false and changes it back to true. Thus instantly closing and then reopening the side nav.
The solution I have attempted looks like the below. I tried to assign a ref within the hamburger child component and pass it to the side nav parent. I wanted to try and compare the callback returned from useClickedOutside to the toggleRef given to the parent but the hamburger component and then if they match then do nothing. This, I was hoping, would knock one of the functions out of action for that interaction. Not entirely sure this is the best way to attempt to achieve something like this.
Perhaps I could get the side nav bounding box and then check if the click coordinates land within it?
//SideNav.jsx
const SideToggle = ({ sideNavToggle, sideIsOpen, setToggleRef }) => {
useEffect(() => {
const toggleRef = React.createRef();
setToggleRef(toggleRef);
}, []);
return (
<Toggle onClick={sideNavToggle}>
<Bars>
<Bar sideIsOpen={sideIsOpen} />
<Bar sideIsOpen={sideIsOpen} />
<Bar sideIsOpen={sideIsOpen} />
</Bars>
</Toggle>
);
};
export default function SideNav() {
const [toggleRef, setToggleRef] = useState(null);
const { sideIsOpen, sideNavToggle, setSideIsOpen } = useContext(
NavigationContext
);
const handlers = useSwipeable({
onSwipedRight: () => {
sideNavToggle();
},
preventDefaultTouchmoveEvent: true,
trackMouse: true,
});
const sideRef = React.createRef();
useClickedOutside(sideRef, (callback) => {
if (callback) {
if (callback === toggleRef) {
return null;
} else setSideIsOpen(false);
}
});
return (
<>
<SideToggle
sideIsOpen={sideIsOpen}
sideNavToggle={sideNavToggle}
setToggleRef={setToggleRef}
/>
<div ref={sideRef}>
<Side sideIsOpen={sideIsOpen} {...handlers}>
<Section>
<Container>
<Col xs={12}>{/* <Menu /> */}</Col>
</Container>
</Section>
</Side>
</div>
</>
);
}
The useClickedOutside hook used above.
//use-clicked-outside.js
export default function useClickedOutside(ref, callback) {
useEffect(() => {
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
callback(event.target);
} else return null;
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
EDIT
Managed to fix part of the problem by moving the toggle component inside of the side nav component. It now has its own problem in that the hamburger component completely rerenders when the side nav props change. Trying to figure out how to stop the hamburger rerendering in a hooks scenario such as this. I've been reading that React.memo may be the answer but unsure how to implement it as of yet.
//SideNav.jsx
export default function SideNav() {
const { sideIsOpen, sideNavClose, sideNavOpen } = useContext(
NavigationContext
);
const handlers = useSwipeable({
onSwipedRight: () => {
sideNavClose();
},
preventDefaultTouchmoveEvent: true,
trackMouse: true,
});
const sideRef = React.createRef();
useClickedOutside(sideRef, (callback) => {
if (callback) {
sideNavClose();
}
});
const SideToggle = () => {
const handleClick = () => {
if (!sideIsOpen) {
sideNavOpen();
}
};
return (
<Toggle onClick={() => handleClick()}>
<Bars>
<Bar sideIsOpen={sideIsOpen} />
<Bar sideIsOpen={sideIsOpen} />
<Bar sideIsOpen={sideIsOpen} />
</Bars>
</Toggle>
);
};
return (
<>
<SideToggle />
<div ref={sideRef}>
<Side sideIsOpen={sideIsOpen} {...handlers}>
<Section>
<Container>
<Col xs={12}>{/* <Menu /> */}</Col>
</Container>
</Section>
</Side>
</div>
</>
);
}
I'd recommend using this package: https://github.com/airbnb/react-outside-click-handler . It handles some edge cases you mention, you can check their source code if you are interested in the details.
Then you need to keep the information about whether the sidebar is opened in the state and pass it down to the affected components somewhat like this (this way you can also change how the site looks in other places depending on the toolbar state, eg. disable scrollbar etc):
function Site() {
const [isOpened, setToolbarState] = React.useState(false);
return (
<React.Fragment>
<Toolbar isOpened={isOpened} setToolbarState={setToolbarState} />
{isOpened ? (<div>draw some background if needed</div>) : null}
<RestOfTheSite />
</React.Fragment>
);
}
function Toolbar(props) {
const setIsClosed = React.useCallback(function () {
props.setToolbarState(false);
}, [props.setToolbarState]);
const setIsOpened = React.useCallback(function () {
props.setToolbarState(true);
}, [props.setToolbarState]);
return (
<OutsideClickHandler onOutsideClick={setIsClosed}>
<div className="toolbar">
<button onClick={setIsOpened}>open</button>
...
{props.isOpened ? (<div>render links, etc here</div>) : null}
</div>
</OutsideClickHandler>
);
}
This way you won't necessary need to juggle refs and handlers around too much.
The way I answered this was to avoid detecting a click outside of the side nav altogether and instead detected if the click was outside of the main body of the site.
If true, then I call the navigation context and set side nav to false the same as I was trying in the side nav itself.
Related
I have a modal that I want to appear on a button click. This works fine, however, in order for it to be positioned correctly, I need to move the modal component to be rendered in a parent component.
I'm unsure how to do this as I'm not sure how I would pass the state down to the component where the button click is created. I'm also unsure of whether this is about passing a state or using a button onClick outside the component the state is defined in.
My code works and does what I want, but, I need <DeleteWarehouseModal show={show} /> to be in the parent component below. How can I do this?
The parent component where I want to render my modal:
function WarehouseComponent(props) {
return (
<section>
<div>
<WarehouseListItems warehouseData={props.warehouseData} />
//Modal component should go here
</div>
</section>
);
}
The component (WarehouseListItems) where the modal is currently being rendered:
function WarehouseListItem(props) {
const [show, setShow] = useState(false);
return (
<>
//some code necessary to the project, but, irrelevant to this issue
<Link>
<div onClick={() => setShow(true)}></div>
</Link>
<DeleteWarehouseModal show={show} />
</>
);
}
The modal:
const DeleteWarehouseModal = (props) => {
if (!props.show) {
return null;
}
return (
//some elements here
);
};
Yes, you can move the state and it's handler in WarehouseComponent component and pass the handler down to child component, which can change the state in parent component.
WarehouseComponent :-
function WarehouseComponent(props) {
const [show, setShow] = useState(false);
const toggleShow = () => {
setShow(state => !state);
}
return (
<section>
<div>
<WarehouseListItems
warehouseData={props.warehouseData}
toggleShow={toggleShow}
/>
<DeleteWarehouseModal show={show} />
</div>
</section>
);
}
WarehouseListItems : -
function WarehouseListItem(props) {
const {toggleShow} = props;
return (
<>
//some code necessary to the project, but, irrelevant to this issue
<Link>
<div onClick={() => toggleShow()}></div>
</Link>
</>
);
}
I recently learned a bit about react and I'm confused about how I would transfer data between two components.
Currently, I have 2 functions implemented as such:
I have topbar.tsx which shows the topbar information, such as showing some button to open the sidebar (which is my next function).
import Sidebar from './sidebar'
export default topbar(props) {
return (
<Drawer
open={isOpen}
onclose={anotherfunction}>
<Sidebar />
</Drawer>
)
}
I also have sidebar.tsx which contains the implementation of sidebar. (This is, if I understand react terminology correctly, the child).
import CloseButton from 'react-bootstrap/CloseButton';
export default Sidebar() {
return (
<CloseButton />
)
}
The issue I'm facing is that, since the topbar is the function that controls the opening and closing of the sidebar, however, I want a close button to appear on my sidebar and do exactly as is said. How would I be able to transfer state information between these two files such that I can open sidebar and close it with the button in sidebar.
Thank you!
You elevate your state to the parent component, and pass event handler functions through.
For instance:
// Holds state about the drawer, and passes functions to mamange that state as props.
function Topbar() {
const [isOpen, setIsOpen] = useState(false)
return (
<Drawer isOpen={isOpen}>
<Sidebar onClose={() => setIsOpen(false)} />
</Drawer>
)
}
// Drawer will show it's children if open.
function Drawer({ isOpen, children }: { isOpen: boolean, children: React.ReactNode }) {
if (!isOpen) return null
return <div>{children}</div>
}
// Sidebar will send onClose to the button's onClick
function Sidebar({onClose}: { onClose: () => void }) {
return (
<CloseButton onClick={onClose} />
)
}
// Close button doesn't care what happens on click, it just reports the click
function CloseButton({onClick}: { onClick: () => void }) {
return <div onClick={onClick} />
}
Playground
First of all, rename topbar to Topbar. Otherwise you can't render your component.
For your question, you can pass the props directly to Sidebar component too.
export default Topbar(props) {
return (
<Drawer
open={isOpen}
onclose={anotherfunction}>
<Sidebar open={isOpen}
onclose={anotherfunction}/>
</Drawer>
)
}
im facing this weird behavior when trying to update the parent component with an set function to the child with props
this hook is to open and close the modal to edit an element
//PARENT FILE
//hook
const [isEditModalOpen, setEditModalOpen] = useState(false)
//more code...
//modal
{isEditModalOpen && <EditExcerciseModal setEditModalOpen={setEditModalOpen} isEditModalOpen={isEditModalOpen} />}
and this is the child code
//CHILD FILE
export const EditExcerciseModal = ({setEditModalOpen, excerciseInfo,fetchExcercisesFromRoutine})
//more code etc etc
<div className="addExcerciseModalContainer">
<span onClick={() =>{ setEditModalOpen(false) }} className="xModal">X</span>
i checked and the onClick is working. if i change the parent state manually the Modal works fine and closes.
the weird case when it its working is when instead of calling the set function i create a function with a setTimeout without time like this:
function closeModal(){
setTimeout(() => { setEditModalOpen(false)}, 0);
}
any ideas?
thanks for the help
You need to create a separation of concern. A Modal consists of three parts
The Modal of its Self.
The Content of the Modal.
And the container of the two.
You should be using the useState() hook and calling setEditModalOpen in the same containing component.
You need to make sure that you're declaring and setting state inside the same component.
// children would be the content of the modal
const Modal = ({ children, selector, open, setOpen }) => {
// we need the useEffect hook so that when we change open to false
// the modal component will re-render and the portal will not be created
useEffect(() => {
setOpen(false);
//provide useEffect hook with clean up.
return () => setOpen(true);
}, [selector]);
return open ? createPortal(children, selector) : null;
};
export const EditExerciseModal = ({ close }) => {
return (
<div>
{/* Instead of creating a handler inside this component we can create it in it's parent element */}
<span onClick={close}>X</span>
{/* Content */}
</div>
);
};
export const ModalBtn = () => {
const [isEditModalOpen, setEditModalOpen] = useState(false);
// this is where it all comes together,
// our button element will keep track of the isEditModalOpen variable,
// which in turn will update both child elements
// when true useEffect hook will re-render Modal Component only now it "will" createPortal()
// when our EditExerciseModal alls close it will set change the isEditModalOpen to false
// which will be passed to the Modal component which
// will then cause the component to re-render and not call createPortal()
return (
<>
<button onClick={() => setEditModalOpen(true)}>EditExerciseModal</button>
{setEditModalOpen && (
<Modal
open={isEditModalOpen}
setOpen={setEditModalOpen}
selector={'#portal'}>
<div className='overlay'>
<EditExerciseModal close={() => setEditModalOpen(false)} />
</div>
</Modal>
)}
</>
);
};
I am trying to build a React Context that will let me know if a area is hovered over or not.
The issue I have currently is with the onMouseOver and onMouseOut which seem to occur a lot so the hover state switches from true to false frequently even though I am hovering over an area.
Here is what I have so far:
const { Provider, Consumer } = createContext({
isHovering: false,
});
Provider.propTypes = {
value: T.shape({
isHovering: T.bool.isRequired,
}),
};
function HoverContextProvider({ children }) {
const [isHovering, setIsHovering] = useState(false);
const value = {
isHovering,
};
return (
<Provider value={value}>
<div
onMouseOver={() => {
setIsHovering(true);
}}
onMouseOut={() => {
setIsHovering(false);
}}
>
{children}
</div>
</Provider>
);
}
HoverContextProvider.propTypes = {
children: T.element.isRequired,
};
export { HoverContextProvider as default, Consumer as HoverContextConsumer };
What I do the is wrap an area with that like this:
<HoverContextProvider> some stuff in here, imagine a page header</HoverContextProvider>
Here is an visual reference:
So as i move my mouse around the green area (which contains lots of text), the isHovering state keeps switching from false to true - I guess because of the text.
Does anyone know how to make this work?
I have a modal that is completely self contained. The modal is opened via going to the modal route and all the functionality to close the modal from button or outside clicks is within the modal component. Basically the modal is not controlled by any parent passing state. I was given a task of making the modals button customizable, meaning passing in a new button component, so we can add the modal to our lib instead of copy pasting the code in projects. Lol this seemed simple enough, and maybe it is and I am just overthinking this.
I cant paste the actual code but I can use a contrived example. This is a very simplified version of the modal, keeping in mind it opens via route so there's really no state and setState in the actual code. Also here is a fiddle
const ModalHeader = ({ onClose }) => {
return (
<div className="modal__header">
<button
className="modal__close-btn"
data-testid="modal-close-button"
onClick={onClose}
/>
</div>
);
};
const Modal = ({ children }) => {
const [state, setState] = React.useState(true);
const handleCloseOutsideClick = () => {
setState(false);
};
const handleCloseButtonClick = () => {
setState(false);
};
const renderModal = () => {
return (
<div className="modal-overlay" onClick={handleCloseOutsideClick}>
<div className="modal">
<ModalHeader onClose={handleCloseButtonClick} />
{children}
</div>
</div>
);
};
return state ? renderModal() : null;
};
const App = () => {
return (
<Modal>
<div>Modal Children</div>
</Modal>
);
};
ReactDOM.render(<App />, document.querySelector('#app'));
I tried a few things, initially I attempted to find a way to pass in a new header component containing a button. Then as I got into the code I realized what I was doing would lose the self contained functionality of the modal. My approach was along the lines of below but obviously the onClick would be an issue since invoking the close functionality is internal.
So I tried using cloneElement to add props within the component if the custom header was detected:
// inside modal component
React.useEffect(() => {
React.Children.map(children, (child: React.ReactElement) => {
if (child && child.type === ModalHeader) {
setHederFound(true);
}
});
}, []);
// inside modal render:
<div className={modalClasses} onClick={stopPropagation}>
{!headerFound ? (
<ModalDefaultHeader onClose={handleCloseButtonClick} />
) : (
React.Children.map(children, (child: React.ReactElement) => {
if (child && child.type === ModalHeader) {
return React.cloneElement(child, {
onClose: handleCloseButtonClick,
});
}
})
)}
{children}
</div>;
Obviously that did not work because there's no onClick in the custom button. Anyways I am thinking that I am over complicating this. I just need a way to pass in a custom button while leaving the functionality internal to the modal. Any assistance would be appreciated.
Thanks in advance.