React useCallback and useEffect at the same time messed the state - reactjs

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.

Related

How to show content when click on button

I have a button that I would like when the user clicks on it to show what is inside my MaterialTextField. Inside my materialtextfield's there is: value={projectFilter.eventName}, {projectFilter.eventYear}, and others that follow the same logic. How can I make it so that I have this result?
UPDATE(here some example of what I want to show when I click the button):
return (
<Grid item xs={4}>
<MaterialTextField
autoComplete="off"
variant="outlined"
label="Name"
type="text"
name="eventName"
value={projectFilter.eventName}
sx={{ width: '100%' }}
/>
</Grid>
)
Please provide some code sample.
Basically you have to set state which will change when user clicks the button.
https://reactjs.org/docs/state-and-lifecycle.html
You need state to store the current value of the field. You can use the useState hook for this. You should really read through the React documentation. The useState hook docs in particular is what you are after but it will make much more sense if you read state and lifecycle and just understand how react works.
Here is an example assuming that this projectFilter object is passed into the component as a prop.
import React from 'react';
export const SomeComponent = ({ projectFilter }) => {
const [fieldValue, setFieldValue] = React.useState('');
const clickHandler = (e) => {
// Set the value of the state here. This will cause a re-render.
setFieldValue(projectFilter.eventName);
};
return (
<Grid item xs={4}>
<MaterialTextField value={fieldValue} />
<button onClick={clickHandler}>Click this</button>
</Grid>
);

Uncaught TypeError: user.toLowerCase is not a function

okay guys so new update. so i did jus that . i moved all the elements to the rendering and tried to just get the state to be alone. when i tried that it told me that map wasnt able to read properties of undefined. so what i did was added the object keys . the page rendered ... kinda. it rendered without all the data. i attached the updated code in here but now im kinda confused because its not showing an error this time .its just not displaying the info. can someone tell me what im doing wrong here ?
The error message tells you that user.toLowerCase is not a function, indicating that user is not a string as toLowerCase only works with strings. Instead, user is the ul element you create in the setInfo(json.students.map()) call in the useEffect. It's not ideal to store DOM elements in state for this reason as it's not easy to work with. Instead, leave your info state variable as an array of objects, and move the logic for creating DOM elements in your return.
In addition, I would not change your state when you filter as you would not be able to easily get the original, unfiltered state values back. Keep track of your filters in state and do conditional rendering based on the filters in your return.
You setting your user info as a ul html list. Instead set the userInfo the json response and then map the ul list when you render the component.
I'm not sure what your user json looks like, so you might have to play around with that.
import React, { useEffect, useState } from "react";
import Card from "#material-ui/core/Card";
import CardContent from '#material-ui/core/CardContent';
import Grid from "#material-ui/core/Grid";
import { Input } from "#material-ui/core";
function StudentProfiles() {
const [info, setInfo] = useState();
const [search, setSearch] = useState('');
useEffect(() => {
fetch("https://api.hatchways.io/assessment/students")
.then(response => response.json())
.then(json => setInfo(json))
},[]);
const average = (array) => array.reduce((a,b) => a + b )/ array.length;
const filter = (e) => {
const keyword = e.target.value;
if(keyword !== '') {
const results = info.filter((user) => {
return user.students.toLowerCase().startsWith(keyword.toLowerCase());
});
setInfo(results);
} else {
setInfo(info)
}
}
return (
<div>
<Card className="card">
<CardContent className="scrollbar scrollbar-primary mt-5 mx-auto">
<Input
className="searchBar"
icon="search"
placeholder="Search by name"
onChange={filter}
/>
{ user.students.map((name) => (
<ul className = "border" key={name.id}>
<Grid item xs={3} sm={6} md={12} style={{display: "flex", gap:"3.5rem", paddingBottom:"8px"}}>
<img alt ="" src={name.pic} className="picture"></img>
<Grid container style={{display: "inline"}} align="left" justify="flex-end" alignItems="flex-start">
<Grid className="studentNames">
<span>{name.firstName + " " + name.lastName}</span>
</Grid>
<span>{name.email}</span>
<br/>
<span>{name.company}</span>
<br/>
<span>{name.skill}</span>
<br/>
<span>Average: {average(name.grades).toFixed(3)}%</span>
</Grid>
</Grid>
</ul>
)))}
</CardContent>
</Card>
</div>
)
}
export default StudentProfiles;

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.

Why is my action function being called on render?

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.

Resources