useMemo hook not working with map elements - reactjs

I am using useMemo hook to render map items.I added items parameter to useMemo hook, based on items change it will render. But changing loading state and items change, Item custom component rendering twice. Am i doing any mistake on using useMemo hook, please correct me.
Home:
import React, { useState, useEffect, useMemo } from "react";
import Item from "./Item";
const array = [1];
const newArray = [4];
const Home = () => {
const [items, setItems] = useState(array);
const [loading, setLoading] = useState(false);
const [dataChange, setDataChange] = useState(1);
const renderItems = (item, index) => {
return (
<div key={item}>
<Item id={item}></Item>
</div>
);
};
useEffect(() => {
if (dataChange === 2) {
setLoading(true);
setTimeout(() => {
setLoading(false);
setItems(newArray);
}, 3000);
}
}, [dataChange]);
const memoData = useMemo(() => {
return <div>{items.map(renderItems)}</div>;
}, [items]);
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<input
onClick={() => {
setDataChange(2);
}}
style={{ height: 40, width: 100, margin: 20 }}
type="button"
value="ChangeItem"
></input>
<div>{loading ? <label>{"Loading"}</label> : <div>{memoData}</div>}</div>
</div>
);
};
export default React.memo(Home);
Item:
import React,{useEffect} from "react";
const Item = (props) => {
console.log("props", props);
useEffect(() => {
// call api with props.id
}, [props]);
return <div>Hello world {props.id}</div>;
};
export default React.memo(Item);
Result:
first time :
props {id: 1}
After click :
props {id: 1}
props {id: 4}

There are a few things which are not right in the code above.
key should be passed to the parent element in an array iteration - in your case the renderItems should pass the key to the div element
you are turning off the loading state before updating the items array, switching the two setState expressions will resolve your case most of the time although setState is an async function and this is not guaranteed
if a constant or a function is not tightly coupled to the component's state it is always best to extract it outside the component as is the case with renderItems
Here's why there is one more console.log executed
also should keep in mind that memoization takes time and you would want to keep it as efficient as possible hence you can totally skip the useMemo with a React.memo component which takes care of the array because it is kept in the state and it's reference won't change on rerender if the state remains the same
const array = [1];
const newArray = [4];
const Home = () => {
const [items, setItems] = useState(array);
const [loading, setLoading] = useState(false);
const [dataChange, setDataChange] = useState(1);
useEffect(() => {
if (dataChange === 2) {
setLoading(true);
setTimeout(() => {
setItems(newArray);
setLoading(false);
}, 3000);
}
}, [dataChange]);
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<input
onClick={() => {
setDataChange(2);
}}
style={{ height: 40, width: 100, margin: 20 }}
type="button"
value="ChangeItem"
></input>
<div>
{loading ? <label>{"Loading"}</label> : <ItemsMemo items={items} />}
</div>
</div>
);
};
const renderItems = (item) => {
return (
<span key={item} id={item}>
{item}
</span>
);
};
const Items = ({ items }) => {
console.log({ props: items[0] });
return (
<div>
Hello world <span>{items.map(renderItems)}</span>
</div>
);
};
const ItemsMemo = React.memo(Items);
UPDATE
This codesandbox shows that useMemo gets called only when the items value changes as it is supposed to do.

useCustomHook:
import { useEffect, useRef } from "react"
export default function useUpdateEffect(callback, dependencies) {
const firstRenderRef = useRef(true)
useEffect(() => {
if (firstRenderRef.current) {
firstRenderRef.current = false
return
}
return callback()
}, dependencies)
}
Create these custom hooks in your project and use them. It will prevent your first calling issue.

Related

How to add multiple events in one tag?

