Re-rendering React component when I call useState hook - reactjs

I have a problem with the re-rendering React component when I call useState() hook inside image.onload(). I'm expecting the component to be re-render once after I call setClassificationResult, but of some reason, it's re-rendering all the time, like I have some infinite loop. Here is my code:
const ImageClassification = React.memo(function() {
const [isModelLoaded, setModelLoaded] = useState(false);
const [uploadedFile, setUploadedFile] = useState();
const [classifier, setClassifier] = useState();
const [classificationResult, setClassificationResult] = useState();
useEffect(() => {
async function modelReady() {
setClassifier(
await MobileNet.load().then(model => {
setModelLoaded(true);
return model;
})
);
}
modelReady();
}, []);
function onDrop(acceptedFiles: File[]) {
setUploadedFile(acceptedFiles);
}
function prepareImage(inputFile: File) {
const image = new Image();
let fr = new FileReader();
fr.onload = function() {
if (fr !== null && typeof fr.result == "string") {
image.src = fr.result;
}
};
fr.readAsDataURL(inputFile);
image.onload = async function() {
const tensor: Tensor = tf.browser.fromPixels(image);
classifier.classify(tensor).then((result: any) => {
// Crazy re-rendering happens when I call this hook.
setClassificationResult(result);
console.log(result);
});
console.log("Tensor" + tensor);
};
}
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
return (
<React.Fragment>
{!isModelLoaded ? (
<CircularProgress />
) : (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop the files here.. </p>
) : (
<p>Drag 'n' drop some files here, or click to select files</p>
)}
{uploadedFile &&
uploadedFile.map((item: File) => {
prepareImage(item);
return classificationResult
? classificationResult.map((result: any) => {
return (
<ClassificationResult
className={result.className}
probability={result.probability}
/>
);
})
: null;
})}
</div>
)}
</React.Fragment>
);
});
export default ImageClassification;
Any idea of how to avoid that crazy re-rendering?

You have a lifecycle issue with your component, as it call prepareImage(item) from your return html value. This means you will call this function at each rendering which is why it create some infinite-loop crazy re-redering.
You need to rethink your algorithm and move it to a better location. A good solution would to only prepareImage onDrop event so it is done only once.
function onDrop(acceptedFiles: File[]) {
setUploadedFile(acceptedFiles);
acceptedFiles.forEach(file => {
prepareImage(file);
});
}
Then maybe store in state an array of Image which should be dislayed and be prepared 😉.

Related

Why I'm getting Rendered more hooks error?

