Why is my action function being called on render? - reactjs

I'm trying to make a simple card view in material UI and when I implement adding and deleting items, the functions seem to be called several times on render.
I know usually the issue is having action={myFunction} but in this case I have definitely used action={() => myFunction()} so I'm not sure what's causing the function to be called on render.
const App: React.FC = () => {
function deleteItem(key: string) : void {
console.log("deleting " + key);
setGridItemMap(gridItemMap.filter( (value, i) => value.key !== key ));
}
function addItem() : void {
setGridItemMap(gridItemMap.concat({key : "key1", props: props1}));
}
const props1 : GridItemProps = {title:"TitleProp1", body:"BodyProp1"};
const [gridItemMap, setGridItemMap] = useState([
{key: "key1", props: props1},
]);
return (
<Container maxWidth="sm">
<Grid
container
direction="column"
justify="space-evenly"
alignItems="stretch"
spacing={1}
>
{gridItemMap.map( (entry) => (
<Grid
container
direction="row"
spacing={1}
>
<GridItem key={entry.key} {...entry.props}></GridItem>
<Fab action={() => deleteItem(entry.key)}>
<DeleteIcon />
</Fab>
</Grid>
))}
<Fab action = {() => addItem()}>
<AddIcon />
</Fab>
</Grid>
</Container>
);
}
When this is run I get the standard "Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops." error and the function is being called 100+ times suggesting I've managed to get an update loop somewhere but I can't find where

It looks like the issue is tied to use use of the action prop. From the docs on ButtonBase (which the FAB passes props to), the action prop is a "[c]allback fired when the component mounts". It seems you should pass the addItem function as the onClick prop rather than the action prop.
Also, as an aside, passing () => addItem() is equivalent to passing addItem as a callback on its own. The issue I believe you're referring to is when onClick={ addItem() } is used.

Related

