Props in react seems to not be usable right away - reactjs

I have a small issue with a really simple component that doesn't display what I want.
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, []);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};
When I refresh the page it will not display my User cards. But If I had a timeout on useEffect like this :
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
setTimeout(function () {
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, 3000);
}, []);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};
Everything's fine!
I thought props were usable immediately but it seems I was wrong.
I tried to add [props] at the end of useEffect to be sure my state will be updated if props changed, but nothing...
I'm sure it's nothing but I've been struggling since yesterday!
Thank you!

Just add useEffect dependency, which will call your useEffect content every time, when dependency changed:
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, [props]);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};

Related

How to cleanup useRef in useEffect?

I have this component, so I want to clean up in useEffect. I googled the issue and there is no helpful information.
const LoadableImg = ({src, alt}) => {
const [isLoaded, setIsLoaded] = useState(false);
let imageRef = useRef(null);
useEffect(() => {
if (isLoaded) return;
if (imageRef.current) {
imageRef.current.onload = () => setIsLoaded(true);
}
return () => {
imageRef.current = null;
};
}, [isLoaded]);
return (
<div className={isLoaded ? 'l_container_loaded' : 'l_container'}>
<img ref={imageRef} className={isLoaded ? "l_image_loaded" : 'l_image'}
src={src}
alt={alt}/>
</div>
) };
I can't figure out how to clean up in useEffect.
UPDATE
added another useEffect, according to Arcanus answer.
const LoadableImg = ({src, alt}) => {
const [isLoaded, setIsLoaded] = useState(false);
let imageRef = useRef(null);
useEffect(() => {
if (isLoaded) return;
if (imageRef.current) {
imageRef.current.onload = () => setIsLoaded(true);
}
}, [isLoaded]);
useEffect(() => {
return () => {
imageRef.current = null;
};
},[])
return (
<div className={isLoaded ? 'l_container_loaded' : 'l_container'}>
<img ref={imageRef} className={isLoaded ? "l_image_loaded" : 'l_image'}
src={src}
alt={alt}/>
</div>
)};
If you want to do this with a ref, then you will need to remove the onload function, but you do not need to null out imageRef.current:
useEffect(() => {
if (isLoaded) return;
const element = imageRef.current;
if (element) {
element.onload = () => setIsLoaded(true);
return () => {
element.onload = null;
}
}
}, [isLoaded]);
That said, i recommend you do not use a ref for this. A standard onLoad prop will work just as well, without the need for all the extra logic for adding and removing the event listener:
const LoadableImg = ({ src, alt }) => {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div className={isLoaded ? "l_container_loaded" : "l_container"}>
<img
className={isLoaded ? "l_image_loaded" : "l_image"}
src={src}
alt={alt}
onLoad={() => setIsLoaded(true)}
/>
</div>
);
};
In your instance, the only time you want to use useEffect is when DOM is fully loaded, and your ref is ready. Hence you need a hook
E.g.
function useHasMounted() {
const [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
setHasMounted(true);
}, []);
return hasMounted;
}
Then your Component should be corrected to be as follows
const hasMounted = useHasMounted();
useEffect(() => {
if (hasMounted) {
imageRef.current.onload = () => setIsLoaded(true);
}
}, [hasMounted]); //<-- call once when dom is loaded.
I understand you want to call onload whenever images is loaded, however, please do note this do not always work because images loaded from cache does not call onload.

React useEffect not running after state change

I am querying Firebase real time database, saving the result into state and then rendering the results.
My data is not displaying because the page is rendering before the data is had. What I don't understand is why
useEffect(() => {
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
}, [projects]);
My use effect is not running once the projects state change is triggered, populating the array and triggering the conditional render line.
What am I missing here?
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const result = [];
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
useEffect(() => {
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
}, [projects]);
return (
<>
{result.length > 0 && result}
</>
);
};
result should also be a state!
Right now at every rerender result is being set to []. So when the useEffect does kick in, the subsequent rerender would set result to [] again.
This should not be a useEffect. Effects run after rendering, but you're trying to put <Project> elements on the page, which must happen during rendering. Simply do it in the body of your component:
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
const result = [];
for (var key in projects) {
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
result.push(<Project props={projectData} />);
}
return (
<>
{result.length > 0 && result}
</>
);
result.push does not mutate result in place. It instead creates a copy of the array with the new value.
As a solution, you could get your current code working by hoisting result into a state variable like so:
const [result, setResult] useState([])
...
useEffect(() => {
for (var key in projects) {
...
setResult([...result, <Project props={projectData} />])
}
}, [result, projects]);
however, this solution would result in an infinite loop...
My suggestion would be to rework some of the logic and use projects to render your Project components, instead of creating a variable to encapsulate your render Components. Something like this:
const [projects, setProjects] = useState();
const { user } = useUserAuth();
const dbRef = ref(db, `/${user.uid}/projects/`);
useEffect(() => {
onValue(dbRef, (snapshot) => {
setProjects(snapshot.val());
});
}, []);
return (
<>
{projects.length > 0 && projects.map(project=>{
var projectData = {
title: projects[key].title,
description: projects[key].description,
};
return <Project props={projectData} />
})}
</>
);
};
You're component is not re-rendering since react doesn't care about your result variable being filled.
Set it up as a state like this: const [result, setResult] = useState([]);
Then use map to return each item of the array as the desire component:
{result.length > 0 && result.map((data, index) => <Project key={index} props={data} />)}

React replace state array using hooks