I was wonder what I'm doing wrong here.
I'm getting this error: "Rendered more hooks than during the previous render."
export default function ProductDetails() {
//Use State
const {qty, increaseQty, decreaseQty, onAdd, setQty} = useStateContext();
//Reset Qty
useEffect(() => {
setQty(1);
}, []);
//Fetch Slug
const {query} = useRouter();
//Fetch Graphql data
const [results] = useQuery({
query: GET_PRODUCT_QUERY,
variables: {slug: query.slug}
})
const {data, fetching, error} = results;
//Check for data coming in
if(fetching) return <p>Loading...</p>;
if(error) return <p>Oh no....</p>;
//Extract Product Data
const {title,description, image, gallery } = data.products.data[0].attributes;
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
console.log(img);
//Create a toast
const notify = () => {
toast.success(`${title} added to your cart`, {duration: 1500});
}
return(
<DetailsStyle>
<Gallery>
<img src={gallery.data[0].attributes.formats.medium.url} alt={title} />
<Thumbnails>
{gallery.data.map((image, index) => (
<SingleThumb key={index} >
<img src={image.attributes.formats.thumbnail.url} alt={title} />
</SingleThumb>
)
)}
</Thumbnails>
</Gallery>
<ProductInfo>
<h3>{title}</h3>
<p>{description}</p>
<Quantity>
<span>Quantity</span>
<button><AiFillMinusCircle onClick={decreaseQty} /></button>
<p>{qty}</p>
<button><AiFillPlusCircle onClick={increaseQty}/></button>
</Quantity>
<Buy onClick={() => {
onAdd(data.products.data[0].attributes, qty)
notify();
}}>Add To Cart</Buy>
</ProductInfo>
</DetailsStyle>
)
}
Something wrong is in this line: const [img, setImg] = useState();
Why I can't use more hooks here.
Does anyone know why I'm getting this?
You are using early return
and this line of code won't execute every time:
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
This is only conditionally called:
const [img, setImg] = useState(gallery.data[0].attributes.formats.medium.url);
Because the component has earlier conditional return statements. Move it to earlier in the function. (Generally I invoke useState operations right away.)
Hooks need to always be consistently called in the same order on every render.
You declare your state after some return statements. It means that if you had any errors or you were in loading state, the state is not defined. But maybe in the next render, the data is set and then your state will be defined with the inital value (gallery.data[0].attributes.formats.medium.url).
It's forbidden in react because all of the hooks should always be in the same order on every single render. In order to fix this, you should change the place of your useState for img.
Hope it helps:
export default function ProductDetails() {
const [img, setImg] = useState('');
//Use State
const {qty, increaseQty, decreaseQty, onAdd, setQty} = useStateContext();
//Reset Qty
useEffect(() => {
setQty(1);
}, []);
//Fetch Slug
const {query} = useRouter();
//Fetch Graphql data
const [results] = useQuery({
query: GET_PRODUCT_QUERY,
variables: {slug: query.slug}
})
const {data, fetching, error} = results;
//Check for data coming in
//Extract Product Data
useEffect(() => {
if(results && results.data) {
const {data} = results
const { gallery } = data.products.data[0].attributes;
setImg(gallery.data[0].attributes.formats.medium.url);
}
}, [results]);
useEffect(() => {
console.log(img);
}, [img]);
//Create a toast
const notify = (title) => {
toast.success(`${title} added to your cart`, {duration: 1500});
}
if(fetching) {
return <p>Loading...</p>;
} else if(error) {
return <p>Oh no....</p>;
} else if(data) {
const { title, description, image, gallery } = data.products.data[0].attributes;
return(
<DetailsStyle>
<Gallery>
<img src={gallery.data[0].attributes.formats.medium.url} alt={title} />
<Thumbnails>
{gallery.data.map((image, index) => (
<SingleThumb key={index} >
<img src={image.attributes.formats.thumbnail.url} alt={title} />
</SingleThumb>
)
)}
</Thumbnails>
</Gallery>
<ProductInfo>
<h3>{title}</h3>
<p>{description}</p>
<Quantity>
<span>Quantity</span>
<button><AiFillMinusCircle onClick={decreaseQty} /></button>
<p>{qty}</p>
<button><AiFillPlusCircle onClick={increaseQty}/></button>
</Quantity>
<Buy onClick={() => {
onAdd(data.products.data[0].attributes, qty)
notify(title);
}}>Add To Cart</Buy>
</ProductInfo>
</DetailsStyle>
)
} else {
return null;
}
}
There should be no return before hooks.
These lines
if(fetching) return <p>Loading...</p>;
if(error) return <p>Oh no....</p>;
should be after all hooks

How to refactor this call to refresh state

I want to know how improve this calls in order to not repeat always the same sentence to refresh the state...
I don't need any huge refactor, only inputs like: you need to put this call inside a function and call it when you want... something like this...
export const CategoriesPage = () => {
const [categories, setCategories] = useState<Category[]>([]);
const [showModal, setShowModal] = useState(false);
const handleCreateCategory = (newCategory: CategoryCreate, file: File) => {
createCategoryHelper(newCategory, file)
.then(() => {
getCategoriesHelper().then(setCategories);
})
.finally(() => handleClose());
};
const handleDeleteCategory = (categoryId: Id) => {
SwalHelper.delete().then(() => {
deleteCategoryHelper(categoryId).then(() =>
getCategoriesHelper().then(setCategories)
);
});
};
const handleClose = () => {
setShowModal(false);
};
const handleModal = () => {
setShowModal(true);
};
useEffect(() => {
getCategoriesHelper().then(setCategories);
}, []);
return (
<>
<PageTitle title="Categories" />
<FilterBar>
<Button type="button" background="green" onClick={handleModal}>
+ Add new
</Button>
</FilterBar>
{showModal && (
<ModalPortal onClose={handleClose}>
<CreateCategoryForm
createCategory={(category, file: File) => {
handleCreateCategory(category, file);
}}
/>
</ModalPortal>
)}
<ListGrid columns={3}>
{categories.map((category) => {
const { id: categoryId } = category;
return (
<CategoryCard
key={categoryId}
{...category}
onClick={() => handleDeleteCategory(categoryId)}
/>
);
})}
</ListGrid>
</>
);
};
When component is mounting, on useEffect, fills the state with response in order to create a list.
When a category is created, I call to setState again to refresh the list.
Same on delete, on then, refresh again to update the list.
Three times calling the same sentence
getCategoriesHelper().then(setCategories)
This is getCategoriesHelper:
export const getCategoriesHelper = async () => {
const service = new CategoryServiceImplementation(apiConfig);
const uploadImageService = new AmplifyS3Service();
const repository = new CategoryRepositoryImplementation(
service,
uploadImageService
);
const useCase = new GetCategoriesUseCaseImplementation(repository);
return await useCase.getCategories();
};
Is there any way to make this code much cleaner and reusable?
Thanks in advance!
Everything is write, and all calls are made as they are designed to do

