Reputation: 29
I'm new to react and I'm learning how to use useState
. The task I want to achieve is to allow the user to select an item in the menu in the UserMenu
component and this component will set the userId
which is a state in the main App
. The result is the page is refreshed to display relevant information for the new userId
.
The method I tried to pass down setUserId
from App
to UserMenu
is using a callback function updateUserId
and getNewUserId
. However, encounter the infinite loop error and I'm not sure what is the cause. Any help is greatly appreciated!
The following are relevant parts.
App.js
function App() {
const classes = useStyles()
const [userId, setUserId] = useState(1)
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
const [userList, setUserList] = useState([])
useEffect(() => {
const getUser = async () => {
const userFromServer = await fetchUser()
if (userFromServer) {
setUser(userFromServer)
} else {
console.log("error")
}
}
getUser()
}, [userId])
useEffect(() => {
const getPosts = async () => {
const postsFromServer = await fetchPosts()
setPosts(postsFromServer)
}
getPosts()
},[userId])
useEffect(() => {
const getUserList = async () => {
const userListFromServer = await fetchUserList()
setUserList(userListFromServer)
}
getUserList()
}, [])
// Fetch user
const fetchUser = async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
const data = await res.json()
return data
}
// Fetch posts
const fetchPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?userId=1')
const data = await res.json()
return data
}
// Fetch list of users
const fetchUserList = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users/')
const data = await res.json()
return data
}
const updateUserId = updatedUserId => {
setUserId(updateUserId)
}
return (
<div>
<Box className={classes.headerImage}>
<UserMenu getNewUserId={updateUserId} userList = {userList} />
</Box>
<Container maxWidth="lg" className={classes.userContainer}>
{user ? <UserInfo user = {user} /> : 'loading...'}
</Container>
<Container maxWidth="lg" className={classes.blogsContainer}>
{user ? <PostList name = {user.name} posts = {posts} /> : 'loading...'}
</Container>
</div>
);
}
export default App;
Full UserMenu.js for reference. I copy-pasted the code from material UI. The only part I modified is in the above snippet. UserMenu.js
import Button from '@material-ui/core/Button';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import Grow from '@material-ui/core/Grow';
import Paper from '@material-ui/core/Paper';
import Popper from '@material-ui/core/Popper';
import MenuItem from '@material-ui/core/MenuItem';
import MenuList from '@material-ui/core/MenuList';
import { makeStyles } from '@material-ui/core/styles';
import { useState, useRef, useEffect } from 'react'
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
},
paper: {
marginRight: theme.spacing(2),
},
button: {
backgroundColor: "rgba(0,0,0,0.1)",
opacity: 0.7,
}
}));
const UserMenu = ({ getNewUserId, userList }) => {
const classes = useStyles();
const [open, setOpen] = useState(false);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (user, event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
console.log(user)
getNewUserId(user.id)
setOpen(false);
};
function handleListKeyDown(event) {
if (event.key === 'Tab') {
event.preventDefault();
setOpen(false);
}
}
// return focus to the button when we transitioned from !open -> open
const prevOpen = useRef(open);
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus();
}
prevOpen.current = open;
}, [open]);
return (
<div className={classes.root}>
<div>
<Button className={classes.button}
ref={anchorRef}
aria-controls={open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
Change User
</Button>
<Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}>
{userList.map((user) => (
<MenuItem onClick={(event) => handleClose(user, event)}>{user.name}</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
);
}
export default UserMenu
Upvotes: 1
Views: 251
Reputation: 571
setUserId(updateUserId)
should be setUserId(updatedUserId)
. If you pass a function (updateUserId
) into setState
, it will actually perform a functional update, as described here: https://reactjs.org/docs/hooks-reference.html#usestate. It is likely that updateUserId
is being called infinitely many times recursively.
Upvotes: 1