useEffect内调用API时Hook调用顺序变更警告的修复求助
解决React Hook调用顺序警告问题
问题重现
PostPage组件代码
const PostPage = () => { const [post, setPost] = useState(null); const [comments, setComments] = useState([]); const router = useRouter() useEffect(() => { if (router.isReady) { apiClient.get(`/posts/${router.query.id}/`) .then((response) => { setPost(response.data) apiClient.get(`/comments/`).then((response) => { setComments(response.data) })}) .catch((error) => console.error('Error fetching post:', error)); }; }, [router.isReady]); if (!post) { return <p>Loading...</p>; } return ( <Grid container spacing={2}> {PostDetails(post, comments)} </Grid> ) }; export default PostPage;
PostDetails组件代码
const PostDetails = (post={}, comments=[]) => { const { title, author, content} = post; const router = useRouter() return ( <div> {/* <Button variant="outlined" color="primary" onClick={() => router.back()}>Close Post</Button> */} <Typography variant="h2">{title}</Typography> <Typography variant="subtitle1">Author: {author}</Typography> <Typography variant="body1" >{content}</Typography> <Typography variant="h3">Comments:</Typography> <List> {comments.map((comment, index) => ( <Comment author={comment.author} text={comment.text} index={index}/> ))} </List> </div> ); }; export default PostDetails;
警告信息
Warning: React has detected a change in the order of Hooks called by PostPage.
Previous render Next render
- useState useState
- useState useState
- useContext useContext
- useEffect useEffect
- undefined useContext ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at PostPage (webpack-internal:///./pages/posts/[id].jsx:20:76)
问题原因
核心问题是Hook调用顺序不一致:
- 第一次渲染时,
post为null,PostPage直接返回<p>Loading...</p>,PostDetails组件不会被渲染,因此其中的useRouter(内部依赖React Context)不会执行。 - 当post加载完成后,第二次渲染PostPage会渲染PostDetails,此时
useRouter被调用,相当于在PostPage的渲染流程中新增了一个Hook调用,违反了React Hook的「每次渲染Hook调用顺序必须一致」规则。
解决方案
方案1:将路由逻辑上移到PostPage,通过props传递
把useRouter从PostDetails移到PostPage,将导航函数作为props传给PostDetails,这样PostDetails不再依赖Hook,避免渲染顺序导致的问题。
修改后的PostPage:
const PostPage = () => { const [post, setPost] = useState(null); const [comments, setComments] = useState([]); const router = useRouter() useEffect(() => { if (router.isReady) { apiClient.get(`/posts/${router.query.id}/`) .then((response) => { setPost(response.data) apiClient.get(`/comments/`).then((response) => { setComments(response.data) })}) .catch((error) => console.error('Error fetching post:', error)); }; }, [router.isReady]); if (!post) { return <p>Loading...</p>; } return ( <Grid container spacing={2}> <PostDetails post={post} comments={comments} onBack={() => router.back()} /> </Grid> ) }; export default PostPage;
修改后的PostDetails:
const PostDetails = ({ post={}, comments=[], onBack }) => { const { title, author, content} = post; return ( <div> <Button variant="outlined" color="primary" onClick={onBack}>Close Post</Button> <Typography variant="h2">{title}</Typography> <Typography variant="subtitle1">Author: {author}</Typography> <Typography variant="body1" >{content}</Typography> <Typography variant="h3">Comments:</Typography> <List> {comments.map((comment, index) => ( <Comment author={comment.author} text={comment.text} index={index}/> ))} </List> </div> ); }; export default PostDetails;
方案2:将Loading逻辑移到PostDetails内部
确保PostDetails在每次渲染时都被调用,避免Hook调用顺序变化。修改PostPage去掉提前返回,把Loading逻辑放到PostDetails里:
修改后的PostPage:
const PostPage = () => { const [post, setPost] = useState(null); const [comments, setComments] = useState([]); const router = useRouter() useEffect(() => { if (router.isReady) { apiClient.get(`/posts/${router.query.id}/`) .then((response) => { setPost(response.data) apiClient.get(`/comments/`).then((response) => { setComments(response.data) })}) .catch((error) => console.error('Error fetching post:', error)); }; }, [router.isReady]); return ( <Grid container spacing={2}> <PostDetails post={post} comments={comments} /> </Grid> ) }; export default PostPage;
修改后的PostDetails:
const PostDetails = ({ post={}, comments=[] }) => { const router = useRouter() if (!post) { return <p>Loading...</p>; } const { title, author, content} = post; return ( <div> <Button variant="outlined" color="primary" onClick={() => router.back()}>Close Post</Button> <Typography variant="h2">{title}</Typography> <Typography variant="subtitle1">Author: {author}</Typography> <Typography variant="body1" >{content}</Typography> <Typography variant="h3">Comments:</Typography> <List> {comments.map((comment, index) => ( <Comment author={comment.author} text={comment.text} index={index}/> ))} </List> </div> ); }; export default PostDetails;
两种方案都能解决Hook调用顺序不一致的问题,推荐方案1,因为它更符合组件职责分离的原则,把路由逻辑放在页面组件中,子组件只负责展示和接收回调。
内容的提问来源于stack exchange,提问作者kirill ras
相关产品推荐
相关产品推荐

