You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现帖子点赞/取消点赞后无需刷新页面更新点赞数?

问题描述

开发带帖子点赞功能的项目,前后端点赞/取消点赞交互正常,但点赞数需刷新页面才会更新。尝试将post.likedBy数组长度存入useState,但该值常为undefined,即便显示也无法正常工作。


代码片段

后端代码

async likePost(req, res) {
    try {
      const post = await Post.findOne({ where: { id: req.params.id } });
      const alreadyLiked = await Likes.findOne({ where: { post_id: post.id, user_id: req.session.user.id } });
      if (!alreadyLiked) {
        const like = await Likes.create({ post_id: post.id, user_id: req.session.user.id });
        console.log('post liked');
        res.json({ like });
      } else {
        await Likes.destroy({ where: { post_id: post.id, user_id: req.session.user.id } });
        console.log('post unliked');
        res.sendStatus(200);
      }
    } catch (error) {
      console.log(error);
      res.sendStatus(500).send('Server Error');
    }
  }

likeSlice代码

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
import axios from "axios";

const initialState = {
  likes: []
}

export const upvotePost = createAsyncThunk(
  'likes/upvotePost',
  async (id, { rejectWithValue, dispatch }) => {
    try {
      const response = await axios.post(`/posts/${id}/likes`);
      dispatch(addLike(response.data.like))
    } catch (error) {
      rejectWithValue(error.message);
    }
  }
)

export const likeSlice = createSlice({
  name: 'likes',
  initialState,
  reducers: {
    addLike: (state, action) => {
      state.likes.push(action.payload);
    }
  }
})

const { addLike } = likeSlice.actions;
export default likeSlice.reducer;

帖子组件代码

const OnePost = () => {

  const post = useSelector(state => state.post.post_id);
  const user = useSelector(state => state.auth.user);
  console.log('post', post);
  
 

  const { id } = useParams();
  const dispatch = useDispatch();


  const num = post.likedBy?.length
  console.log('num', num);
  const [like, setLike] = useState(num)
  console.log('like--->', like);
  


  useEffect(() => {
    dispatch(getOnePost(id))
  }, [like])

  const navigate = useNavigate();

  const openUserPageHandler = () => {
    dispatch(getOneUser(post.user_id))
  }


  const likePostHandler = () => {
    // if (post.likedBy?.map(el => el.Likes.user_id == user.id)) {
    //   setLike(prev => prev - 1)
      dispatch(upvotePost(id))
    // } else {
    //   setLike(prev => prev + 1)
    //   dispatch(upvotePost(id))
    // }
  }

  const avatar = `http://localhost:3001${post.userAvatar}`
  const createdAt = new Date(post.createdAt).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' });
  const comments = useSelector(state => state.comment.comments);

  useEffect(() => {
    dispatch(getPostComments(id));
  }, [])


  return (
    <>

      <div className='one-post__container'>
        <div className='one-post'>
          <div className='one-post__info'>
            <div className='one-post__info__head'>
              <div onClick={() => navigate(-1)}>
                <ArrowBackIcon className='one-post__back__arrow' />
              </div>
              <span className='profile__name'>
                <h3>Tweet</h3>
              </span>
            </div>
            <div className='one-post__info__mid'>
              <Link to={`/user/${post.userId}`} className='one-post__link'>
                <div className='one-post__name' onClick={openUserPageHandler}>
                  <img src={post.userAvatar ? avatar : default_avatar} alt='' className='one-post__profile__avatar' />
                  <h4 className='one-post__user__name'>{post.userName}</h4>
                </div>
              </Link>
              <div className='one-post__options'>
                <MoreHoriz />
              </div>
            </div>

            <div className='one-post__body'>{post.text}</div>
            <div className='one-post__footer'>
              <div className='one-post__time'>{createdAt}</div>
              <div className='one-post__icons'>
                <div className='one-post__icons__options'>
                  <ChatBubbleOutline fontSize='small' />
                </div>
                <div className='one-post__icons__options'>
                  <FavoriteBorderOutlined fontSize='small' onClick={likePostHandler} />
                  {like > 0 ? like : null}
                </div>
              </div>
            </div>
          </div>
        </div>
        
      </div>
    </>
  )
}

