~~~~~~~~~~ (Contents) MERN ~~~~~~~~~~~
[MERN①] Express & MongoDB Setup
https://qiita.com/niyomong/private/3281af84486876f897f7
[MERN②]User API Routes & JWT Authentication
https://qiita.com/niyomong/private/c11616ff7b64925f9a2b
[MERN③] Profile API Routes
https://qiita.com/niyomong/private/8cff4e6fa0e81b92cb49
[MERN④] Post API
https://qiita.com/niyomong/private/3ce66f15375ad04b8989
[MERN⑤] Getting Started With React & The Frontend
https://qiita.com/niyomong/private/a5759e2fb89c9f222b6b
[MERN⑥] Redux Setup & Alerts
https://qiita.com/niyomong/private/074c27259924c7fd306b
[MERN⑦] React User Authentication
https://qiita.com/niyomong/private/37151784671eff3b92b6
[MERN⑧] Dashboard & Profile Management
https://qiita.com/niyomong/private/ab7e5da1b1983a226aca
[MERN⑨] Profile Display
https://qiita.com/niyomong/private/42426135e959c7844dcb
[MERN⑩] Posts & Comments
https://qiita.com/niyomong/private/19c78aea482b734c3cf5
[MERN11] デプロイ
https://qiita.com/niyomong/private/150f9000ce51548134ad
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. Post Reducer, Action & Initial Component
① GET_POSTSとPOST_ERRORアクションを設置
//...
export const ACCOUNT_DELETED = 'ACCOUNT_DELETED';
+ export const GET_POSTS = 'GET_POSTS';
+ export const POST_ERROR = 'POST_ERROR';
② postReducerを生成
//...
import profile from './profile';
+ import post from './post';
export default combineReducers({
alert,
auth,
profile,
+ post,
});
import { GET_POSTS, POST_ERROR } 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 POST_ERROR:
return {
...state,
error: payload,
loading: false,
};
default:
return state;
}
}
③ postアクション作成
import axios from 'axios';
import { setAlert } from './alert';
import { GET_POSTS, POST_ERROR } 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 (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
④ posts
ルートをApp.jsに設置
//...
+ import Posts from './components/posts/Posts';
import PrivateRoute from './components/routing/PrivateRoute';
//...
<Switch>
<Route exact path="/register" component={Register} />
<Route exact path="/login" component={Login} />
+ <Route exact path="/posts" component={Posts} />
<PrivateRoute exact path="/dashboard" component={Dashboard} />
//...
⑤ 左上のサービス名にPosts一覧リンクを設置
//...
return (
<nav className="navbar bg-dark">
<h1>
+ <Link to="/posts">
<i className="fas fa-code" /> Refnote
</Link>
</h1>
{!loading && (
<Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
//...
2. Post Item Component
① Postsコンポーネントを生成。
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import { getPosts } from '../../actions/post';
const Posts = ({ getPosts, post: { posts, loading } }) => {
useEffect(() => {
getPosts();
}, [getPosts]);
return loading ? (
<Spinner />
) : (
<Fragment>
<h1 className="large text-primary">Posts</h1>
<p className="lead">
<i className="fas fa-user" /> Welcome to the Posts
</p>
{/* PostForm */}
<div className="Posts">
{posts.map((post) => (
<PostItem key={post._id} post={post} />
))}
</div>
</Fragment>
);
};
Posts.propTypes = {
getPosts: PropTypes.func.isRequired,
post: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
post: state.post,
});
export default connect(mapStateToProps, { getPosts })(Posts);
② PostItemコンポーネントを生成。
以下項目追加。
・写真{avatar}、ユーザ名{name}、投稿日時{date}、投稿内容{text}、likes数{likes.length}、comments数{comments.length}、認証済の場合の削除ボタン{!auth.loading && user ===...}
・ProfileDisplayリンク設置{/profile/${user}
}
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Moment from 'react-moment';
import { connect } from 'react-redux';
const PostItem = ({
auth,
post: { _id, text, name, avatar, user, likes, comments, date },
}) => {
return (
<div class="post bg-white p-1 my-1">
<div>
<Link to={`/profile/${user}`}>
<img class="round-img" src={avatar} alt="" />
<h4>{name}</h4>
</Link>
</div>
<div>
<p class="my-1">{text}</p>
<p class="post-date">
Posted on <Moment format="YYY/MM/DD">{date}</Moment>
</p>
<button type="button" class="btn btn-light">
<i class="fas fa-thumbs-up" />{' '}
{likes.length > 0 && <span>{likes.length}</span>}
</button>
<button type="button" class="btn btn-light">
<i class="fas fa-thumbs-down"></i>
</button>
<Link to={`/posts/${_id}`} class="btn btn-primary">
Discussion{' '}
{comments.length > 0 && (
<span class="comment-count">{comments.length}</span>
)}
</Link>
{auth.isAuthenticated === false
? null
: !auth.loading &&
user === auth.user._id && (
<button
onClick={(e) => deletePost(_id)}
type="button"
className="btn btn-danger"
>
<i className="fas fa-times"></i>
</button>
)}
</div>
</div>
);
};
PostItem.propTypes = {
post: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
});
export default connect(mapStateToProps, {})(PostItem);
3. Like & Unlike Functionality
① TYPEアクションにUPDATE_LIKES設置
actions/types.js
//...
+ export const UPDATE_LIKES = 'UPDATE_LIKES';
② Likeアクションを設置
・Get postsをコピペしてAddLike
を加工
・api/postsAPIにputしたい(payloadは、postのidとlikes)
・removeLikeはaddLikeとほぼ同じ。
//...
+ import { GET_POSTS, POST_ERROR, UPDATE_LIKES } from './types';
//...
// 以下、Get postsをコピペして加工
// Add like
export const AddLike = (id) => async (dispatch) => {
try {
const res = await axios.put(`/api/posts/like/${id}`);
dispatch({
type: UPDATE_LIKES,
payload: { id, likes: res.data },
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
// Remove like
export const removeLike = (id) => async (dispatch) => {
try {
const res = await axios.put(`/api/posts/unlike/${id}`);
dispatch({
type: UPDATE_LIKES,
payload: { id, likes: res.data },
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
③ UPDATE_LIKE Reducerを設置
・postsを操作する。each post
・postのidとpayloadのid(actionで設置)がマッチしていれば、postとlikesを返す。マッチしていなければ、do nothing(ただpostを返す)
+ import { GET_POSTS, POST_ERROR, UPDATE_LIKES } from '../actions/types';
//...以下追加。
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map((post) =>
post._id === payload.id ? { ...post, likes: payload.likes } : post
),
loading: false,
};
default:
return state;
}
}
④ PostItemコンポーネントにLikeボタン設置
//...
+ import { addLike, removeLike } from '../../actions/post';
const PostItem = ({
+ addLike,
+ removeLike,
auth,
post: { _id, text, name, avatar, user, likes, comments, date },
}) => {
return (
//...
<button
+ onClick={(e) => addLike(_id)}
type="button"
className="btn btn-light"
>
<i className="fas fa-thumbs-up" />{' '}
{likes.length > 0 && <span>{likes.length}</span>}
</button>
<button
+ onClick={(e) => removeLike(_id)}
type="button"
className="btn btn-light"
>
<i className="fas fa-thumbs-down"></i>
</button>
//...
PostItem.propTypes = {
post: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
+ addLike: PropTypes.func.isRequired,
+ removeLike: PropTypes.func.isRequired,
};
+ export default connect(mapStateToProps, { addLike, removeLike })(PostItem);
4. Deleting Posts
① DELETE_POSTタイプ
//...
+ export const DELETE_POST = 'DELETE_POST';
② deletePostアクション設置
・removeLikeアクションをコピペして更新
//...
+ import { DELETE_POST, GET_POSTS, POST_ERROR, UPDATE_LIKES } from './types';
//...
// Delete post
export const deletePost = (id) => async (dispatch) => {
try {
await axios.delete(`/api/posts/${id}`);
dispatch({
type: DELETE_POST,
payload: id,
});
dispatch(setAlert('Post Removed', 'success'));
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
③ DELETE_POST reducer
//..
UPDATE_LIKES,
+ DELETE_POST,
} from '../actions/types';
//...
+ case DELETE_POST:
+ return {
+ ...state,
+ posts: state.posts.filter((post) => post._id !== payload),
+ loading: false,
+ };
//...
④ Deleteボタン設置。
//...
+ import { addLike, removeLike, deletePost } from '../../actions/post';
const PostItem = ({
addLike,
removeLike,
+ deletePost,
auth,
post: { _id, text, name, avatar, user, likes, comments, date },
}) => {
return (
//...
{!auth.loading && user === auth.user._id && (
<button
+ onClick={(e) => deletePost(_id)}
type="button"
className="btn btn-danger"
>
<i className="fas fa-times"></i>
</button>
)}
//...
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
);
5. Adding Posts
① ADD_POSTアクションタイプ設置
//...
+ export const ADD_POST = 'ADD_POST';
② addPostアクション
・deletePostアクションをコピペして編集。
//...
import {
+ ADD_POST,
DELETE_POST,
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
} from './types';
//...
// 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 (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
④ ADD_POST Reducer
~~~ 説明 ~~~
posts: [payload(Newポスト), ...state.posts(既存ポスト配列)]
→ 配列の順番もNewポストが一番上に来るようになる。
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
+ ADD_POST,
} from '../actions/types';
//...
switch (type) {
//...
+ case ADD_POST:
+ return {
+ ...state,
+ posts: [payload, ...state.posts],
+ loading: false,
+ };
//...
⑤ポストフォームコンポーネントを生成
【前提】 rafcp + htmlテンプレ + Fragment+className以外の追加を + で表記。
+ import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
+ import { connect } from 'react-redux';
+ import { addPost } from '../../actions/post';
+ const PostForm = ({ addPost }) => {
+ const [text, setText] = useState('');
return (
<Fragment>
<div className="post-form">
<div className="bg-primary p">
<h3>Say Something...</h3>
</div>
<form
className="form my-1"
+ onSubmit={(e) => {
+ e.preventDefault();
+ addPost({ text });
+ setText('');
+ }}
>
<textarea
name="text"
cols="30"
rows="5"
placeholder="Create a post"
+ value={text}
+ onChange={(e) => setText(e.target.value)}
required
></textarea>
<input type="submit" className="btn btn-dark my-1" value="Submit" />
</form>
</div>
</Fragment>
);
};
PostForm.propTypes = {
+ addPost: PropTypes.func.isRequired,
};
+ export default connect(null, { addPost })(PostForm);
⑥Postコンポーネントにを追加。
//...
import PostItem from './PostItem';
+ import PostForm from './PostForm';
import { getPosts } from '../../actions/post';
//...
return loading ? (
<Spinner />
) : (
<Fragment>
<h1 className="large text-primary">Posts</h1>
<p className="lead">
<i className="fas fa-user" /> Welcome to the Posts
</p>
+ <PostForm />
<div className="Posts">
{posts.map((post) => (
<PostItem key={post._id} post={post} />
))}
</div>
</Fragment>
);
};
//...
6. Single Post Display
GET_POSTアクションタイプ
//...
export const GET_POSTS = 'GET_POSTS';
+ export const GET_POST = 'GET_POST';
//...
① getPost
・getPostsをコピペして一部修正。
//...
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
+ GET_POST,
} from './types';
//...
//以下すべて追加。
// 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 (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
② GET_POSTリデューサーを設置。
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
+ GET_POST,
} from '../actions/types';
//...
switch (type) {
case GET_POSTS:
return {
...state,
posts: payload,
loading: false,
};
+ case GET_POST:
+ return {
+ ...state,
+ post: payload,
+ loading: false,
+ };
③ Postルート設置。
//...
import Posts from './components/posts/Posts';
+ import Post from './components/post/Post';
//...
<Route exact path="/posts" component={Posts} />
+ <Route exact path="/posts/:id" component={Post} />
//...
④ PostItemにshowActions
を設置
~~~ 説明 ~~~
showActions: 他のコンポーネントでshowActions内のFragmentは表示させないようにする
・PostItemコンポーネント上では表示させたいので、true
設定
・Postコンポーネント上では非表示にしたいので、false
設定
//...
const PostItem = ({
addLike,
removeLike,
deletePost,
auth,
post: { _id, text, name, avatar, user, likes, comments, date },
+ showActions,
}) => {
return (
//...
+ {showActions && (
+ <Fragment>
<button
onClick={(e) => addLike(_id)}
type="button"
className="btn btn-light"
>
//...
{!auth.loading && user === auth.user._id && (
<button
onClick={(e) => deletePost(_id)}
type="button"
className="btn btn-danger"
>
<i className="fas fa-times"></i>
</button>
)}
+ </Fragment>
+ )}
</div>
</div>
</Fragment>
);
};
+ PostItem.defaultProps = {
+ showActions: true,
+ };
PostItem.propTypes = {
post: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
addLike: PropTypes.func.isRequired,
removeLike: PropTypes.func.isRequired,
deletePost: PropTypes.func.isRequired,
};
//...
⑤ Postコンポーネント
PostItemコンポーネントを持ってくる(しかし、showActionsをfalseにする)
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from '../posts/PostItem';
import { getPost } from '../../actions/post';
const Post = ({ getPost, post: { post, loading }, match }) => {
useEffect(() => {
getPost(match.params.id);
}, [getPost]);
return loading || post === null ? (
<Spinner />
) : (
<Fragment>
<Link to="/posts" className="btn">
Back To Posts
</Link>
<PostItem post={post} showActions={false} />
</Fragment>
);
};
Post.propTypes = {
getPost: PropTypes.func.isRequired,
post: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
post: state.post,
});
export default connect(mapStateToProps, { getPost })(Post);
7. Adding Comments
① アクションタイプ設定
//...
export const ADD_POST = 'ADD_POST';
+ export const ADD_COMMENT = 'ADD_COMMENT';
+ export const REMOVE_COMMENT = 'REMOVE_COMMENT';
② addComment + deleteComment アクションを追加。
・addComment: addPostアクションをコピペして修正。
・deleteComment: addCommentアクションをコピペして修正。
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
+ ADD_COMMENT,
+ REMOVE_COMMENT,
} from './types';
//...
//以下全て追加。
// 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 (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.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 (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
③ ADD_COMMENT + REMOVE_COMMENT リデューサー設置
・comments: payload
-> postのすべてのコメントデータを持ってくる。
・comments: state.post.comments.filter((comment) => comment._id !== payload)
-> 削除したcomment._id以外のすべてのコメントデータを持ってくる。
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
+ ADD_COMMENT,
+ REMOVE_COMMENT,
} from '../actions/types';
//...
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;
}
}
④ コメントフォームを設置。
・Fragment内は、PostFormからコピペして修正。
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addComment } from '../../actions/post';
const CommentForm = ({ postId, addComment }) => {
const [text, setText] = useState('');
return (
<Fragment>
<div className="post-form">
<div className="bg-primary p">
<h3>Leave a Comment</h3>
</div>
<form
className="form my-1"
onSubmit={(e) => {
e.preventDefault();
addComment(postId, { text });
setText('');
}}
>
<textarea
name="text"
cols="30"
rows="5"
placeholder="Create a post"
value={text}
onChange={(e) => setText(e.target.value)}
required
></textarea>
<input type="submit" className="btn btn-dark my-1" value="Submit" />
</form>
</div>
</Fragment>
);
};
CommentForm.propTypes = {
addComment: PropTypes.func.isRequired,
};
export default connect(null, { addComment })(CommentForm);
⑤ PostページにCommentFormを設置。
//...
import PostItem from '../posts/PostItem';
+ import CommentForm from '../post/CommentForm';
import { getPost } from '../../actions/post';
//...
return loading || post === null ? (
<Spinner />
) : (
<Fragment>
<Link to="/posts" className="btn">
Back To Posts
</Link>
<PostItem post={post} showActions={false} />
+ <CommentForm postId={post._id} />
</Fragment>
);
};
//...
8. Comment Display & Delete
use
IDはcomment
から持ってくる。
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Moment from 'react-moment';
import { deleteComment } from '../../actions/post';
const CommentItem = ({
postId,
comment: { _id, text, name, avatar, user, date },
auth,
deleteComment,
}) => {
return (
<Fragment>
<div class="post bg-white p-1 my-1">
<div>
<Link to={`/profile/${user}`}>
<img class="round-img" src={avatar} alt="" />
<h4>{name}</h4>
</Link>
</div>
<div>
<p class="my-1">{text}</p>
<p class="post-date">
Posted on <Moment format="YYY/MM/DD">{date}</Moment>
</p>
{!auth.loading && user === auth.user._id && (
<button
onClick={(e) => deleteComment(postId, _id)}
type="button"
className="btn btn-danger"
>
<i className="fas fa-times" />
</button>
)}
</div>
</div>
</Fragment>
);
};
CommentItem.propTypes = {
postId: PropTypes.string.isRequired,
comment: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
deleteComment: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
});
export default connect(mapStateToProps, { deleteComment })(CommentItem);