React listen to child's state from parent

Damn, two days, two noob questions, sorry guys.
Yesterday, I spent the whole afternoon reading the docs but my fart-ey brain cannot process how to use react hooks to pass data from a child to a parent.
I want to create a button on my parent that can listen to his child's state to check on it and change the background color depending on its value.
Thing is, the child component is mapping some stuff so I cannot create a button (otherwhise it would be rendered multiple times and not only once like I want).
I've thought about moving all the data to my parent component but I cannot understand how since I'm fairly new to React and it's been only two months of learning how to code for me basically.
I will now provide the code for the parent and the child component.
The parent :
import React from "react";
import Quizz from "./components/Quizz";
export default function App() {
const [quizz, setQuizz] = React.useState([]);
React.useEffect(() => {
async function getData() {
const res = await fetch(
"https://opentdb.com/api.php?amount=5&category=27&type=multiple"
);
const data = await res.json();
setQuizz(data.results)
}
getData();
}, []);
function checkOnChild(){ /* <== the function I'd like to use to check on my Quizz component's "activeAnswer" state */
console.log(quizz);
}
const cards = quizz.map((item, key) => {
return <Quizz {...item} key={key}/>;
});
return (
<div>
{cards}
<button onClick={checkOnChild}>Check answers</button> /* <== the button that will use the function */
</div>
);
}
and the child :
import React from "react";
import { useRef } from "react";
export default function Quizz(props) {
const [activeAnswer, setActiveAnswer] = React.useState('');/* <== the state I'd like to check on from my parent component */
function toggle(answer) {
setActiveAnswer(answer);
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
let answers = props.incorrect_answers;
const ref = useRef(false);
if (!ref.current) {
answers.push(props.correct_answer);
shuffleArray(answers);
ref.current = true;
}
const answerDiv = answers.map((answer, key) => (
<div key={key} className="individuals" onClick={()=> toggle(answer)}
style={{background: answer == activeAnswer ? "#D6DBF5" : "transparent" }}>
{answer}
</div>
));
console.log(answers);
console.log(activeAnswer);
console.log(props.correct_answer);
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{answerDiv}</div>
<hr />
</div>
);
}
I'm really sorry If it seems dumb or if I'm making forbidden things lmao, but thanks in advance for your help guys!
This should get you a bit further I think.
export default function App() {
const [quizData, setQuizData] = useState([])
const [quizState, setQuizState] = useState({})
useEffect(() => {
async function getData() {
const res = await fetch('https://opentdb.com/api.php?amount=5&category=27&type=multiple')
const data = await res.json()
const results = data.results
setQuizData(results)
setQuizState(results.reduce((acc, curr) => ({ ...acc, [curr.question]: '' }), {}))
}
getData()
}, [])
function checkOnChild() {
console.log(quizState)
}
const cards = quizData.map((item) => {
return <Quizz {...item} key={item.question} quizState={quizState} setQuizState={setQuizState} />
})
return (
<div>
{cards}
<button onClick={checkOnChild}>Check answers</button>
</div>
)
}
export default function Quizz(props) {
function handleOnClick(answer) {
props.setQuizState(prevState => ({
...prevState,
[props.question]: answer,
}))
}
const answers = useMemo(() => {
const arr = [...props.incorrect_answers, props.correct_answer]
return shuffleArray(arr)
}, [props.incorrect_answers, props.correct_answer])
const answerDiv = answers.map((answer) => (
<div
className="individuals"
key={answer}
onClick={() => handleOnClick(answer)}
style={{ background: answer == props.quizState[props.question] ? '#D6DBF5' : 'transparent' }}
>
{answer}
</div>
))
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{answerDiv}</div>
<hr />
</div>
)
}

React Context. how to avoid "Cannot read properties of undefined" error before having a value

