"React" how to pass event to adjacent component - javascript

I need to make a click on the button in one component and on this click call a function in the adjacent one. What's the easiest way?
I implemented like this. https://stackblitz.com/edit/react-l5beyi But I think you can do it much easier. React is new to me, and this construction looks strange ...
const App = () => {
const [isAdded, setIsAdded] = useState(false);
function handleClick(status) {
setIsAdded(status)
}
return (
<div>
<ComponentFirst
HandleClick={handleClick}
/>
<ComponentSecond
isAdded={isAdded}
handleCreate={handleClick}
/>
</div>
);
}
const ComponentFirst = ({ HandleClick }) => {
return (
<button
onClick={HandleClick}
>button</button>
)
}
const ComponentSecond = (props) => {
let { isAdded, handleCreate } = props;
const result = () => {
alert('work')
console.log('work')
}
React.useEffect(() => {
if (isAdded) {
result()
handleCreate(false);
}
}, [isAdded, handleCreate]);
return (
<></>
)
}

In your (contrived, I suppose) example the second component doesn't render anything, so it doesn't exist. The work should be done by the parent component:
const App = () => {
const handleClick = React.useCallback((status) => {
alert(`work ${status}`);
// or maybe trigger some asynchronous work?
}, []);
return (
<div>
<ComponentFirst handleClick={handleClick} />
</div>
);
};
const ComponentFirst = ({ handleClick }) => {
return <button onClick={() => handleClick("First!")}>button</button>;
};

You can also use a CustomEvent to which any component can listen to.
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
return (
<div>
<ComponentFirst />
<ComponentSecond />
</div>
);
}
const ComponentFirst = () => {
const handleClick = (e) => {
// Create the event.
const event = new CustomEvent("myCustomEventName", {
detail: "Some information"
});
// target can be any Element or other EventTarget.
window.dispatchEvent(event);
};
return <button onClick={handleClick}>button</button>;
};
const ComponentSecond = (props) => {
function eventHandler(e) {
console.log("Dispatched Detail: " + e.detail);
}
//Listen for this event on the window object
useEffect(() => {
window.addEventListener("myCustomEventName", eventHandler);
return () => {
window.removeEventListener("myCustomEventName", eventHandler);
};
});
return <></>;
};

Related

React: saving state of child component triggered by parent component

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?

Passing the callback function

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}/>
</>

React component rendered from object does not get unmounted

I have the following code, where I need to run clean-up when unmounting each component step. I've set a useEffect on each Step to check if the component has been unmounted. When the parent gets a new currentStep it swaps the currently active component but the clean-up never runs. I'm wondering if this has to do with the nature of the component being rendered from an object
const Step1 = () => {
useEffect(() => {
console.log("doing things here");
return () => {
console.log("clean-up should happen here but this won't print")
}
}, []}
}
const StepMap = {
step1: <Step1/>
step2: <Step2/>
step3: <Step3/>
}
const Parent = ({ currentStep }) => {
return (
<div>
{ StepMap[currentStep] }
</div>
)
}
Alternatively this piece of code does run the clean-up, but I do find the former cleaner
const Parent = ({ currentStep }) => {
return (
<div>
{ currentStep === "step1" && StepMap[currentStep]}
{ currentStep === "step2" && StepMap[currentStep]}
</div>
)
}
Why does the first approach not work? is there a way to make it work like the second while keeping a cleaner implementation?
if you want to write javascript inside jsx we have write it inside {} curly braces like this:
import React, { useEffect, useState } from "react";
const Step1 = () => {
useEffect(() => {
console.log("Step1 doing things here");
return () => {
console.log("Step1 clean-up should happen here but this won't print");
};
}, []);
return <div>stepOne</div>;
};
const Step2 = () => {
useEffect(() => {
console.log("Step2 doing things here");
return () => {
console.log("Step2 clean-up should happen here but this won't print");
};
}, []);
return <div>steptw0</div>;
};
const Step3 = () => {
useEffect(() => {
console.log("Step3 doing things here");
return () => {
console.log("Step3 clean-up should happen here but this won't print");
};
}, []);
return <div>stepthree</div>;
};
export const StepMap = {
step1: <Step1 />,
step2: <Step2 />,
step3: <Step3 />,
};
export const Parent = ({ currentStep }) => {
return <div>{StepMap[currentStep]}</div>;
};
const App = () => {
const [steps, setSteps] = React.useState("step1");
React.useEffect(() => {
setTimeout(() => setSteps("step2"), 5000);
setTimeout(() => setSteps("step3"), 15000);
}, []);
return <Parent currentStep={steps} />;
};
export default App;

Is it possible to expose a function defined within a React function component to be called in other components?

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>
);
}

Create HOC for outsideClick function, using Redux

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>
)
}

Categories