解决方案

1. 后端返回明确的操作结果和点赞数

后端当前取消点赞仅返回200状态码,前端无法判断操作类型,需修改接口返回操作类型和更新后的点赞数:

async likePost(req, res) {
  try {
    const post = await Post.findOne({ where: { id: req.params.id } });
    const alreadyLiked = await Likes.findOne({ where: { post_id: post.id, user_id: req.session.user.id } });
    let action;

    if (!alreadyLiked) {
      await Likes.create({ post_id: post.id, user_id: req.session.user.id });
      action = 'like';
    } else {
      await Likes.destroy({ where: { post_id: post.id, user_id: req.session.user.id } });
      action = 'unlike';
    }

    // 获取更新后的点赞数
    const updatedPost = await Post.findOne({ 
      where: { id: req.params.id },
      include: [{ model: Likes }]
    });
    const likeCount = updatedPost.Likes.length;

    res.json({ success: true, action, likeCount });
  } catch (error) {
    console.log(error);
    res.status(500).send('Server Error');
  }
}

2. 重构Redux的likeSlice,关联具体帖子

原Slice仅维护全局likes数组,无法对应单个帖子,改为按帖子ID存储点赞状态:

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
import axios from "axios";

const initialState = {
  postLikes: {} // 键为postId,值为{ count: 点赞数, isLiked: 是否已点赞 }
}

export const upvotePost = createAsyncThunk(
  'likes/upvotePost',
  async (id, { rejectWithValue }) => {
    try {
      const response = await axios.post(`/posts/${id}/likes`);
      return { postId: id, ...response.data };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
)

export const likeSlice = createSlice({
  name: 'likes',
  initialState,
  extraReducers: (builder) => {
    builder
      .addCase(upvotePost.fulfilled, (state, action) => {
        const { postId, action: likeAction, likeCount } = action.payload;
        state.postLikes[postId] = {
          count: likeCount,
          isLiked: likeAction === 'like'
        };
      })
      .addCase(upvotePost.rejected, (state, action) => {
        console.error('点赞操作失败:', action.payload);
      });
  }
})

export default likeSlice.reducer;

3. 组件移除本地useState,直接从Redux/Post状态取数

原组件用useState存储点赞数会导致数据不同步,且useEffect依赖like引发无限循环,修改如下:

const OnePost = () => {
  const post = useSelector(state => state.post.post_id);
  const user = useSelector(state => state.auth.user);
  const postLikeInfo = useSelector(state => state.likes.postLikes[id]);
  const { id } = useParams();
  const dispatch = useDispatch();
  const navigate = useNavigate();

  // 优先用Redux实时数据,无数据则用post的likedBy
  const likeCount = postLikeInfo?.count ?? post?.likedBy?.length ?? 0;
  const isLiked = postLikeInfo?.isLiked ?? post?.likedBy?.some(item => item.user_id === user?.id);

  // 初始化加载帖子,依赖id而非like,避免无限循环
  useEffect(() => {
    dispatch(getOnePost(id));
  }, [id, dispatch]);

  const likePostHandler = () => {
    dispatch(upvotePost(id));
  };

  const avatar = `http://localhost:3001${post.userAvatar}`;
  const createdAt = new Date(post.createdAt).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' });
  const comments = useSelector(state => state.comment.comments);

  useEffect(() => {
    dispatch(getPostComments(id));
  }, [id, dispatch]);

  return (
    <>
      <div className='one-post__container'>
        <div className='one-post'>
          {/* 其余代码不变 */}
          <div className='one-post__footer'>
            <div className='one-post__time'>{createdAt}</div>
            <div className='one-post__icons'>
              {/* 其余图标不变 */}
              <div className='one-post__icons__options'>
                {/* 根据isLiked切换图标 */}
                {isLiked ? (
                  <Favorite fontSize='small' onClick={likePostHandler} />
                ) : (
                  <FavoriteBorderOutlined fontSize='small' onClick={likePostHandler} />
                )}
                {likeCount > 0 ? likeCount : null}
              </div>
            </div>
          </div>
        </div>
      </div>
    </>
  )
}

内容的提问来源于stack exchange,提问作者t1saif

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 18:12:51