I'm making a counting timer which is described below with this react functional component
import {useEffect, useState, useRef} from 'react'
function Content() {
const [countdown, setCountdown] = useState(10)
const [show, setShow] = useState(true)
const ref = useRef()
function handleStart() {
ref.current = setInterval(() => {
setCountdown(prev => prev - 1)
}, 1000)
}
function handleStop() {
clearInterval(ref.current)
}
return (
<div>
<h2 style={{padding: 20}}>Time remaining: {countdown}</h2>
<button onClick={handleStart}>Start</button>
<button onClick={handleStop}>Stop</button>
</div>
)
}
export default Content;
How do I hide these two buttons after clicking one of the two.
Assuming show is the variable to control whether the buttons are visible or not.
<div>
<h2 style={{padding: 20}}>Time remaining: {countdown}</h2>
{show && <>
<button onClick={() => {
setShow(false)
handleStart()
}}>Start</button>
<button onClick={() => {
setShow(false)
handleStop()
}}>Stop</button>
</>}
</div>
React children need to return one element, so you can either wrap it in a div, or an empty element, <> </>, so you can return multiple nodes without adding a div, span, etc.
show && <></> means if show is true, the right-hand side will render, otherwise, it won't be rendered.
First, you have to introduce new state variable, you need one ror the start btn and another for the stop btn.
You have to setShow to false on either click and render the buttons conditionally depending on show variable:
const [countdown, setCountdown] = useState(10)
const [showStart, setShowStart] = useState(true)
const [showStop, setShowStop] = useState(true);
const ref = useRef()
function handleStart() {
setShowStart(false);
ref.current = setInterval(() => {
setCountdown(prev => prev - 1)
}, 1000)
}
function handleStop() {
setShowStop(false);
clearInterval(ref.current)
}
return (
<div>
<h2 style={{padding: 20}}>Time remaining: {countdown}</h2>
{showStart && <button onClick={handleStart}>Start</button>}
{showStop && <button onClick={handleStop}>Stop</button>}
</div>
)
Hope the Below Code Solver Your Problem
import React, { useEffect, useState, useRef } from 'react';
function Example() {
const [countdown, setCountdown] = useState(10);
const [show, setShow] = useState(true);
const ref = useRef();
function handleStart() {
setShow(!show);
ref.current = setInterval(() => {
setCountdown((prev) => prev - 1);
}, 1000);
}
function handleStop() {
setShow(!show);
clearInterval(ref.current);
}
return (
<div>
<h2 style={{ padding: 20 }}>Time remaining: {countdown}</h2>
{show && (
<div>
<button onClick={handleStart}>Start</button>
<button onClick={handleStop}>Stop</button>
</div>
)}
</div>
);
}
export default Example;

Passing Property To Another Component - React

I need to pass "notecards" (an array) down from "Notecard.js" to "LoadQuestions.js". Console log shows that it is passing, but when I use {notecards} within the "return" it errors as "undefined". Could you please take a look?
Notecard.js (without the imports):
const useStyles = makeStyles((theme) => ({
root: {
maxWidth: 345,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
}));
export default function Notecard( {notecards} ) {
const classes = useStyles();
const next = () => {
console.log('Next Button Clicked')
};
const previous = () => {
console.log('Back Button Clicked')
};
const hint = () => {
console.log('Hint Button Clicked')
};
console.log({notecards});
return (
<Card className={classes.root}>
<div id="cardBody">
<CardHeader
title="Kate Trivia"
// subheader="Hint: In the 20th century"
/>
<CardContent>
<LoadQuestions notecards={notecards}/>
</CardContent>
</div>
</Card>
);
}
LoadQuestions.js (without imports)
const {useState} = React;
export default function LoadQuestions( {notecards} ) {
const [currentIndex, setCounter] = useState(0);
console.log({notecards});
return (
<div>
<Toggle
props={notecards}
render={({ on, toggle }) => (
<div onClick={toggle}>
{on ?
<h1>{props.notecards} hi</h1> :
<h1>{this.props[currentIndex].backSide}</h1>
}
</div>
)}
/>
<button onClick={() => {
console.log({notecards})
if (currentIndex < (this.props.length-1)) {
setCounter(currentIndex + 1);
} else {
alert('no more cards')
}
}}>Next Card
</button>
<button onClick={() => {
if (currentIndex > 0 ) {
setCounter(currentIndex -1);
} else {
alert('no previous cards')
}
}}>Previous Card
</button>
</div>
);
}
Thanks in advance!
That's all the details I have for you, but stack overflow really wants me to add more before it will submit. Sorry!
You should check if props exists, first time it renders the component it has no props so it shows undefined.
First i must say you destructured notecards out, so no need to use props.
If you want to use props you should change
({notecards}) to (props)
and if not you can directly use notecards since it is destructured
I suggest you two ways
adding question mark to check if exists
<h1>{props?.notecards} hi</h1>//in the case you want to use props
or
add the props in a if statement
<h1>{props.notecards?props.notecards:''} hi</h1> // if notecards is destructured remove the "props."

Update UI when useRef Div Width Changes

I have a useRef attached to a div. I need to update my UI when the div's width changes. I can access this using ref.current.innerWidth, however, when its width changes, it doesn't update other elements that depend on ref.current.innerWidth.
How can I do this?
CODE:
let ref = useRef();
return (
<>
<Box resizable ref={ref}>
This is a resizable div
</Box>
<Box width={ref.current.innerWidth}>
This box needs the same with as the resizable div
</Box>
</>
);
You could use a ResizeObserver. Implemented like so, it will set the width everytime the size of the ref changes:
let ref = useRef()
const [width, setwidth] = useState(0)
useEffect(() => {
const observer = new ResizeObserver(entries => {
setwidth(entries[0].contentRect.width)
})
observer.observe(ref.current)
return () => ref.current && observer.unobserve(ref.current)
}, [])
return (
<>
<Box ref={ref}>
This is a resizable div
</Box>
<Box width={width}>
This box needs the same with as the resizable div
</Box>
</>
)
You should make a lifecycle using useEffect and useState and event listener on window to listen the data change then re-render your component based on that.
CodeSandBox
const [size, setSize] = useState(null);
let ref = useRef();
const updateDimensions = () => {
console.log(ref.current.clientWidth);
if (ref.current) setSize(ref.current.clientWidth);
};
useEffect(() => {
window.addEventListener("resize", updateDimensions);
setSize(ref.current.clientWidth);
return () => {
console.log("dismount");
window.removeEventListener("resize", updateDimensions);
};
}, []);
return (
<>
<div ref={ref}>This is a resizable div</div>
<div
style={{
width: size,
border: "1px solid"
}}
>
This div needs the same with as the resizable div
</div>
</>
);
For anyone looking for a reusable logic and a Typescript support, I created the below custom hook based on #fredy's awesome answer, and also fixed some issues I've found in his answer:
import { useState, useRef, useEffect } from "react";
export const useObserveElementWidth = <T extends HTMLElement>() => {
const [width, setWidth] = useState(0);
const ref = useRef<T>(null);
useEffect(() => {
const observer = new ResizeObserver((entries) => {
setWidth(entries[0].contentRect.width);
});
if (ref.current) {
observer.observe(ref.current);
}
return () => {
ref.current && observer.unobserve(ref.current);
};
}, []);
return {
width,
ref
};
};
Then, import useObserveElementWidth, and use it like this:
const YourComponent = () => {
const { width, ref } = useObserveElementWidth<HTMLDivElement>();
return (
<>
<Box resizable ref={ref}>
This is a resizable div
</Box>
<Box width={width}>
This box needs the same with as the resizable div
</Box>
</>
);
};
I've created an example codesandbox for it.

Why doesn't the function work after I have changed the state in another component?

Those are the three components I’m using, excluding the component that displays them in the DOM, but that’s not needed.
Here I have a Parent and two Child components.
For some reason when the popup is active, and I click the Refresh Child 1 Component button, it changes the state back to Child1, but I lose the functionality within that component. So the popUpToggle function stops working.
It was working fine before. When I click the Refresh Child 1 Component again however, it starts working. Why is that?
import React, { useState, useEffect } from 'react';
import Child1 from './child1'
import Child2 from './child2'
const Parent = () => {
const [display, setDisplay] = useState('');
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(!popUp)
console.log('PopUp Toggle ran')
};
const reRenderComponent= () => {
setRenderCount(renderCount + 1);
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
popUpToggle();
console.log('reRenderComponent ran, and the key is ' + renderCount)
};
useEffect(() => {
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
}, [])
return (
<div>
<button
style={{position: 'fixed', zIndex: '999', right: '0'}}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp ? <Child2 popUpToggle={popUpToggle}/> : null}
{display}
</div>
);
};
export default Parent;
Child 1:
import React from 'react';
const Child1 = ({ popUpToggle, renderCount }) => {
return (
<>
<button onClick={popUpToggle}>
Pop Up Toggle function
</button>
<h1>Child 1 is up, count is {renderCount}</h1>
</>
);
};
export default Child1;
Child 2:
import React, { useState } from 'react';
const Child2 = ({ popUpToggle }) => {
return (
<div
style={{
backgroundColor: 'rgba(0,0,0, .7)',
width: '100vw',
height: '100vh',
margin: '0',
}}
>
<h1>Child 2 is up</h1>
<h2>PopUp active</h2>
<button onClick={popUpToggle}>Toggle Pop Up</button>
</div>
);
};
export default Child2;
setDisplay(<Child1 /*etc*/ />);
Putting elements into state is usually not a good idea. It makes it very easy to cause bugs exactly like the one you're seeing. An element in state never gets updated, unless you explicitly do so, so it can easily refer to stale data. In your case, i think the issue is that the child component has a stale reference to popUpToggle, which in turn has an old instance of popUp in its closure.
The better approach, and the standard one, is for your state to contain just the minimal data. The elements get created when rendering, based on the data. That way, the elements are always in sync with the latest data.
In your case it looks like all the data already exists, so we don't need to add any new state variables:
const Parent = () => {
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(prev => !prev);
};
const reRenderComponent = () => {
setRenderCount(prev => prev + 1);
popUpToggle();
};
return (
<div>
<button
style={{ position: "fixed", zIndex: "999", right: "0" }}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp && <Child2 popUpToggle={popUpToggle} />}
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
</div>
);
};