React Material UI Grid Item doesn`t render after data update

I'm unable to make following code render grid items when props.data changes from the top component.
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Grid from '#material-ui/core/Grid';
import ProductCard from './ProductCard';
const useStyles = makeStyles((theme) => ({
grid: {
padding: "8px",
},
}));
export default function CenteredGrid(props) {
const classes = useStyles();
const visibleProductData = props.data === null ? {} : props.data;
return (
<Grid container >
{console.log("This is from the product card grid")}
{console.log(visibleProductData)}
{Object.entries(visibleProductData).map(productData => (
<Grid key={productData[0]} className={classes.grid} item md={3} sm={6} xs={12}>
<ProductCard data={productData[1]}/>
</Grid>
))}
</Grid>
);
}
When I run this, after the data updates, the console logs visibleProductData which is a dictionary consisting of three products, as expected. However these products are not visible, in fact when I inspect I see no children for Grid container. What is weird is that, even after small changes in code, when a fast refresh occurs products become visible. What might be the issue here ?
PS: I'm using nextjs along with material ui.
Edit / Update - Parent Component
const classes = useStyles();
const { buyer, categoryData, filterData, visibleProductData } = useContext(BuyerContext);
if (!buyer) {
return (
<AuthRequired/>
)} else {
return (
<>
<HeaderBar/>
<Grid className={classes.breadcrumb} container>
<Breadcrumb />
</Grid>
<Divider variant="middle" />
<main className={classes.main}>
<Grid container>
<Grid item xs={2}>
<Box display={{ xs: 'none', sm: 'block' }}>
<CategoryList data={categoryData}/>
</Box>
</Grid>
<Grid item sm={10} xs={12}>
<FilterGrid data={filterData}/>
<ProductCardGrid data={visibleProductData}/>
</Grid>
</Grid>
</main>
<Footer/>
</>
)
}
}
Try the following line,
const visibleProductData = props.data === null ? {} : {...props.data};
It might be because your visibleProductData variable is always getting the same reference object. You need to create a new reference object each time props.data changes. If the issue still persists, then we need to see your parent component. The issue might be there.
Writing const visibleProductData = props.data === null ? {} : props.data; in React functional component body is not the correct "React way". You should:
define a local state variable called, for example, visibleProductData:
const [visibleProductData, setVisibleProductData] = useState({});
use useEffect hook to "listen" new values comes from parent object. Something like:
useEffect(() => {
setVisibleProductData(props.data === null ? {} : {...props.data});
}, [props.data]);
In this way, every time props.data changes, useEffect will be fired and it will update local visibleProductData.
I could at last solve the problem, it was a small typo that gives no error and therefore hard to debug. Instead of putting another "(" within map like so, {array.map(element => (...))} I should have done without it like this {array.map(element => ...)}.

how to update state to wait for a function to complete in React

I am developing a React functional component for a model CRUD operations, this component will render save and delete buttons on the model form, and I am trying to show a waiting indicator when the user clicks the save or delete button and hide the indicator when the process completes.
I am using the material-ui React components library, and for the waiting indicator I am using the Backdrop component.
the component props are the save and delete callbacks and set by the parent component.
I added a boolean state to show/hide this backdrop, but the waiting indicator is not showing as the setState in react is asynchronous. so how can I achieve this?
here is my component:
export default function ModelControls({onSave, onDelete}) {
const [wait, setWait] = useState(false);
const saveClick = () => {
setWait(true);
const retId = onSave();
setWait(false);
...
};
return (
<Container maxWidth={"xl"}>
<Grid container spacing={2}>
<Grid item xs={6}>
<Box display="flex" justifyContent="flex-end">
<Box component="span">
<Button size="small" color="primary" onClick={saveClick}>
<SaveIcon />
</Button>
</Box>
</Box>
</Grid>
</Grid>
<Backdrop open={wait}>
<CircularProgress color="primary" />
</Backdrop>
</Container>
);
}
Just make the function async and add await in front of the save function.
const saveClick = async () => {
setWait(true);
const retId = await onSave();
setWait(false);
};
Thanks #Dipansh, you inspired me to the following solution.
now the onSave callback from parent must return a promise object
const saveClick = () => {
setWait(true);
onSave().then((retId) => {
...
setWait(false);
});
};
this way it is working as needed.

React useCallback and useEffect at the same time messed the state

I created a SignaturePad for my application that will pass the value later on to Formik.
Problem 1:
I have some issue with using the useEffect and useCallback function of react. Previously before I add useEffect, handleClear function works just fine. However after I added useEffect, sigPad refs always returns null in handleClear.
I'm not sure if my mental model is correct, can anyone please explain why is this happening?
Problem 2:
I realised however, after I removed the [sigPad] at handleClear I am able to receive the ref again. Which part of my code re-renders and how does useCallback not realise that sigPad is changing from null to the correct ref?
Problematic code:
export function Signature() {
let sigPad = useRef(null);
const [sig, setSig] = useState("");
const classes = formStyles();
useEffect(() => {
console.log(sigPad);
setSig(sigPad.toData());
console.log(sigPad);
}, [sig]);
const handleClear = useCallback(() => {
console.log(sigPad);
if (sigPad) sigPad.clear();
}, [sigPad]);
return (
<div className="row">
<Grid spacing={3} container>
<Grid item xs={12}>
<h3 className="text-bold">Signature</h3>
</Grid>
<Grid item xs={12}>
<div className="sigCanvas">
<SignatureCanvas
penColor="black"
canvasProps={{ className: "sigPad" }}
ref={ref => {
sigPad = ref;
}}
/>
</div>
</Grid>
<Grid item xs={3}>
<Button
variant="contained"
component="label"
className={classes.instructions}
onClick={handleClear}
>
Clear
</Button>
</Grid>
<Grid item xs={3}>
<Button
component="label"
variant="contained"
className={classes.instructions}
>
Save
</Button>
</Grid>
</Grid>
</div>
);
}
Current Fix:
const handleClear = useCallback(() => {
console.log(sigPad);
if (sigPad) sigPad.clear();
});
Note: Before I add useEffect I don't have to remove the [sigPad] at my callback
Hooks are executed in the order they are called, this means that your useEffect takes precedence over your useCallback function. Also, this [] is called the dependency array and is used to create new instances of the underlying structure. When using useRef you don't need to do this
ref={ref => { sigPad = ref; }}
You can just do
ref={sigPad}
And try moving your useCallback above useEffect.

React not rendering all elements within a list of components

I have a system in which when the user presses a button, a new component is pushed to a list of components and the state is updated. I render the list of components using {}, but only the first element is rendered.
I've used console.log to ensure that my list is actually updating. All the questions I've seen so far for this problem involve using a class that extends React.Component. Since I'm using a function to render, I don't see how I can use those solutions
export default function ExampleManager() {
const [examples, setExamples] = React.useState([<Example key={0}/>);
function handleClick() {
examples.push(<Example key={examples.length}/>);
setExamples(examples);
}
return (
<>
{examples}
<Button variant="outlined" color="primary" onClick={handleClick}>
Add Example
</Button>
</>
);
}
If the button was to be clicked multiple times, I would expect there to be multiple Example components, however at the moment only the first element works
As far as I know, examples is immutable and isn't updated by using examples.push().
Change your handleClick to the following code, to remove the reference of your example variable:
function handleClick() {
// create a new array to prevent referencing the old on
setExamples([
...examples,
<Example key={examples.length}/>
]);
}
Nonetheless you shouldn't add components per se into your array. Try to split values and its representation like the following:
function ExampleManager() {
const [examples, setExamples] = React.useState([0]);
const handleClick = () => setExamples([...examples, examples.length])
return (
<>
{examples.map((item, key) => <Example key={key} data={item} />)}
<Button variant="outlined" color="primary" onClick={handleClick}>
Add Example
</Button>
</>
)
}

semantic-ui-react List onClick declaration

i'm trying to create a list of documents dynamically with semantic-ui-react. I'd like to get the document title back when the list item is clicked. According to the documentation:
https://react.semantic-ui.com/elements/list
there is an onItemClick prop for this reason, however when im using it i get a warning when it's rendered:
Warning: Failed prop type: Prop onItemClick in List conflicts with props: children. They cannot be defined together, choose one or the other.
Also clicking on the list item does nothing (atm i just want to log the doc title to the console). Here is the code:
handleListItemClick(event, data) {
console.log("list item clicked: " + data.value);
}
buildResultsContainer() {
return this.props.listOfResults.map((document,index) =>
{
return (
<List.Item
as='a'
key={index}>
<Icon name='file' />
<List.Content>
<List.Header>{document.properties.title}</List.Header>
<List.Description>
{document.properties.description}
</List.Description>
</List.Content>
</List.Item>
);
}
);
}
render() {
return (
<div>
<List onItemClick={this.handleListItemClick}>
{this.buildResultsContainer()}
</List>
</div>
)
}
Can you please tell me how to use properly the onItemClick prop for the List component?
Less important, do you have any tip how to refactor the list rendering? Just wanted to keep the render function short and clean, but this function call looks a bit overkill....
Thanks a lot!
I think maybe the intent when using onItemClick is that you would use the items prop on List since then you wouldn't have any children e.g.
render() {
const items = this.props.listOfResults.map(document => {
return {
icon: 'file',
content: document.properties.title,
description: document.properties.description,
onClick: e => console.log(document.title)
}
});
return <List items={items} />
}
If you had your listOfResults prop in the above format, you wouldn't even need to do this map and your render function would be super tight:
render() {
return <List items={this.props.listOfResults} />;
}
Alternately, List.Item takes an onClick prop that you could define in your buildResultsContainer() function. Because each onClick function is unique based on the current document object, you will need to use an anonymous function to call your handleClick function as follows:
<List.Item
onClick={() => this.handleClick(document.title)}
...etc
/>
You would then have:
handleClick = docTitle => {
console.log(docTitle);
};
If what you wanted was obtainable from event.target, you could just pass the reference of handleClick to the onClick i.e.
handleClick = event => {
console.log(e.target.innerText);
};
<List.Item
onClick={this.handleClick}
/>

Resources