I am learning React, and trying to build a photo Album with a a modal slider displaying the image clicked (on a different component) in the first place.
To get that, I set <img src={albums[slideIndex].url} /> dynamically and set slideIndex with the idof the imgclicked , so the first image displayed in the modal slider is the one I clicked.
The problem is that before I click in any image albums[slideIndex].urlis obviously undefined and I get a TypeError :cannot read properties of undefined
How could I solve that?
I tried with data checks with ternary operator, like albums ? albums[slideIndex].url : "no data", but doesn't solve it.
Any Ideas? what i am missing?
this is the component where I have the issue:
import React, { useContext, useEffect, useState } from "react";
import { AlbumContext } from "../../context/AlbumContext";
import AlbumImage from "../albumImage/AlbumImage";
import "./album.css";
import BtnSlider from "../carousel/BtnSlider";
function Album() {
const { albums, getData, modal, setModal, clickedImg } =
useContext(AlbumContext);
console.log("clickedImg id >>", clickedImg.id);
useEffect(() => {
getData(); //-> triggers fetch function on render
}, []);
///////////
//* Slider Controls
///////////
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
console.log("SlideINDEx", slideIndex ? slideIndex : "no hay");
const nextSlide = () => {
if (slideIndex !== albums.length) {
setSlideIndex(slideIndex + 1);
} else if (slideIndex === albums.length) {
setSlideIndex(1);
}
console.log("nextSlide");
};
const prevSlide = () => {
console.log("PrevSlide");
};
const handleOnclick = () => {
setModal(false);
console.log(modal);
};
return (
<div className="Album_Wrapper">
<div className={modal ? "modal open" : "modal"}>
<div>
<img src={albums[slideIndex].url} alt="" />
<button className="carousel-close-btn" onClick={handleOnclick}>
close modal
</button>
<BtnSlider moveSlide={nextSlide} direction={"next"} />
<BtnSlider moveSlide={prevSlide} direction={"prev"} />
</div>
</div>
<div className="Album_GridContainer">
{albums &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
</div>
);
}
export default Album;
THis is my AlbumContext :
import React, { createContext, useState } from "react";
export const AlbumContext = createContext();
export const AlbumContextProvider = ({ children }) => {
const [albums, setAlbums] = useState();
const [modal, setModal] = useState(false);
const [clickedImg, setClickedImg] = useState("");
const showImg = (img) => {
setClickedImg(img);
setModal(true);
console.log(clickedImg);
};
const getData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/albums/1/photos"
);
const obj = await response.json();
console.log(obj);
setAlbums(obj);
} catch (error) {
// console.log(error.response.data.error);
console.log(error);
}
};
console.log(`Albums >>>`, albums);
return (
<AlbumContext.Provider
value={{ albums, getData, showImg, modal, setModal, clickedImg }}
>
{children}
</AlbumContext.Provider>
);
};
Thanks very much in advance
Your clickedImg starts out as the empty string:
const [clickedImg, setClickedImg] = useState("");
And in the consumer, you do:
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
So, it takes the value of clickedImg.id on the first render - which is undefined, because strings don't have such properties. As a result, both before and after fetching, slideIndex is undefined, so after fetching:
albums ? albums[slideIndex].url : "no data"
will evaluate to
albums[undefined].url
But albums[undefined] doesn't exist, of course.
You need to figure out what slide index you want to be in state when the fetching finishes - perhaps start it at 0?
const [slideIndex, setSlideIndex] = useState(0);
maybe because your code for checking albums is empty or not is wrong and its always return true condition so change your code to this:
<div className="Album_GridContainer">
{albums.length > 0 &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
change albums to albums.length

Using React hook form getValues() within useEffect return function, returns {}

I'm using react-hook-form library with a multi-step-form
I tried getValues() in useEffect to update a state while changing tab ( without submit ) and it returned {}
useEffect(() => {
return () => {
const values = getValues();
setCount(values.count);
};
}, []);
It worked in next js dev, but returns {} in production
codesandbox Link : https://codesandbox.io/s/quirky-colden-tc5ft?file=/src/App.js
Details:
The form requirement is to switch between tabs and change different parameters
and finally display results in a results tab. user can toggle between any tab and check back result tab anytime.
Implementation Example :
I used context provider and custom hook to wrap setting data state.
const SomeContext = createContext();
const useSome = () => {
return useContext(SomeContext);
};
const SomeProvider = ({ children }) => {
const [count, setCount] = useState(0);
const values = {
setCount,
count
};
return <SomeContext.Provider value={values}>{children}</SomeContext.Provider>;
};
Wrote form component like this ( each tab is a form ) and wrote the logic to update state upon componentWillUnmount.
as i found it working in next dev, i deployed it
const FormComponent = () => {
const { count, setCount } = useSome();
const { register, getValues } = useForm({
defaultValues: { count }
});
useEffect(() => {
return () => {
const values = getValues(); // returns {} in production
setCount(values.count);
};
}, []);
return (
<form>
<input type="number" name={count} ref={register} />
</form>
);
};
const DisplayComponent = () => {
const { count } = useSome();
return <div>{count}</div>;
};
Finally a tab switching component & tab switch logic within ( simplified below )
const App = () => {
const [edit, setEdit] = useState(true);
return (
<SomeProvider>
<div
onClick={() => {
setEdit(!edit);
}}
>
Click to {edit ? "Display" : "Edit"}
</div>
{edit ? <FormComponent /> : <DisplayComponent />}
</SomeProvider>
);
}

Resources