简体   繁体   中英

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM