How to test if a react component was clicked - javascript

I have a react component called HelpButton, which looks like this:
import helpLogo from '../Resources/helplogo.svg';
function HelpButton(props) {
const [isOpen, setisOpen] = React.useState(false)
function toggle() {
setisOpen(prevIsOpen => !isOpen)
if (isOpen)
console.log("Help Open")
else
console.log("Help Closed")
}
return (
<div className="helpIconBgrnd" onClick={toggle} data-testid="helpIconBgrnd">
<img className="help-icon" src={helpLogo} alt='help-icon' />
</div>
)
}
export default HelpButton
I want to test if this component was clicked and I know there is an approach where you use expect(mockCallbackFunction).toHaveBeenCalledTimes(number). But the problem is that I do not pass the toggle function as a prop to HelpButton, instead it is declared inside the HelpButton component, so I don't think I can use this approach.
How can I test if the HelpButton component was clicked?

So you want to check in 'Some Component' whether HelpButton Component is open or not?
Try this:
import { useState } from 'react';
const Parent = () => {
const [isOpen, setisOpen] = useState(false);
//now you know if the component is open, you can do whatever you need with it
const handleToggle = () => setisOpen(prevState => !prevState);
return (
<HelpButton onToggle={handleToggle}/>
)
}
export default Parent;
const HelpButton ({ onToggle }) {
return (
<div className="helpIconBgrnd" onClick={onToggle} data-testid="helpIconBgrnd">
<img className="help-icon" src={helpLogo} alt='help-icon' />
</div>
);
}
export default HelpButton;

Try This. It's re-render while isOpen property is changed.
useEffect(()=>{
toggle();
},[isOpen]);

Related

React: Calling a child component's function when a tab is clicked

I am trying to create a React app that has two tabs(Tab1, Tab2) and when you click on Tab2, a message saying 'Function A is called' will appear in the console.
But it does not work with the following code.
The tabs works fine but no message on the console.
How can I solve it?
App.js
import MyComponent from './MyComponent';
import React from 'react';
import './App.css';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
function App() {
function handleTabSelect(index, last) {
if (index === 1 && myComponentRef.current) {
myComponentRef.current.funcA();
}
}
const myComponentRef = React.useRef();
return (
<div className="App">
<Tabs onSelect={handleTabSelect}>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanel>
HelloWorld123
</TabPanel>
<TabPanel>
<MyComponent ref={myComponentRef} />
</TabPanel>
</Tabs>
</div>
);
}
export default App;
MyComponent.jsx as a child component.
import React from 'react';
const MyComponent = React.forwardRef((props, ref) => {
function funcA() {
console.log('Function A is called');
myRef.current && myRef.current.focus();
}
// Save the ref to the DOM node you want to reference
const myRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
funcA: funcA
}));
return (
<div>
<p ref={myRef}>This is a message for Tab 2</p>
</div>
);
});
export default MyComponent;
Because of conditional render, TabPanel-2 can't forward anything to parent component. see here. https://beta.reactjs.org/reference/react/forwardRef#my-component-is-wrapped-in-forwardref-but-the-ref-to-it-is-always-null
At least two ways to call funcA.
call funcA by setTimeout in handleTabSelect;While TabPanel Has render to display in this render ,setTimeout will get the current target instead of null.
setTimeout(() => {
if (index === 1 && myComponentRef.current) {
myComponentRef.current.funcA();
}
}, 0);
send a prop to TabPanel-2 and use Effect to watch and call funcA.
const [index, setIndex] = useState(0)
function handleTabSelect(index, last) {
setIndex(index)
}
<MyComponent index={index} />
useEffect(() => {
if(props.index===1){
funcA();
}
}, [props.index])

How to receive data through props React and send it back?

