lllzzzlll
lllzzzlll

Reputation: 29

Too many re-renders. React limits the number of renders to prevent an infinite loop. useState problem?

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

Answers (1)

tmwilliamlin168
tmwilliamlin168

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

Related Questions