React 首次渲染时如何按点赞数从高到低初始排序博客?
博客应用初始渲染排序问题解决
我用React 18开发博客应用,首次渲染时需要从后端拉取所有博客并按点赞数从高到低排序,但在useEffect钩子中实现时要么触发依赖警告,要么导致后端API无限调用。
原App.jsx代码
import { useEffect, useRef, useState } from 'react'; import Blog from './components/Blog'; import LoginForm from './components/LoginForm'; import NewBlogForm from './components/NewBlogForm'; import Notification from './components/Notification'; import Togglable from './components/Togglable'; import blogService from './services/blogs'; import loginService from './services/login'; const App = () => { const [blogs, setBlogs] = useState([]); const blogFormRef = useRef(); const [notification, setNotification] = useState({ message: '', isError: false, }); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [user, setUser] = useState(null); useEffect(() => { blogService.getAll().then((initialBlogs) => setBlogs(initialBlogs)); // 想要在首次渲染时排序博客 }, []); useEffect(() => { const loggedUserJSON = window.localStorage.getItem('loggedBlogappUser'); if (loggedUserJSON) { const user = JSON.parse(loggedUserJSON); setUser(user); blogService.setToken(user.token); } }, []); const compareBlogs = (a, b) => { if (a.likes < b.likes) return 1; if (a.likes > b.likes) return -1; return 0; }; // 这个函数用于更新博客时排序,也想用于初始博客排序 const sortBlogsAsPerMostLikedFirst = (blogs) => { const sortedBlogs = blogs.sort(compareBlogs); setBlogs(sortedBlogs); }; const removeNotificationAfterFiveSeconds = () => { setTimeout(() => { setNotification({ message: '', isError: false, }); }, 5000); }; const handleLogin = async (event) => { event.preventDefault(); try { const user = await loginService.login({ username, password, }); window.localStorage.setItem('loggedBlogappUser', JSON.stringify(user)); blogService.setToken(user.token); setUser(user); } catch (error) { setNotification({ message: error.response.data.error, isError: true, }); removeNotificationAfterFiveSeconds(); } }; const handleLogout = async () => { try { window.localStorage.removeItem('loggedBlogappUser'); setUser(null); setUsername(''); setPassword(''); } catch (exception) { console.error(exception.message); } }; const addBlog = (blogObject) => { blogFormRef.current.toggleVisibility(); blogService .create(blogObject) .then((returnedBlog) => { setBlogs(blogs.concat(returnedBlog)); setNotification({ message: `SUCCESS: Added ${returnedBlog.title} blog`, isError: false, }); removeNotificationAfterFiveSeconds(); }) .catch((error) => { setNotification({ message: error.response.data.error, isError: true, }); removeNotificationAfterFiveSeconds(); }); }; const increaseLikes = (blogToUpdate) => { const blog = blogs.find((b) => b.id === blogToUpdate.id); const updatedBlog = { ...blog, likes: blog.likes + 1 }; blogService .update(blog.id, updatedBlog) .then((returnedBlog) => { const blogList = blogs.map((blog) => blog.id !== blogToUpdate.id ? blog : returnedBlog); // 这里调用排序函数 sortBlogsAsPerMostLikedFirst(blogList); }) .catch((error) => { setNotification({ message: `Blog '${blog.title}' was already removed from server`, isError: true, }); removeNotificationAfterFiveSeconds(); const blogList = blogs.filter((b) => b.id !== blogToUpdate.id); setBlogs(blogList); console.error(error.message); }); }; const deleteBlog = (blog) => { const toDelete = window.confirm(`Delete "${blog.title}" blog?`); console.log({ toDelete }); if (toDelete) { blogService .deleteBlog(blog) .then(() => { const blogList = blogs.filter((b) => blog.id !== b.id); setBlogs(blogList); console.log('Blog deleted'); }) .catch((err) => { console.log(err.message); alert('Sorry! Blog cannot be deleted as it is not created by you.'); }); } }; const loginForm = () => { return ( <Togglable buttonLabel="login"> <LoginForm username={username} password={password} handleUsernameChange={({ target }) => setUsername(target.value)} handlePasswordChange={({ target }) => setPassword(target.value)} handleSubmit={handleLogin} /> </Togglable> ); }; const blogForm = () => ( <Togglable buttonLabel="new blog" ref={blogFormRef}> <NewBlogForm createBlog={addBlog} /> </Togglable> ); return ( <div> <h1>Blogs</h1> <Notification notification={notification} /> {!user && loginForm()} {user && ( <div> <p> {user.name} logged in{' '} <button type="button" onClick={handleLogout}> logout </button> </p> {blogForm()} <ul style={{ listStyleType: 'none', padding: 0 }}> {blogs.map((blog) => ( <Blog key={blog.id} blog={blog} loggedInUsername={user.username} handleLikes={() => increaseLikes(blog)} handleDeleteBlog={() => deleteBlog(blog)} /> ))} </ul> </div> )} </div> ); }; export default App;
遇到的问题
- 尝试直接在useEffect中调用排序函数:
会收到依赖警告:useEffect(() => { blogService.getAll().then((initialBlogs) => sortBlogsAsPerMostLikedFirst(initialBlogs)); }, []);React Hook useEffect has a missing dependency: 'sortBlogsAsPerMostLikedFirst'. Either include it or remove the dependency array. - 若将
sortBlogsAsPerMostLikedFirst加入依赖数组,后端API会被无限调用。
解决方案
方法一:将排序逻辑内联到useEffect中
直接在获取博客的回调里完成排序,避免依赖外部函数,同时注意不要原地修改原数组:
useEffect(() => { blogService.getAll().then((initialBlogs) => { // 复制数组后再排序,防止修改后端返回的原始数据 const sortedBlogs = [...initialBlogs].sort((a, b) => { if (a.likes < b.likes) return 1; if (a.likes > b.likes) return -1; return 0; }); setBlogs(sortedBlogs); }); }, []);
这种写法既满足初始排序需求,又不会触发依赖警告,也不会导致无限渲染。
方法二:用useCallback缓存排序函数
如果需要复用排序函数,用useCallback包裹函数,避免每次组件渲染生成新的函数引用:
// 先引入useCallback import { useEffect, useRef, useState, useCallback } from 'react'; // ... // 用useCallback缓存比较函数 const compareBlogs = useCallback((a, b) => { if (a.likes < b.likes) return 1; if (a.likes > b.likes) return -1; return 0; }, []); // 用useCallback缓存排序函数,依赖compareBlogs const sortBlogsAsPerMostLikedFirst = useCallback((blogs) => { const sortedBlogs = [...blogs].sort(compareBlogs); setBlogs(sortedBlogs); }, [compareBlogs]); // 修改useEffect,加入缓存后的函数作为依赖 useEffect(() => { blogService.getAll().then((initialBlogs) => sortBlogsAsPerMostLikedFirst(initialBlogs)); }, [sortBlogsAsPerMostLikedFirst]);
这样函数引用不会随组件渲染频繁变化,就不会触发useEffect的无限执行循环。
关键注意点
- 数组的
sort方法是原地修改原数组的,排序前一定要复制一份(比如用[...blogs]或者blogs.slice()),避免意外修改后端返回的原始数据。 - 之前的无限API调用问题,本质是每次组件渲染都会生成新的
sortBlogsAsPerMostLikedFirst函数引用,加入useEffect依赖后,函数变化会触发useEffect重新执行,进而更新状态、触发组件重新渲染,形成循环。
内容的提问来源于stack exchange,提问作者qqq
相关产品推荐
相关产品推荐

