I have a Modelmenu that is nested in the parent component, it implements the function of opening a modal window by click. Also in this parent component there is a child component to which you need to bind the function of opening a modal window by one more element. I can do this if all the logic is in the parent component, but this is a bad practice, since I will have to duplicate the code on each page in this way. How can I do this? I'm sorry, I'm quite new, I can't understand the callback.
Parent:
const Home: NextPage = () => {
const handleCallback = (handleOpen: any) => {
}
return (
<>
<ModalMenu parentCallback={handleCallback}/>
<Slider handleCallback={handleCallback}/>
</>
)
}
Modal:
export const ModalMenu: FC = (props) => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
props.parentCallback(setPlay(!play))
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return
}
Child:
export const Slider: FC= (props) => {
return (
<>
<Image nClick={props.handleCallback}/>
</>
I did as advised in the comments using hook, it works fine, maybe it will be useful to someone. Custom hook is really convenient
export const useModal = () => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
setPlay(!play)
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return {
play, handleOpen, handleClose
}
}
If you're looking to pass down values and/or functions from a parent to two or more children, I think it's better to just have the values and functions in the parent
Parent :
const Home: NextPage = () => {
const [play, setPlay] = useState<boolean>(false)
const handleOpen = () => {
setPlay(prev => !prev)
};
const handleClose = () => {
setPlay(false)
setPlay(!play)
};
return (
<>
<ModalMenu handleOpen={handleOpen} handleClose={handleClose} play={play}/>
<MainSlider <ModalMenu handleOpen={handleOpen} handleClose={handleClose} play={play}/>
</>
)
}
if you want to pass an interface to the props in the children for typescript your interface will look something like this
interface iProps {
play : boolean;
handleOpen : () => void;
handleClose : () => void;
}
export const ModalMenu: FC = (props):iProps => {
// you can easily access all you want
const {handleClose, handleOpen, play} = props
return
}
export const Slider: FC= (props): iProps => {
const {handleClose, handleOpen, play} = props
return (
<>
<Image onClick={handleOpen}/>
</>
Related
Is there a way to do this?
I want to save a ChildComponent's state into an array state from the GrandParentComponent whenever I click a button from the ParentComponent?
Here's an overview of my components:
const GrandParentComponent = () => {
const [array, setArray] = useState([]);
return (
<div>
<ParentComponent array={array} setArray={setArray} />
</div>
);
};
const ParentComponent = ({ array, setArray }) => {
const ref = useRef(null);
const handleClick = () => {
ref.current.setArray();
};
return (
<div>
<button onClick={handleClick}>save</button>
{array.map((item) => (
<ChildComponent array={array} setArray={setArray} ref={ref} />
))}
</div>
);
};
const ChildComponent = forwardRef(({ array, setArray }, ref) => {
const [childState, setChildState] = useState("")
useImperativeHandle(ref, () => {
return {
setArray: () => {
setArray((array) => [{ ...childState }, ...array]);
}
};
});
return <div>ChildComponent</div>;
});
The problem I'm encountering is it only saves the value of the last ChildComponent. What I want is to save all of the values from the ChildComponents.
What am I doing it wrong here? Or is there a better or correct way of doing what I'm trying to do?
Say I want to create a modal and handle the state through Reacts Context API
export const ModalUIProvider: FC = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const openModal = useCallback(() => setIsOpen(true), [setIsOpen]);
const closeModal = useCallback(() => setIsOpen(false), [setIsOpen]);
const toggleModal = useCallback(
() => setIsOpen(!isOpen),
[isOpen, setIsOpen]
);
const modalContext = useMemo(() => {
return {
isModalOpen: isOpen,
openModal,
closeModal,
toggleModal,
};
}, [isOpen, openModal, closeModal, toggleModal]);
return (
<ModalContext.Provider value={modalContext}>
{children}
</ModalContext.Provider>
);
};
export const useModal = () => {
return useContext<IModalContext>(ModalContext);
};
<ModalUIProvider>
<ParentModal />
</ModalUIProvider>
const { isModalOpen, openModal, closeModal } = useModal();
Now, within this modal I want to have a button, that upon being clicked opens another model ontop of the current one.
How can I do this, without interfering with the wrapping context?
I'm refactoring some old code for an alert widget and am abstracting it into its own component that uses DOM portals and conditional rendering. I want to keep as much of the work inside of this component as I possibly can, so ideally I'd love to be able to expose the Alert component itself as well as a function defined inside of that component triggers the render state and style animations so that no outside state management is required. Something like this is what I'm looking to do:
import Alert, { renderAlert } from '../Alert'
const CopyButton = () => (
<>
<Alert text="Text copied!" />
<button onClick={() => renderAlert()}>Copy Your Text</button>
</>
)
Here's what I currently have for the Alert component - right now it takes in a state variable from outside that just flips when the button is clicked and triggers the useEffect inside of the Alert to trigger the renderAlert function. I'd love to just expose renderAlert directly from the component so I can call it without the additional state variable like above.
const Alert = ({ label, color, stateTrigger }) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger])
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
};
export default Alert;
Though it's not common to "reach" into other components and invoke functions, React does allow a "backdoor" to do so.
useImperativeHandle
React.forwardRef
The idea is to expose out the renderAlert function imperatively via the React ref system.
Example:
import { forwardRef, useImperativeHandle } from 'react';
const Alert = forwardRef(({ label, color, stateTrigger }, ref) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger]);
useImperativeHandle(ref, () => ({
renderAlert,
}));
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
});
export default Alert;
...
import { useRef } from 'react';
import Alert from '../Alert'
const CopyButton = () => {
const ref = useRef();
const clickHandler = () => {
ref.current?.renderAlert();
};
return (
<>
<Alert ref={ref} text="Text copied!" />
<button onClick={clickHandler}>Copy Your Text</button>
</>
)
};
A more React-way to accomplish this might be to abstract the Alert state into an AlertProvider that renders the portal and handles the rendering of the alert and provides the renderAlert function via the context.
Example:
import { createContext, useContext, useState } from "react";
interface I_Alert {
renderAlert: (text: string) => void;
}
const AlertContext = createContext<I_Alert>({
renderAlert: () => {}
});
const useAlert = () => useContext(AlertContext);
const AlertProvider = ({ children }: { children: React.ReactElement }) => {
const [text, setText] = useState<string>("");
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
...
const renderAlert = (text: string): void => {
setAlertRendered(false);
setAlertVisible(false);
setText(text);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
const ele = <div>{alertRendered && <div> ..... </div>}</div>;
return (
<AlertContext.Provider value={{ renderAlert }}>
{children}
// ... portal ...
</AlertContext.Provider>
);
};
...
const CopyButton = () => {
const { renderAlert } = useAlert();
const clickHandler = () => {
renderAlert("Text copied!");
};
return (
<>
<button onClick={clickHandler}>Copy Your Text</button>
</>
);
};
...
function App() {
return (
<AlertProvider>
...
<div className="App">
...
<CopyButton />
...
</div>
...
</AlertProvider>
);
}
I created a modal component that utilizes React's context, and hooks for easy usage. But for some reason, the element that I want to render in the modal doesn't appear.
Here's my code for the ModalContainer which contains the provider and the modal
ModalContainer.tsx
export type ModalElement =
| React.ComponentClass<any, any>
| React.FC<any>
| JSX.Element;
const ModalContainer: React.FC = ({ children }) => {
const [visible, setVisible] = useState(false);
const modal = useRef<HTMLDivElement>(null);
const showModal = (element: ModalElement) => {
if (element && modal.current) {
if (React.isValidElement(element)) {
ReactDOM.render(element, modal.current);
} else {
const el = React.createElement(
element as React.ComponentClass<any, any> | React.FC<any>,
{}
);
ReactDOM.render(el, modal.current);
}
setVisible(true);
}
};
const dismissModal = () => {
setVisible(false);
};
return (
<ModalContext.Provider value={{ showModal, dismissModal }}>
{children}
<Modal visible={visible} onClose={dismissModal} ref={modal} />
</ModalContext.Provider>
);
};
export default ModalContainer;
useModal.ts
export const useModal = (element: ModalElement) => {
const context = useContext(ModalContext);
if (!context) {
Error(`Can't get modal context`);
}
const show = useCallback(() => {
context.showModal(element);
}, [context, element]);
const dismiss = useCallback(() => {
context.dismissModal();
}, [context]);
return [show, dismiss];
};
In a way that you'll use it as such:
const DivModal: React.FC = () => {
return <div>Test</div>
}
const Component: React.FC = () => {
const [show, dismiss] = useModal(DivModal);
const onClick = () => {
show();
}
return <button onClick={onClick}/>
}
The problem seems to happen when I create an element using React.createElement in the ModalContainer.tsx found specifically in this line:
const el = React.createElement(
element as React.ComponentClass<any, any> | React.FC<any>,
{}
);
ReactDOM.render(el, modal.current);
The modal renders properly if I replace el in the render function with a test JSX such as <div>test</div>. Can't really figure out what's wrong so I'll post it here just in case anyone knows what the problem is. Thanks!
I have a React HOC that hides a flyover/popup/dropdown, whenever I click outside the referenced component. It works perfectly fine when using local state. My HOC looks like this:
export default function withClickOutside(WrappedComponent) {
const Component = props => {
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const handleClickOutside = event => {
if (ref?.current && !ref.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => setOpen(false);
}, [ref]);
return <WrappedComponent open={open} setOpen={setOpen} ref={ref} {...props} />;
};
return Component;
}
When in use I just wrap up the required component with the HOC function
const TestComponent = () => {
const ref = useRef();
return <Wrapper ref={ref} />;
}
export default withClickOutside(TestComponent);
But I have some flyover containers that are managed from Redux when they are shown, or when they are hidden. When the flyover is shown, I want to have the same behavior, by clicking outside the referenced component to hide it right away. Here's a example of a flyover:
const { leftFlyoverOpen } = useSelector(({ toggles }) => toggles);
return (
<div>
<Wrapper>
<LeftFlyoverToggle
onClick={() => dispatch({ type: 'LEFT_FLYOVER_OPEN' })}
>
...
</Wrapper>
{leftFlyoverOpen && <LeftFlyover />}
{rightFlyoverOpen && <RightFlyover />}
</div>
);
Flyover component looks pretty straightforward:
const LefFlyover = () => {
return <div>...</div>;
};
export default LefFlyover;
Question: How can I modify the above HOC to handle Redux based flyovers/popup/dropdown?
Ideally I would like to handle both ways in one HOC, but it's fine if the examples will be only for Redux solution
You have a few options here. Personally, I don't like to use HOC's anymore. Especially in combination with functional components.
One possible solution would be to create a generic useOnClickOutside hook which accepts a callback. This enables you to dispatch an action by using the useDispatch hook inside the component.
export default function useOnClickOutside(callback) {
const [element, setElement] = useState(null);
useEffect(() => {
const handleClickOutside = event => {
if (element && !element.contains(event.target)) {
callback();
}
};
if (element) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [element, callback]);
return setElement;
}
function LeftFlyOver() {
const { leftFlyoverOpen } = useSelector(({ toggles }) => toggles);
const dispatch = useDispatch();
const setElement = useOnClickOutside(() => {
dispatch({ type: 'LEFT_FLYOVER_CLOSE' });
});
return (
<Dialog open={leftFlyoverOpen} ref={ref => setElement(ref)}>
...
</Dialog>
)
}