I am trying to replace a state array using a separately given array. I am unable to get my state to update to hold the values of the separate array. I've tried several things
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
if (message.length > userFriendsList.length)
{
console.log(message)
setUserFriendsList(message);
console.log(userFriendsList)
}
});
const [userFriendsList, setUserFriendsList] = useState({friendsList: []});
window.chatAPI.recvFriendsList(message => {
if (message.length > userFriendsList['friendsList'].length)
{
console.log(message)
setUserFriendsList({friendsList : message});
console.log(userFriendsList)
}
});
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
if (message.length > userFriendsList.length)
{
console.log(message)
setUserFriendsList([ ... userFriendsList, message]);
console.log(userFriendsList)
}
});
None of these are updating the state.
Edit:
Component -
const FriendsList = () =>
{
const [checkFriendsList, setCheckFriendsList] = useState(true)
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
console.log(message)
setUserFriendsList(oldFriendList => [ ...oldFriendList, ...message]);
console.log(userFriendsList)
});
useEffect ( () => {
if (checkFriendsList)
{
setCheckFriendsList(false);
window.chatAPI.getFriendsList();
}
}, [checkFriendsList])
return (
<div className="friends-list-container">
<List className="friends-list">
<ListItem className="friends-list-title"> Friends List </ListItem>
</List>
</div>
);
}
output:
The problem is in the condition if (message.length > userFriendsList.length).
If message is a non empty-string it will always be longer that your empty userFriendsList state, remove the condition and update the array with:
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
setUserFriendsList(oldFriendList => [ ...oldFriendList, message]);
});
If messageĀ is an array just do:
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
setUserFriendsList(oldFriendList => [ ...oldFriendList, ...message]);
});
Try disabling the if statement.
// if( your condition)
{
setUserFriendsList([ ... userFriendsList, ...message]);
}

Updating React state in nested setTimeout callbacks

Can someone please tell me what's wrong with this and why the state of the 'video variable' remains false? So, even after the h2 element has rendered and is visible (i.e. the state of the video variable has been updated to true), when I click and call the hideVideo function, the video state remains false? Many thanks.
export default function App() {
const [message, showMessage] = useState(false);
const [video, setVideo] = useState(false);
let modalTimeout, videoTimeout;
useEffect(() => {
window.addEventListener("click", hideVideo);
setupTimeouts();
return () => {
clearTimeout(modalTimeout);
clearTimeout(videoTimeout);
};
}, []);
const setupTimeouts = () => {
modalTimeout = setTimeout(() => {
showMessage(true);
videoTimeout = setTimeout(() => {
showMessage(false);
setVideo(true);
}, 4000);
}, 2000);
};
const hideVideo = () => {
console.log(video);
showMessage(false);
if (video === true) {
setVideo(false);
}
};
return (
<div className="App">
{message && <h1>Message</h1>}
{video && <h2>Video</h2>}
</div>
);
}
When you call useEffect the window listener attach the default video value that is false to the function hideVideo() so it will be always false, I created a button to show you that the video state value does change. check the last test function
export default function App() {
const [message, showMessage] = useState(false);
const [video, setVideo] = useState(false);
let modalTimeout, videoTimeout;
useEffect(() => {
window.addEventListener("click", hideVideo);
setupTimeouts();
return () => {
clearTimeout(modalTimeout);
clearTimeout(videoTimeout);
};
}, []);
const setupTimeouts = () => {
modalTimeout = setTimeout(() => {
showMessage(true);
videoTimeout = setTimeout(() => {
showMessage(false);
setVideo(true);
}, 4000);
}, 2000);
};
const hideVideo = () => {
console.log(video);
showMessage(false);
if (video) {
setVideo(false);
}
};
const test = (event) => {
event.stopPropagation();
console.log(video)
}
return (
<>
{message && <h1>Message</h1>}
{video && <h2>Video</h2>}
<button onClick={test}>test</button>
</>
);
}

react hooks issue when infinite paging filter applied

I have an infinite paging setup in a react redux project like this..
const ItemDashboard = () => {
const items= useSelector(state => state.items.items);
const dispatch = useDispatch();
const [loadedItems, setLoadedItems] = useState([]);
const [categories, setCategories] = useState([
'cycling',
'diy',
'electrical',
'food',
'motoring',
'travel'
]);
const initial = useRef(true);
const [loadingInitial, setLoadingInitial] = useState(true);
const [moreItems, setMoreItems] = useState([]);
const onChangeFilter = (category, show) => {
!show
? setCategories(categories.filter(c => c != category))
: setCategories([...categories, category]);
};
const loadItems = () => {
dispatch(getItems(categories, items && items[items.length - 1]))
.then(more => setMoreItems(more));
}
const getNextItems = () => {
loadItems();
};
useEffect(() => {
if(initial.current) {
loadItems();
setLoadingInitial(false);
initial.current = false;
}
}, [loadItems]);
useEffect(() => {
if(items) {
setLoadedItems(loadedItems => [...loadedItems, ...items]);
}
}, [items]);
useEffect(() => {
//this effect is fired on intial load which is a problem!
setLoadingInitial(true);
initial.current = true;
}, [categories]);
return (
<Container>
<Filter onFilter={onChangeFilter} categories={categories} />
{loadingInitial ? (
<Row>
<Col sm={8} className='mt-2'>
<LoadingComponent />
</Col>
</Row>
) : (
<ItemList
items={loadedItems}
getNextItems={getNextItems}
moreItems={moreItems}
/>
)}
</Container>
);
};
In the filter component, when the filter is changed the onChangeFilter handler method is fired which updates the array of categories in state. When this filter is changed I need to set the loadedItems in state to an empty array and call the load items callback again but I can't work out how to do it. If I add another effect hook with a dependency on categories state, it fires on the initial load also. I'm probably doing this all wrong as it feels a bit hacky the whole thing. Any advice much appreciated.

Resources