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

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

Related

onClick react error Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop

**I'm getting react Error while trying to change parent component styles onMouseEnter event. How could I change the styles via child component button?
The error is - Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
That seems odd for onMouseLeave={() => setHovered(false)} is an arrow function.
https://codesandbox.io/s/trusting-moon-djocul?
**
// App js
import React, { useState, useEffect } from "react";
import Categories from "./Categories";
import ShopPage from "./components/Products";
export default function App() {
const [data, setData] = useState(Categories);
useEffect(() => {
setData(data);
}, []);
return (
<div className="wrapper">
<ShopPage products={data} filterResult={filterResult} />
</div>
);
}
// Shopping page
const ShopPage = ({ products }) => {
return (
<>
<div>
<Products products={products} />
</div>
</>
);
};
// Parent component, the main part goes here
const Products = ({products}) => {
const [hovered, setHovered] = useState(false);
const [style, setStyle] = useState(false);
if (hovered) {
setStyle({
// inline styles
});
} else {
setStyle({
// inline styles
});
}
return (
<>
<Product setHovered={setHovered} style={style} products={products}/>
</>
);
};
export default Products;
// Child component
const Product = ({ setHovered, style, products }) => {
return (
<div className={styles.items}>
{products.map((value) => {
return (
<>
<div style={style}>
<button
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
Add to Cart
</button>
</div>
</>
);
})}
</div>
);
};
export default Product;
The issue is you are setting setHovered state in component, the simple solution could be to use it in useEffect and add required dependency.
If we talk about your code so you can easily do this by using the state in child component instead of passing through props.
I have updated your code below:
https://codesandbox.io/s/nameless-cookies-e6pwxn?file=/src/components/Product.js:141-151

How to test if a react component was clicked

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

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