Я создаю приложение для социальных сетей, используя стек MERN и Redux в качестве менеджера состояний. У меня есть компонент канала, который отображает компоненты PostItem, которые отображают сообщение и позволяют выполнять такие действия, как симпатия и комментирование. У меня также есть компонент Post, который отображает тот же компонент PostItem, который открывается, когда пользователь нажимает кнопку комментария на компоненте PostItem в ленте. Когда мне нравится публикация через компонент фида, она получает обновленный реквизит и повторно отображает компонент, показывающий изменения. Однако, когда я нажимаю кнопку «Мне нравится» в компоненте Post, он обновляет хранилище Redux, но не получает обновленные реквизиты.
Feed. js
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPosts } from '../../actions/post';
// Components
import UserProfileCard from './UserProfileCard';
import PostForm from './PostForm';
import Footer from '../layout/Footer';
import PostItem from '../posts/PostItem';
import Spinner from '../layout/Spinner';
const Feed = ({ getPosts, post: { posts, loading } }) => {
//Same as component did mount
useEffect(() => {
getPosts();
}, [getPosts]);
return (
<Fragment>
<div className='main-container mt-3'>
<div className='container'>
<div className='row'>
<UserProfileCard />
<PostForm />
</div>
{loading ? (
<Spinner />
) : (
posts.map(post => <PostItem key={post._id} post={post} />)
)}
</div>
</div>
<Footer />
</Fragment>
);
};
Feed.propTypes = {
getPosts: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPosts })(Feed);
Post. js
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPost } from '../../actions/post';
import { Link } from 'react-router-dom';
// Components
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import PostCommentForm from './PostCommentForm';
import Comment from './Comment';
// Assets
import { ArrowLeft } from 'react-feather';
const Post = ({ getPost, post: { post, loading }, match }) => {
useEffect(() => {
// get id from url in params for getPost function
getPost(match.params.id);
}, [getPost]);
return loading || post === null ? (
<Spinner />
) : (
<div className='main-container mt-3'>
<div className='container'>
<div className='row'>
{/* TODO ADD BROWSER HISTORY FUNCTIONALITY TO ALLOW USER TO GO BACK TO PROFILE OR FEED */}
<Link className='mb-1' to='/feed'>
<button className='btn btn-logo-color'>
<ArrowLeft />
</button>
</Link>
<PostItem key={post._id} post={post} />
<PostCommentForm />
</div>
<Comment />
</div>
</div>
);
};
Post.propTypes = {
getPost: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPost })(Post);
PostItem. js
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Moment from 'react-moment';
import { connect } from 'react-redux';
import { addLike, removeLike, deletePost } from '../../actions/post';
// Assets
import { ThumbsUp, ThumbsDown, MessageSquare, XCircle } from 'react-feather';
import avi from '../assets/default-avatar.png';
const PostItem = ({
auth,
post: { _id, text, firstname, lastname, user, likes, comments, date },
addLike,
removeLike,
deletePost
}) => {
return (
<div className='card mt-1 post'>
<div className='card-body'>
<div className='row'>
<div className='col-lg-2'>
<Link className='feed-link' to={`/profile/${user}`}>
<img src={avi} alt='avatar' className='avatar' />
<h5 className='card-title mt-2'>
{firstname} {lastname}
</h5>
</Link>
</div>
<div className='col-lg-10'>
<p className='card-text'>{text}</p>
<p className='text-muted post-date'>
<Moment format='LLL'>{date}</Moment>
</p>
<div className='post-buttons'>
<button
type='button'
className='btn btn-outline-primary mr-1'
onClick={e => addLike(_id)}
>
<ThumbsUp />
<span className='badge badge-light'>
{likes.length > 0 && <span>{likes.length}</span>}
</span>
</button>
<button
type='button'
className='btn btn-outline-danger mr-1'
onClick={e => removeLike(_id)}
>
<ThumbsDown />
</button>
{/* TODO ADD CONDITIONAL RENDERING TO REMOVE WHEN POST IS OPEN */}
<Link to={`/post/${_id}`}>
<button type='button' className='btn btn-outline-info mr-1'>
<MessageSquare />
<span className='badge badge-light'>
{comments.length > 0 && (
<span className='comment-count'>{comments.length}</span>
)}
</span>
</button>
</Link>
{!auth.loading && user === auth.user._id && (
<button
type='button'
className='btn btn-outline-danger mr-1'
onClick={() => deletePost(_id)}
>
<XCircle />
</button>
)}
</div>
</div>
</div>
</div>
</div>
);
};
PostItem.propTypes = {
post: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
addLike: PropTypes.func.isRequired,
removeLike: PropTypes.func.isRequired,
deletePost: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps, { addLike, removeLike, deletePost })(
PostItem
);
Post Reducer
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT
} from '../actions/types';
const initialState = {
posts: [],
post: null,
loading: true,
error: {}
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_POSTS:
return {
...state,
posts: payload,
loading: false
};
case GET_POST:
return {
...state,
post: payload,
loading: false
};
case ADD_POST:
return {
...state,
posts: [payload, ...state.posts],
loading: false
};
case DELETE_POST:
return {
...state,
posts: state.posts.filter(post => post._id !== payload),
loading: false
};
case POST_ERROR:
return {
...state,
error: payload,
loading: false
};
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.postId ? { ...post, likes: payload.likes } : post
),
loading: false
};
case ADD_COMMENT:
return {
...state,
post: { ...state.post, comments: payload },
loading: false
};
case REMOVE_COMMENT:
return {
...state,
post: {
...state.post,
comments: state.post.comments.filter(
comment => comment._id !== payload
)
},
loading: false
};
default:
return {
...state
};
}
}
Действия после публикации
import axios from 'axios';
import { setAlert } from './alert';
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT
} from './types';
//Get posts
export const getPosts = () => async dispatch => {
try {
const res = await axios.get('/api/posts');
dispatch({
type: GET_POSTS,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add like
export const addLike = postId => async dispatch => {
try {
const res = await axios.put(`/api/posts/like/${postId}`);
dispatch({
type: UPDATE_LIKES,
payload: { postId, likes: res.data }
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Remove like
export const removeLike = postId => async dispatch => {
try {
const res = await axios.put(`/api/posts/unlike/${postId}`);
dispatch({
type: UPDATE_LIKES,
payload: { postId, likes: res.data }
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add Post
export const addPost = formData => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
try {
const res = await axios.post('/api/posts', formData, config);
dispatch({
type: ADD_POST,
payload: res.data
});
dispatch(setAlert('Post Created', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Delete Post
export const deletePost = id => async dispatch => {
try {
const res = await axios.delete(`/api/posts/${id}`);
dispatch({
type: DELETE_POST,
payload: id
});
dispatch(setAlert('Post Removed', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
//Get post
export const getPost = id => async dispatch => {
try {
const res = await axios.get(`/api/posts/${id}`);
dispatch({
type: GET_POST,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Add Comment
export const addComment = (postId, formData) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
try {
const res = await axios.post(
`/api/posts/comment/${postId}`,
formData,
config
);
dispatch({
type: ADD_COMMENT,
payload: res.data
});
dispatch(setAlert('Comment Added', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};
// Delete Comment
export const deleteComment = (postId, commentId) => async dispatch => {
try {
const res = await axios.delete(`/api/posts/comment/${postId}/${commentId}`);
dispatch({
type: REMOVE_COMMENT,
payload: commentId
});
dispatch(setAlert('Comment Removed', 'success'));
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.data.msg, status: error.response.status }
});
}
};