I want rerender when props data change with react hooks but i got infinite rerender

Sorry for my bad English.. But I need help you guys!
The problem is I wanna rerender when props change which is in EditField Components.
So I tried two ways,
first, I was trying to using useEffect parameter
In UseEffect Hooks I tried useEffect(() => {},[facils]... like this way
but when I tried this way, I got infinite rerender...
So I tried another ways, I passed GetDB function to EditField Components and when i edit my props data i tried call GetDBfunction.
But it didn't work what I expected...
could you give me any advices..?
const GetDB = () => {
const [facils, setFacils] = useState([]);
useEffect(() => {
firebase
.firestore()
.collection("facils")
.get()
.then((snapshot) => {
const newFacils = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
// prevent firebase quote exceed
console.log("!!!!FB warniing!!!!");
setFacils(newFacils);
});
}, []);
return facils;
};
const FacilityLists = () => {
const facils = GetDB();
const [id, setId] = useState([]);
const [name, setName] = useState([]);
const [showEdit, setShowEdit] = useState(false);
...
...
return (
<div>
<div style={{ display: "flex", justifyContent: "space-evenly" }}>
<div>Id</div>
<div>시설이름</div>
</div>
<div>
{facils.length > 0 ? (
facils.map((item) => (
<ul key={item.id} onClick={() => EditItem(item.id, item.name)}>
{item.id} - {item.name}
</ul>
))
) : (
<p>there is no data</p>
)}
</div>
<div>
{showEdit ? (
<EditField
id={id}
name={name}
showSave={() => setShowEdit(false)}
handleChange={handleChange}
refreshData={() => GetDB}
/>
) : (
<CreateField />
)}
</div>
</div>
You cannot call a hook from a handler function. What you can do is implement and expose a fetcher from GetDB hook and call it.
Also make sure that when you create custom hooks, you prefix their name with use since it will tell react to apply the rules of hooks on it and warn you for incorrect usage
const useGetDB = () => {
const [facils, setFacils] = useState([]);
const fetchData = () => {
firebase
.firestore()
.collection("facils")
.get()
.then((snapshot) => {
const newFacils = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
// prevent firebase quote exceed
console.log("!!!!FB warniing!!!!");
setFacils(newFacils);
});
}
useEffect(() => {
fetchData();
}, []);
return { facils, fetchData};
};
Post that you can use it like below
const FacilityLists = () => {
const {facils, fetchData} = useGetDB();
const [id, setId] = useState([]);
const [name, setName] = useState([]);
const [showEdit, setShowEdit] = useState(false);
...
...
return (
<div>
<div style={{ display: "flex", justifyContent: "space-evenly" }}>
<div>Id</div>
<div>시설이름</div>
</div>
<div>
{facils.length > 0 ? (
facils.map((item) => (
<ul key={item.id} onClick={() => EditItem(item.id, item.name)}>
{item.id} - {item.name}
</ul>
))
) : (
<p>there is no data</p>
)}
</div>
<div>
{showEdit ? (
<EditField
id={id}
name={name}
showSave={() => setShowEdit(false)}
handleChange={handleChange}
refreshData={() => fetchData()}
/>
) : (
<CreateField />
)}
</div>
</div>
Rename your GetDB to useFacils. A hook's name starts with use word, and hooks (useEffect) are only called within function components or other hooks. Not sure why you didn't got a lint error when you created the GetDb hook.
from the docs:
Its name should always start with use so that you can tell at a glance
that the rules of Hooks apply to it.
and also:
Do I have to name my custom Hooks starting with “use”? Please do. This
convention is very important. Without it, we wouldn’t be able to
automatically check for violations of rules of Hooks because we
couldn’t tell if a certain function contains calls to Hooks inside of
it.
This was just an advice regarding your hooks name, as Shubham pointed in his answer, to make the refresh work, you should also make your custom hook return the fetch/refresh function.
const useDbFacils = () => {
const [facils, setFacils] = useState([]);
const fetchFacils = React.useCallback(() => {
firebase
.firestore()
.collection("facils")
.get()
.then((snapshot) => {
const newFacils = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setFacils(newFacils);
});
}, [setFacils]);
useEffect(() => {
fetchFacils();
}, [fetchFacils]); // only you knows if this dependency is needed
return [facils, fetchFacils];
};
In your component:
function FacilsList() {
const [facils, fetchFacils] = useDbFacils();
// later
{facils.length > 0 ? facils.map...
// and
<EditField
id={id}
name={name}
showSave={() => setShowEdit(false)}
handleChange={handleChange}
refreshData={fetchFacils}
/>
}
You need to preset the custom hook with "use", or react will not recognize it as a hook and will create a new hook on every render because it will not be memorized.
Change it to useDB and it should work.

Resources