I'm getting a placeholder value through props in my input component and I need to send the input value back to the main class. I'm using React but I'm not getting it. Follow my code.... The value I need to send is the value of 'usuario'
import React, { useState } from 'react';
import { EntradaDados } from './styled';
const PesqDados = ({placeholder, usuario}) => {
const [usuario, SetUsuario] = useState('')
const setValor =(e)=>{
SetUsuario(e.target.value);
}
console.log(usuario);
return(
<EntradaDados
onChange={setValor}
placeholder={placeholder}
>
</EntradaDados>
);
}
export default PesqDados;
You need to add a callback prop (onUsuarioChange) to your PesqDados component and call it with the new usuario. You have two options:
Call it from a useEffect with usuario as dependency (assuming usuario could get updated from somewhere other than setValor.
Call it from setValor, assuming that's the only place where usuario is going to get updated from.
This is how this should look:
import React, { useState } from 'react';
import { EntradaDados } from './styled';
const PesqDados = ({
placeholder,
usuario,
onUsuarioChange
}) => {
const [usuario, setUsuario] = useState('');
// Option 1:
useEffect(() => {
onUsuarioChange(usuario);
}, [usuario]);
const setValor = (e) => {
const nextUsuario = e.target.value;
setUsuario(nextUsuario);
// Option 2:
onUsuarioChange(nextUsuario);
};
return (
<EntradaDados
onChange={ setValor }
placeholder={ placeholder } />
);
}
export default PesqDados;
After studying properly, I found that I don't need to implement the function in the component page. I just needed to create a hook that calls the component's OnChange property on the component's page and then create a function just in the place where the component is installed. In this case, App.js.
Page Component....
const PesqDados = ({placeholder, Dados}) => {
return(
<EntradaDados
onChange={Dados}
placeholder={placeholder}
>
</EntradaDados>
);
}
export default PesqDados;
Page App.js
function App() {
const [usuario, SetUsuario] = useState('Aguardando Dados...')
const setValor =(e)=>{
SetUsuario(e.target.value);
}
const teste = ()=>{
alert("O usuário digitado foi : "+usuario)
};
return (
<>
<div className='divRepos'>
<div className='bloco'>
<div className='pesquisar'>
<p>{usuario}</p>
<PesqDados
placeholder={"Digite um username válido"}
Dados={setValor}
/>
<Button nomeBotao={"Pesquisar Perfil"}
onClick={teste}/>
</div>
...

React memo creates rerender

I'm having an issue with react memo when using nextjs. In the _app e.g. I have a button imported:
import { ChildComponent } from './ChildComponent';
export const Button = ({ classN }: { classN?: string }) => {
const [counter, setCounter] = useState(1);
const Parent = () => {
<button onClick={() => setCounter(counter + 1)}>Click me</button>
}
return (
<div>
{counter}
<Parent />
<ChildComponent />
</div>
);
};
Child component:
import React from 'react';
export const ChildComponent = React.memo(
() => {
React.useEffect(() => {
console.log('rerender child component');
}, []);
return <p>Prevent rerender</p>;
},
() => false
);
I made one working in React couldn't figure it out in my own app:
https://codesandbox.io/s/objective-goldwasser-83vb4?file=/src/ChildComponent.js
The second argument of React.memo() must be a function that returns true if the component don't need to be rerendered and false otherwise - or in the original definition, if the old props and the new props are equal or not.
So, in your code, the solution should be just change the second argument to:
export const ChildComponent = React.memo(
() => { ... },
// this
() => true
);
Which is gonna tell React that "the props didn't change and thus don't need to rerender this component".
So my issue was that I made a function called Button and returned inside a button or Link. So I had a mouseEnter inside the button which would update the state and handle the function outside the function. Kinda embarrassing. This fixed it. So the only change was I moved usestate and handlemousehover inside the button function.
const Button = () => {
const [hover, setHover] = useState(false);
const handleMouseHover = (e: React.MouseEvent<HTMLElement>) => {
if (e.type === 'mouseenter') {
setHover(true);
} else if (e.type === 'mouseleave') setHover(false);
};
return (
<StyledPrimaryButton
onMouseEnter={(e) => handleMouseHover(e)}
onMouseLeave={(e) => handleMouseHover(e)}
>
<StyledTypography
tag="span"
size="typo-20"
>
{title}
</StyledTypography>
<ChildComponent />
</StyledPrimaryButton>
);
};

Why is a function passed through props into a child component behaving differently on a key press vs. button click?

I have a React project where the parent component (functional) holds state to determine what formatting is applied to a list. When the user clicks a button, a modal is generated - I pass an anonymous function to that modal as props (onMainButtonClick), which when called flips the state and changes the formatting of the list (logic for this is in parent component).
When I use the button in my modal component (onClick={() => onMainButtonClick()}), the code works as expected. However, I would also like an enter press to trigger this. Therefore I have the following code implemented for this, but it doesn't function as expected. The modal closes and the function fires (I know this as I put a console log in there...) but the state that impacts the formatting is not changed.
EDIT: Having made a proposed change below (to memo'ize the onEnterPress function so it gets removed properly), here's the full code for the modal:
import { React, useRef, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";
const Modal = ({
title,
description,
isOpen,
onClose,
onMainButtonClick,
mainButtonText,
secondaryButtonText,
closeOnly,
}) => {
const node = useRef();
const onEnterPress = useCallback(
(e) => {
if (e.key === "Enter") {
onMainButtonClick();
}
},
[onMainButtonClick]
);
useEffect(() => {
window.addEventListener("keydown", onEnterPress);
return () => {
window.removeEventListener("keydown", onEnterPress);
};
}, [onMainButtonClick]);
const handleClickOutside = (e) => {
if (node.current.contains(e.target)) {
return null;
}
onClose();
};
if (!isOpen) return null;
return ReactDOM.createPortal(
{// JSX here, removed for brevity},
document.body
);
};
export default Modal;
And the code for the parent (which shows the modal and passes down onMainButtonClick):
import { React, useState } from "react";
import { decode } from "html-entities";
import useList from "../../queries/useList";
import useBuyItem from "../../mutations/useBuyItem";
import Header from "../Shared/Header";
import Description from "../Shared/Description";
import ListItem from "./ListItem";
import BuyOverlay from "./BuyOverlay";
import Modal from "../Modal";
import ViewOperations from "./ViewOperations";
const View = (props) => {
const [buyOverlayOpen, setBuyOverlayOpen] = useState(false);
const [viewBuyersOverlayOpen, setBuyersOverlayOpen] = useState(false);
const [viewBuyers, setViewBuyers] = useState(false);
const [buyItemId, setBuyItemId] = useState();
const { isLoading, isError, data, error } = useList(props.match.params.id);
const buyItemMutation = useBuyItem(buyItemId, props.match.params.id, () =>
setBuyOverlayOpen(false)
);
if (isLoading) {
return <div>Loading the list...</div>;
}
if (isError) {
return <div>An error occured, please refresh and try again</div>;
}
return (
<div className="w-full">
<div className="mb-10 text-gray-600 font-light">
<Header text={data.name} />
<Description text={data.description} />
<ViewOperations
toggleViewBuyers={() => setViewBuyers(!viewBuyers)}
toggleBuyersOverlay={() =>
setBuyersOverlayOpen(!viewBuyersOverlayOpen)
}
viewBuyers={viewBuyers}
/>
<div id="list" className="container mt-10">
{data.items.length === 0
? "No items have been added to this list"
: ""}
<ul className="flex flex-col w-full text-white md:text-xl">
{data.items.map((item) => {
return (
<ListItem
item={decode(item.item)}
description={decode(item.description)}
itemId={item._id}
isBought={decode(item.bought)}
boughtBy={decode(item.boughtBy)}
boughtDate={decode(item.boughtDate)}
viewlink={item.link}
handleBuy={() => {
setBuyItemId(item._id);
setBuyOverlayOpen(true);
}}
key={item._id}
viewBuyers={viewBuyers}
/>
);
})}
</ul>
</div>
</div>
<BuyOverlay
isOpen={buyOverlayOpen}
message="Message"
onClose={() => setBuyOverlayOpen(false)}
onConfirmClick={(buyerName) => {
buyItemMutation.mutate(buyerName);
}}
/>
<Modal
title="Title"
description="Description"
isOpen={viewBuyersOverlayOpen}
onClose={() => {
setBuyersOverlayOpen(false);
}}
onMainButtonClick={() => {
setViewBuyers(true);
setBuyersOverlayOpen(false);
}}
mainButtonText="OK"
secondaryButtonText="Cancel"
/>
</div>
);
};
export default View;
Any ideas for why this is happening? I have a feeling it might be something to do with useEffect here, but I'm a bit lost otherwise...
onEnterPress is most probably directly defined in your functional component and that's why its reference changes every time your component re-renders. Your useEffect closes over this newly defined function on each render so your handler is not going to work as you expected.
You can wrap your onEnterPress with useCallback to memoize your handler onEnterPress so its definition stays the same throughout each render just like this:
const onEnterPress = useCallback(e => {
if (e.key === "Enter") {
onMainButtonClick();
}
}, [onMainButtonClick]); // Another function dep. You can also wrap it with useCallback or carry over its logic into the callback here
useEffect(() => {
window.addEventListener('keydown', onEnterPress);
return () => {
window.removeEventListener('keydown', onEnterPress);
};
}, [onEnterPress]);
I figured this out in the end - turns out it was as simple as my enterPress callback needing a preventDefault statement before the if block (e.preventDefault()).
There must have been something else on the page that was capturing the enter press and causing it to behave strangely.

How to set the state using context.provider in react and typescript?

i am using context.provider usecontext reacthook to show a dialog. i set this around MainComponent. For the value attribute of context.provider i get error type {setDialogOpen(Open: boolean) => void} is not assignable to type boolean.
what i am trying to do?
I want to display a dialog when user clicks either a button in home or books component. on clicking hide button in dialog the dialog shouldnt be open.
below is my code,
function MainComponent() {
const DialogContext = React.createContext(false);
let [showDialog, setShowDialog] = React.useState(false);
return (
<DialogContext.Provider value={{
setDialogOpen: (open: boolean) => setShowDialog(open)}}>//get error
{showDialog && <Dialog DialogContext={DialogContext}/>
<Route
path="/items">
<Home DialogContext={DialogContext}/>
</Route>
<Route
path="/id/item_id">
<Books DialogContext={DialogContext}/>
</Route>
</DialogContext.Provider>
)
}
function Home({DialogContext} : Props) {
const dialogContext= React.useContext(DialogContext);
const handleClick = (dialogContext: any) {
dialogContext.setDialogOpen(true);
}
return (
<button onClick={handleClick(dialogContext)}>button1</button>
)
}
function Books({DialogContext} : Props) {
const dialogContext= React.useContext(DialogContext);
const handleClick = (dialogContext: any) {
dialogContext.setDialogOpen(true);
}
return (
<button onClick={handleClick(dialogContext)}>button2</button>
)
}
function Dialog({DialogContext}: Props) {
return(
<div>
//sometext
<button> hide</button>
</div>
)
}
I have tried something like below,
return (
<DialogContext.Provider value={{
setShowDialog(open)}}>//still get a error
{showDialog && <Dialog DialogContext={DialogContext}/>
)
Could someone help me fix this or provide a better approach to show the dialog on clicking a button in home and books component using usecontext hook. thanks.
There are few issues that you have to fix in your code.
You are creating context with the default value of false. Then later you try to override it to an object and hence the type error.
To fix the issue, create & export the context in separate file/helper. Don't pass them down as props.
import the context in parent and child components.
your handleClick fun in child component is missing an arrow.
the button onClick in child component is directly calling the function. You should pass just the function reference.
See the updated code with corrections below.
context helper
...
type ContextProps = {
setDialogOpen?: (open: boolean) => void,
};
export const DialogContext = React.createContext<ContextProps>({});
MainComponent
import {DialogContext} from '../contextHelper';
function MainComponent() {
// const DialogContext = React.createContext(false); //<---- remove this
let [showDialog, setShowDialog] = React.useState(false);
return (
<DialogContext.Provider value={{
setDialogOpen: (open: boolean) => setShowDialog(open)}}>
...
Home & Book Component
import {DialogContext} from '../contextHelper';
function Home() {
const dialogContext= React.useContext(DialogContext);
const handleClick = () => {
dialogContext.setDialogOpen(true);
}
return (
<button onClick={handleClick}>button1</button>
)
}
import {DialogContext} from '../contextHelper';
function Books() {
const dialogContext= React.useContext(DialogContext);
const handleClick = () => {
dialogContext.setDialogOpen(true);
}
return (
<button onClick={handleClick}>button2</button>
)
}

Categories

Resources