Calling a function from another hooked component with ref/forwardRef - reactjs

I'm trying to call a function from another component, with the old fashion react Class style I was able to do it easily, since I'm trying to hooked everything I'm facing this kind of issue
This code doesn't work when we call setText() using the reference :
export function MyComp(props, ref) {
const [theText, setText] = useState(props.theText);
return (
<div>
<h1>{theText}</h1>
<button
onClick={e => {
setText("clicked with inside button");
}}
>
inside button
</button>
<button
onClick={e => {
setText("not clicked");
}}
>
reinit
</button>
</div>
);
}
export const MyRefComp = React.forwardRef((props, ref) => (
<MyComp ref={ref} {...props}>
{props.children}
</MyComp>
));
function App() {
const compref = useRef();
return (
<div>
<MyRefComp ref={compref} theText="not clicked" />
<button
onClick={e => {
compref.current.setText("clicked with outside button");
}}
>
outside button
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
here is the editable code : https://codesandbox.io/s/reactforwardrefproblem-ublk0
Thanks for helping

Here is the answer to your question, but i don't think it's a good pattern to do like this.
You need explain what are you trying to do, so we can help you. I assume context or HOC is what you needed.
Working example.

Thanks #RTW,
It's incredible how many combinaisons I tried and I didn't manange to do it.
Context or HOC won't fit in my case.
I've also simplified it to avoid the intermediaite component, and allow multiple calls with an object that contains the func.
here is it :
const MyComp = React.forwardRef((props, ref) => {
const [theText, setText] = useState(props.theText);
ref.current = { setText: setText };
return (
<div>
<h1>{theText}</h1>
<button
onClick={e => {
setText("clicked with inside button");
}}
>
inside button
</button>
<button
onClick={e => {
setText("not clicked");
}}
>
reinit
</button>
</div>
);
});
function App() {
let compref = useRef();
return (
<div>
<MyComp ref={compref} theText="not clicked" />
<button
onClick={e => {
compref.current.setText("clicked with outside button");
}}
>
outside button
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
https://codesandbox.io/s/react-example-x194f

Related

React-testing-library: getByTestId() or queryByTestId() not working, thros Unable to find an element by data-testid

component is like:
<Button and <Icon are customized components which wrap up the <button and <icon
const handleToggleShow = useCallback(() => {
setShow(!show);
}, [show]);
const displayUI = (
<div>
<Icon
testId="editIcon"
onClick={handleToggleShow}
className="edit-icon"
>
</div>
);
const editUI = (
<form data-testid="form" onSubmit={handleSubmit}
<InputComponent />
<Button
testId="saveButton"
text="Save"
disabled={...}
size="large"
color="blue"
type="submit"
/>
<Button
testId="cancelButton"
text="Cancel"
disabled={...}
size="large"
color="grey"
onClick={handleClickCancel}
/>
</form>
);
return(
<div>
{show ? editUI}
{!show? displayUI}
</div>
);
Test is like:
test("show render edit ui when click button", () => {
render(<A {...props} />)
const icon = screen.getByTestId("editIcon");
expect(icon).toBeInDocument();
fireEvent.click(element);
const form = screen.getByTestId("form");
//here throws error: unable to find an element by [data-testid="form"]
expect(form).toBeInDocument();
});
Then I tried queryByTestId("form") and tried getByTestId("saveButton"), it throws error "received value must be an HTMLElement or as an SVGElement",
I was thinking maybe icon click event was not triggered, then I ran this test, still got error
test("show render edit ui when click button", () => {
const handleToggleShow = jest.fn();
render(<A {...props} />)
const icon = screen.getByTestId("editIcon");
expect(icon).toBeInDocument();
fireEvent.click(element);
expect(handleToggleShow).toHaveBeenCalled(); //received number of calls 0
});
Anyone can help? why getByTestId or queryByTestId is not working
Update here:
In the previous test, I didn't pass any mock props to the component.
after passing props, the issue somehow fixed.

Hide modal on click outside in react hooks

i have a modal component in my react app and i need to close it on click outside
import React from "react";
import ReactDOM from "react-dom";
import style from "./Modal.module.scss";
const Modal = ({ isShowing, hide, childrenContent, childrenHeader }) =>
isShowing
? ReactDOM.createPortal(
<React.Fragment>
<div className={style.modalOverlay} />
<div
className={style.modalWrapper}
aria-modal
aria-hidden
tabIndex={-1}
role="dialog"
>
<div className={style.modal}>
<div className={style.modalHeader}>
{childrenHeader}
<button
type="button"
className={style.modalCloseButton}
data-dismiss="modal"
aria-label="Close"
onClick={hide}
>
<span aria-hidden="true">×</span>
</button>
</div>
{childrenContent}
</div>
</div>
</React.Fragment>,
document.body
)
: null;
export default Modal;
i was try to use this solution but it's not work in my code, how can i fix it?
Just a tip, when looking at the html you can use the native <dialog> tag, this is the semantically correct way to display a dialog type pop-up box, which yours looks to be.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
Dialog has a showModal() method, and a .close() method. This would be a better way of displaying a pop-up type dialog, than using <div> tags. It also allows you to use the native HTML5 methods, rather than trying to provide a work around using React.
I would reccomend this method over trying to look for work arounds
const Modal = ({ children, showModal, toggleModal }) => {
const wrapperRef = React.useRef(null);
const closeModal = React.useCallback(
({ target }) => {
if (
wrapperRef &&
wrapperRef.current &&
!wrapperRef.current.contains(target)
) {
toggleModal();
}
},
[toggleModal]
);
React.useEffect(() => {
document.addEventListener("click", closeModal, { capture: true });
return () => {
document.removeEventListener("click", closeModal, { capture: true });
};
}, [closeModal]);
return showModal
? ReactDOM.createPortal(
<>
<div ref={wrapperRef} className="modal">
{children}
</div>
</>,
document.body
)
: null;
};
Modal.propTypes = {
children: PropTypes.node.isRequired,
showModal: PropTypes.bool.isRequired,
toggleModal: PropTypes.func.isRequired
};
export default Modal;
in your parent component :
const Parent = () => {
const [showModal, setModalState] = React.useState(false);
const toggleModal = React.useCallback(() => {
setModalState((prevState) => !prevState);
}, []);
return (
<div>
<Modal showModal={showModal} toggleModal={toggleModal}>
<h1>Hello!</h1>
... some other childrens
<button
onClick={toggleModal}
>
Close
</button>
</Modal>
</div>
);
};

removeEventListener() is not working by React component

I tried any workaround I can think ,
but still cant remove EventListener .
here are all my ways of thinking
I cant think of any other way to solve it .
hope someone can tell me what can i do
delete directly
function doSomething(){}
const [testP, setTestP] = useState();
useEffect(() => { setTestP(document.querySelector("#test")); }, [testP]);
function App(){
return(
<>
<p id="test"></p>
<button onClick={ testP.addEventListener("click",doSomething); }></button>
<button onClick={ testP.removeEventListener("click",doSomething); }></button>
< />
);
}
use one useEffect() hook
function doSomething(){}
const [testP, setTestP] = useState();
useEffect(() => { setTestP(document.querySelector("#test")); }, [testP]);
const [do, setDo] = useState(false);
useEffect(() => {
if(do === true) testP.addEventListener("click", doSomething);
else testP.removeEventListener("click", doSomething);
}, [do]);
function App(){
return(
<>
<p id="test"></p>
<button onClick={ setDo(true); }></button>
<button onClick={ setDo(false); }></button>
< />
);
}
use two useEffect() hook
function doSomething(){}
const [testP, setTestP] = useState();
useEffect(() => { setTestP(document.querySelector("#test")); }, [testP]);
const [enable, setEnable] = useState(true);
const [disable, setDisable] = useState(true);
useEffect(() => { testP.addEventListener("click", doSomething); }, [enable]);
useEffect(() => { testP.removeEventListener("click", doSomething); }, [disable]);
function App(){
return(
<>
<p id="test"></p>
<button onClick={ setEnable(!enable); }></button>
<button onClick={ setDisable(!disable); }></button>
< />
);
}
use useState hook
function doSomething(){}
const [foo, setFoo] = useState();
function App(){
return(
<>
<p id="test" onClick={foo}></p>
<button onClick={ setFoo(doSomething); }></button>
<button onClick={ setFoo(null); }></button>
< />
);
}
As far as I understand you are trying to add and remove Event listener on p by clicking on buttons. So If you want to do that then you can simply make use of useRef hook, there is no need of using useState or useEffect. You can take the ref of p using useRef and simply attach eventListener or detach event listener by clicking on buttons.
CODESANDBOX LINK
import { useRef } from "react";
export default function Comp() {
const pRef = useRef(null);
function doSomething() {
console.log("Loggin from doSomething");
}
function attachEventListener() {
if (pRef.current) pRef.current.addEventListener("click", doSomething);
}
function detachEventListener() {
if (pRef.current) pRef.current.removeEventListener("click", doSomething);
}
return (
<>
<p id="test" ref={pRef}>
paragraph
</p>
<button onClick={attachEventListener}>addEventListener</button>
<button onClick={detachEventListener}>removeEventListener</button>
</>
);
}
You have to pass a function to onClick instead you are invoking it.
<button onClick={ setFoo(doSomething); }></button>
So above snippet should be
<button onClick={ () => setFoo(doSomething) }></button>
I would suggest using controlled component
function doSomething() {
console.log('Do something')
}
export function App(props) {
const [enabled, setEnabled] = React.useState(false)
return (
<div className='App'>
<p onClick={enabled ? doSomething : null}>paragraph</p>
<button onClick={() => setEnabled(true)}>Add event</button>
<button onClick={() => setEnabled(false)}>Remove event</button>
</div>
);
}

Unable to pass a function as a prop to child component in React

I'm trying to pass a function (addTask) as a prop from parent component (MainComponent.js) to Child Component (Button.js) but it's not working. I've also passed a variable (btnColor) as prop and it's working properly. What am I missing?
MainComponent.js:
import Button from "./Button";
const MainComponent = () => {
const addTask = () => {
console.log("Task Added...");
};
return (
<div>
<div> Header Component here </div>
<div> Some Data </div>
<Button onClick={addTask} btnColor="red" />
<div> Footer Component here </div>
</div>
);
};
export default MainComponent;
Button.js:
const Button = ({ addTask, btnColor }) => {
return (
<button style={{ backgroundColor: btnColor }} onClick={addTask}>
Add
</button>
);
};
export default Button;
I'm expecting the console to log 'Task Added...' but it isn't logging anything.
You're not passing it through correctly. You're passing it to onClick whereas you want to pass it through as addTask
Incorrect: <Button onClick={addTask} btnColor="red" />
Correct: <Button addTask={addTask} btnColor="red" />
I think it should be like this
const MainComponent = () => {
const addTask = () => {
console.log('Added new task...')
}
return (
<Button addTask={addTask} />
)
}
const Button = ({ addTask }) => {
return (
<button onClick={addTask} ></button>
)
}

How to pass props to Bootstrap OverlayTrigger overlay component?

I created a sandbox: https://codesandbox.io/s/happy-rgb-vks06?file=/src/App.js
I am trying to pass props to the SlotSettings component, but I get this error:
Warning: Function components cannot be given refs. Attempts to access
this ref will fail. Did you mean to use React.forwardRef()?
I tried to read both Bootstrap docs and React docs but I could not understand how this should work.
This is the code I'm using:
const SlotSettings = props => {
console.log(props.hello); // this works
return <Popover {...props} id="popover-basic">
<Popover.Title as="h3">Popover right</Popover.Title>
<Popover.Content>
And here's some <strong>amazing</strong> content. It's very engaging.
right?
</Popover.Content>
</Popover>
}
const getDaySlots = slots => {
if (slots.length >= 1) {
return slots.map(slot => {
const variant = slot.status === "free" ? "success" : "secondary";
const buttonRef = createRef();
return (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={<SlotSettings hello="hello" />}
rootClose
>
<Button size="sm" ref={buttonRef} variant={variant} style={{ margin: "8px"}}>{slot.start}</Button>
</OverlayTrigger>
)
});
}
return "No lessons available."
}
I accomplished this by utilizing the popperConfig property of OverlayTrigger.
PopperConfig is used to pass and object to the the underlying popper instance.
link to docs
Simple example:
function renderTooltip(props) {
let message = ""
//Sometimes, props.popper.state is undefined.
//It runs this function enough times that state gets a value
if (props.popper.state) {
message = props.popper.state.options.testObj
}
return (
<Tooltip id="button-tooltip" {...props}>
{message}
</Tooltip>
);
}
function getDaySlots(slots) {
//Other stuff
return (
<OverlayTrigger
placement="right"
delay={{ show: 250, hide: 400 }}
overlay={renderTooltip}
popperConfig={{testObj:"hello there"}}
>
<Button variant="success">Click here</Button>
</OverlayTrigger >
);
}
I messed with your codesandbox, but couldn't get popper to get a state value for some reason. What I posted above works for my project, hope this helps you get started.
import React, { useState ,useRef} from 'react';
import { Popover, Overlay } from 'react-bootstrap';
const DemoComponent = () => {
const [show, setShow] = useState(false);
const target = useRef(null);
return (
<div className="">
<span ref={target} onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
open tooltip
</span>
<Overlay target={target.current} show={show} placement="top">
{(props) => (
<Popover id="popover-basic" className="customize-tooltip" {...props}>
<Popover.Body>
put here dyanamic content
</Popover.Body>
</Popover>
)}
</Overlay>
</div>
);}
export default DemoComponent;

Resources