~~~~~~~~~~ (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. Protected Route For Dashboard
① dashboardフォルダ生成 -> Dashboardコンポーネントファイル生成
//rafcp(ショートカット) -> 'Enter'
import React from 'react';
import PropTypes from 'prop-types';
const Dashboard = (props) => {
return <div>Dashboard</div>;
};
Dashboard.propTypes = {};
export default Dashboard;
② PrivateRoute設定 + App.jsでPrivateRoute設置
~~ 詳細な説明 ~~
(1) 小文字のcomponent propsを大文字のComponent変数に代入。
-> 残りのpropsは全て...rest
に格納
(2) App.jsが受け入れているRoute
を呼び出す。
・未認証 AND 未lodingの場合は、/login
に遷移
・認証済みの場合は、App.jsのPrivateRouteの先のComponent(例;Dashboardコンポ)に遷移。
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const PrivateRoute = ({
(1) component: Component,
auth: { isAuthenticated, loading },
(1) ...rest
}) => (
<Route
(2) {...rest}
(2) render={(props) =>
(2) !isAuthenticated && !loading ? (
(2) <Redirect to="/login" />
(2) ) : (
(2) <Component {...props} />
)
}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
});
export default connect(mapStateToProps)(PrivateRoute);
//...
import Alert from './components/layout/Alert';
+ import Dashboard from './components/dashboard/Dashboard';
+ import PrivateRoute from './components/routing/PrivateRoute';
//Redux
//...
return (
//...
<Switch>
<Route exact path="/register" component={Register} />
<Route exact path="/login" component={Login} />
+ <PrivateRoute exact path="/dashboard" component={Dashboard} />
</Switch>
//...
2. Profile Reducer & Get Current Profile
① PROFILE TYPE追加
+ export const GET_PROFILE = 'GET_PROFILE';
+ export const PROFILE_ERROR = 'PROFILE_ERROR';
② ProfileReducer生成
import { GET_PROFILE, PROFILE_ERROR } from '../actions/types';
const initialState = {
profile: null,
profiles: [], //profile listingページ用
repos: [], //不要かも。。。
loading: true,
error: {},
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_PROFILE:
return {
...state,
profile: payload,
loading: false,
};
case PROFILE_ERROR:
return {
...state,
error: payload,
loading: false,
profile: null
};
default:
return state;
}
}
③ Profileアクション
import axios from 'axios';
import { setAlert } from './alert';
import { GET_PROFILE, PROFILE_ERROR } from './types';
// Get current users profile
export const getCurrentProfile = () => async (dispatch) => {
try {
const res = await axios.get('/api/profile/me');
dispatch({
type: GET_PROFILE,
payload: res.data,
});
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
④ Dashboardコンポーネント
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getCurrentProfile } from '../../actions/profile';
const Dashboard = ({ getCurrentProfile, auth, profile }) => {
useEffect(() => {
getCurrentProfile();
}, [getCurrentProfile]);
return <div>Dashboard</div>;
};
Dashboard.propTypes = {
getCurrentProfile: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
profile: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
profile: state.profile,
});
export default connect(mapStateToProps, { getCurrentProfile })(Dashboard);
3. Starting On The Dashboard + Spinner
① NavbarにDashboardリンクを設置。
//...
const Navbar = ({ auth: { isAuthenticated, loading }, logout }) => {
const authLinks = (
<ul>
+ <li>
+ <Link to="/dashboard">
+ <i className="fas fa-user" />
+ <span className="hide-sm">Dashboard</span>
+ </Link>
+ </li>
<li>
//...
② Landingに認証済みならDashboardに遷移するように設定。
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
+ import { connect } from 'react-redux';
+ import PropTypes from 'prop-types';
+ const Landing = ({ isAuthenticated }) => {
+ if (isAuthenticated) {
+ return <Redirect to="/dashboard" />;
+ }
//...
+ Landing.propTypes = {
+ isAuthenticated: PropTypes.bool,
+ };
+ const mapStateToProps = (state) => ({
+ isAuthenticated: state.auth.isAuthenticated,
+ });
+ export default connect(mapStateToProps)(Landing);
③ スピナーを設置
spinner.gif
はインターネットから持ってくる。
import React, { Fragment } from 'react';
import spinner from `./spinner.gif`;
export default () => (
<Fragment>
<img
src={spinner}
style={{ width: '200px', margin: 'auto', display: 'block' }}
alt='Loading...'
/>
</Fragment>
);
④ ログアウトした時に、前回ログインしたユーザーの情報がStateに残ってる原因を解消。
-> CLEAR_PROFILE
をauthアクションとprofileリデューサに設置。
export const GET_PROFILE = 'GET_PROFILE';
export const PROFILE_ERROR = 'PROFILE_ERROR';
+ export const CLEAR_PROFILE = 'CLEAR_PROFILE';
//...
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
LOGOUT,
+ CLEAR_PROFILE,
} from './types';
import setAuthToken from '../utils/setAuthToken';
//...
// Logout / Clear Profile
export const logout = () => (dispatch) => {
+ dispatch({ type: CLEAR_PROFILE });
dispatch({ type: LOGOUT });
};
+ import { GET_PROFILE, PROFILE_ERROR, CLEAR_PROFILE } from '../actions/types';
//...
case PROFILE_ERROR:
return {
...state,
error: payload,
loading: false,
profile: null,
};
+ case CLEAR_PROFILE:
+ return {
+ ...state,
+ profile: null,
};
default:
//...
⑤ Spinnerコンポ設置と、Profile設定されてない場合の設定。
import React, { Fragment, useEffect } from 'react';
+ import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+ import Spinner from '../layout/Spinner';
import { getCurrentProfile } from '../../actions/profile';
const Dashboard = ({
getCurrentProfile,
+ auth: { user },
+ profile: { profile, loading },
}) => {
useEffect(() => {
getCurrentProfile();
}, []);
// 以下すべて追加。
return loading && profile === null ? (
<Spinner />
) : (
<Fragment>
<h1 className="large">Dashboard</h1>
<p className="lead"></p>
<i className="fas fa-user"> Welcome {user && user.name}</i>
{profile !== null ? (
<Fragment>has</Fragment>
) : (
<Fragment>
<p>You have not yet set up a profile, please add some info</p>
<Link to="/create-profile" className="btn btn-primary my-1">
Create Profile
</Link>
</Fragment>
)}
</Fragment>
);
};
//...
4. CreateProfile Component
① profile-formsフォルダ生成 -> profile-formsファイル生成
~~~ 詳細説明 ~~~
(1) rafcp -> 'Enter'
(2) HooksであるuseStateを設置
Hooks(Classコンポーネントと関数(const xx)コンポーネントの違い)とは
今までは...
・クラスはstateを保持できるが、関数はそれができない。
・クラスはライフサイクル(componentDidMount, componentDidUpdate)を持つが、関数はそれを持たない。
・クラスはPureComponentを継承できるが、関数は都度レンダリングされる。
が、しかし!!! これらを解消するのが、Hooooks!!
(Hooksのルール - 名前が「use」で始まらなければならない)
【書き方】 const [state名, set更新関数 ] = useState;
(3) プロパティをformDataに格納
(4) html_themeをコピペ -> Fragment
で囲む -> class
をclassName
に変更
(5) ソーシャルメディアをトグル設定 'toggleSocialInputs'
(6)イベントハンドラーonChangeを定義(フォームの選択や入力欄に変更があった場合に指定の処理を行わせる)
-> HooksのuseStateを更新するためのset更新関数
を設定。(今回はsetFormData
)
-> inputタグ
に、value={プロパティ名}
とonChange={(e) => onChange(e)}
を追加
(2)import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const CreateProfile = (props) => {
(2) const [formData, setFormData] = useState({
status: '', skills: '', bio: '', website: '', youtube: '', twitter: '', facebook: '', linkedin: '', });
(5) const [displaySocialInputs, toggleSocialInputs] = useState(false);
(3) const { status, skills, bio, website, youtube, twitter, facebook, linkedin } = formData;
const onChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
return (
<Fragment>
<h1 className="large text-primary">Create Your Profile</h1>
<p className="lead">
<i className="fas fa-user"></i> Let's get some information to make your
profile stand out
</p>
<small>* = required field</small>
<form className="form">
<div className="form-group">
<select name="status" value={status} onChange={(e) => onChange(e)}>
<option value="0">* Select Professional Status</option>
<option value="Developer">Developer</option>
<option value="Junior Developer">Junior Developer</option>
<option value="Senior Developer">Senior Developer</option>
<option value="Manager">Manager</option>
<option value="Student or Learning">Student or Learning</option>
<option value="Instructor">Instructor or Teacher</option>
<option value="Intern">Intern</option>
<option value="Other">Other</option>
</select>
<small className="form-text">
Give us an idea of where you are at in your career
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="Website"
name="website"
value={website}
onChange={(e) => onChange(e)}
/>
<small className="form-text">
Could be your own or a company website
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="* Skills"
name="skills"
value={skills}
onChange={(e) => onChange(e)}
/>
<small className="form-text">
Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
</small>
</div>
<div className="form-group">
<textarea
placeholder="A short bio of yourself"
name="bio"
value={bio}
onChange={(e) => onChange(e)}
></textarea>
<small className="form-text">Tell us a little about yourself</small>
</div>
<div className="my-2">
<button
(5) onClick={() => toggleSocialInputs(!displaySocialInputs)}
type="button"
className="btn btn-light"
>
Add Social Network Links
</button>
<span>Optional</span>
</div>
(5) {displaySocialInputs && (
<Fragment>
<div className="form-group social-input">
<i className="fab fa-twitter fa-2x"></i>
<input
type="text"
placeholder="Twitter URL"
name="twitter"
value={twitter}
onChange={(e) => onChange(e)}
/>
</div>
<div className="form-group social-input">
<i className="fab fa-facebook fa-2x"></i>
<input
type="text"
placeholder="Facebook URL"
name="facebook"
value={facebook}
onChange={(e) => onChange(e)}
/>
</div>
<div className="form-group social-input">
<i className="fab fa-youtube fa-2x"></i>
<input
type="text"
placeholder="YouTube URL"
name="youtube"
value={youtube}
onChange={(e) => onChange(e)}
/>
</div>
<div className="form-group social-input">
<i className="fab fa-linkedin fa-2x"></i>
<input
type="text"
placeholder="Linkedin URL"
name="linkedin"
value={linkedin}
onChange={(e) => onChange(e)}
/>
</div>
</Fragment>
(5) )}
<input type="submit" className="btn btn-primary my-1" />
<a className="btn btn-light my-1" href="dashboard.html">
Go Back
</a>
</form>
</Fragment>
);
};
CreateProfile.propTypes = {};
export default CreateProfile;
② App.jsにCreateProfile
ルートを設置
//...
import Dashboard from './components/dashboard/Dashboard';
+ import CreateProfile from './components/profile-forms/CreateProfile';
import PrivateRoute from './components/routing/PrivateRoute';
//...
<PrivateRoute exact path="/dashboard" component={Dashboard} />
+ <PrivateRoute exact path="/create-profile" component={CreateProfile} />
//...
5. Create Profile Action
① Create or update profile
(1): withRouterとセットで使用。次の②参照。
//...
// Create or update profile
export const createProfile = (formData, history, edit = false) => async (
dispatch
) => {
try {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const res = await axios.post('/api/profile', formData, config);
dispatch({
type: GET_PROFILE,
payload: res.data,
});
dispatch(setAlert(edit ? 'Profile Updated' : 'Profile Created'), 'sucess');
if (!edit) {
(1) history.push('/dashboard');
}
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
}
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
② ProfileコンポーネントにcreateProfileアクションを設置。
(1) react-routerのページ遷移をhandleで行う時にはwithRouterを使う
・react-routerでページ遷移の基本はLinkだが、onClickなどのhandleでは使えない
=> handleで呼び出すには、withRouter(XxxYyyy)とthis.props.history.push(/zzzz)を使う。
import React, { Fragment, useState } from 'react';
(1)+ import { Link, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+ import { createProfile } from '../../actions/profile';
(1)+ const CreateProfile = ({ createProfile, history }) => {
const [formData, setFormData] = useState({
//...
const onChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
+ const onSubmit = (e) => {
+ e.preventDefault();
(1)+ createProfile(formData, history);
+ };
//...
<small>* = required field</small>
+ <form className="form" onSubmit={onSubmit}>
<div className="form-group">
<select name="status" value={status} onChange={(e) => onChange(e)}>
//...
CreateProfile.propTypes = {
+ createProfile: PropTypes.func.isRequired,
};
(1)+ export default connect(null, { createProfile })(withRouter(CreateProfile));
6. Edit Profile
① Editボタン設置。
rafe -> 'Enter'
//以下全て追加
import React from 'react';
import { Link } from 'react-router-dom';
const DashboardActions = () => {
return (
<div className="dash-buttons">
<Link to="/edit-profile" className="btn btn-light">
<i className="fas fa-user-circle text-primary"></i> Edit Profile
</Link>
</div>
);
};
export default DashboardActions;
//...
+ import DashboardActions from './DashboardActions';
import { getCurrentProfile } from '../../actions/profile';
//...
<Fragment>
<h1 className="large">Dashboard</h1>
<p className="lead"></p>
<i className="fas fa-user"> Welcome {user && user.name}</i>
{profile !== null ? (
<Fragment>
+ <DashboardActions />
</Fragment>
) : (
<Fragment>
<p>You have not yet set up a profile, please add some info</p>
//...
② Edit Profileコンポーネント
(1) CreateProfile.jsと同じようなコードなのでコピペ
(2) useEffect
はクラスのライフサイクルcomponentDidMount, componentDidUpdateとcomponentWillUnmountの3つと同様な処理を行うことができるHooks。useEffectを利用することでコンポーネントをレンダリングする際に外部のサーバからAPIを経由してデータを取得したり、コンポーネントが更新する度に別の処理を実行するということが可能。
(参考URL) https://reffect.co.jp/react/react-useeffect-understanding#React_useEffect
(3) useEffectの第二引数[配列]
・[state変数]配列にstate変数を追加した場合 -> ある特定のstate変数の更新があった時だけuseEffectを実行する。=>「useEffectがstate変数に依存している状態」
・にした場合 -> 最初の一回のuseEffectは実行されるが、それ以降はuseEffectが更新されない。*しかし、空配列を渡すとバグを起こしやすい
(4) actions/profile.jsのcreateProfile変数にデフォルトだとedit = false
を格納しているので、EditProfileの時は->true
にしてやる。
(2)+ import React, { Fragment, useState, useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+ import { createProfile, getCurrentProfile } from '../../actions/profile';
+ const EditProfile = ({
+ profile: { profile, loading },
createProfile,
+ getCurrentProfile,
history,
}) => {
const [formData, setFormData] = useState({
//...
(2)//userEffect以下すべて追加
useEffect(() => {
if (!profile) getCurrentProfile();
if (!loading && profile) {
const profileData = { ...initialState };
for (const key in profile) {
if (key in profileData) profileData[key] = profile[key];
}
for (const key in profile.social) {
if (key in profileData) profileData[key] = profile.social[key];
}
if (Array.isArray(profileData.skills)) {
profileData.skills = profileData.skills.join(', ');
}
setFormData(profileData);
}
(3) }, [loading, getCurrentProfile, profile]);
//...
const onChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = (e) => {
e.preventDefault();
(4)+ createProfile(formData, history, true);
};
//...
+ EditProfile.propTypes = {
createProfile: PropTypes.func.isRequired,
+ getCurrentProfile: PropTypes.func.isRequired,
+ profile: PropTypes.object.isRequired,
};
+ const mapStateToProps = (state) => ({
+ profile: state.profile,
+});
+ export default connect(mapStateToProps, { createProfile, getCurrentProfile })(
withRouter(EditProfile)
);
③ App.jsにEditProfileのルート設定。
//...
import CreateProfile from './components/profile-forms/CreateProfile';
+ import EditProfile from './components/profile-forms/EditProfile';
//...
<PrivateRoute exact path="/create-profile" component={CreateProfile} />
+ <PrivateRoute exact path="/edit-profile" component={EditProfile} />
</Switch>
//...
7. Delete Account
① TypeアクションにDELETE_ACCOUNT追加
//...
+ export const DELETE_ACCOUNT = 'DELETE_ACCOUNT';
② profileアクションにdeleteAccount
関数追加。
-> 安易にアカウント削除できないように確認ウィンドウの設置。
//...
import {
ACCOUNT_DELETED,
CLEAR_PROFILE,
GET_PROFILE,
PROFILE_ERROR,
ACCOUNT_DELETED,
} from './types';
//...
// Delete account & profile
export const deleteAccount = () => async (dispatch) => {
if (window.confirm('Are you sure? This can NOT be undone!')) {
try {
await axios.delete('api/profile');
dispatch({ type: CLEAR_PROFILE });
dispatch({ type: ACCOUNT_DELETED });
dispatch(setAlert('Your account has been permanently deleted'));
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
}
};
③ auth reducerにACCOUNT_DELETED追加。
//...
LOGIN_FAIL,
LOGOUT,
+ ACCOUNT_DELETED,
} from '../actions/types';
//...
case LOGIN_FAIL:
case LOGOUT:
+ case ACCOUNT_DELETED:
localStorage.removeItem('token');
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
};
default:
return state;
}
}
④ アカウント削除ボタンを設置
//...
import DashboardActions from './DashboardActions';
+ import { getCurrentProfile, deleteAccount } from '../../actions/profile';
const Dashboard = ({
getCurrentProfile,
+ deleteAccount,
auth: { user },
profile: { profile, loading },
}) => {
useEffect(() => {
getCurrentProfile();
}, [getCurrentProfile]);
//...
<Fragment>
<DashboardActions />
+ <div className="my-2">
+ <button className="btn-danger" onClick={() => deleteAccount()}>
+ <i className="fas fa-user-minus"></i> Delete My Account
+ </button>
+ </div>
</Fragment>
//...
Dashboard.propTypes = {
getCurrentProfile: PropTypes.func.isRequired,
+ deleteAccount: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
profile: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
profile: state.profile,
});
+ export default connect(mapStateToProps, { getCurrentProfile, deleteAccount })(
Dashboard
);
⑤ ProfileAPIにユーザーアカウントを削除したら、そのユーザーのPostsも全て削除する設定
//...
const Profile = require('../../models/Profile');
const User = require('../../models/User');
+ const Post = require('../../models/Post');
//...
// @route DELETE api/profile
// @desc Delete profile, user & posts
// @access Private
router.delete('/', auth, async (req, res) => {
try {
- // @todo - remove users posts
+ // Remove user posts
+ await Post.deleteMany({ user: req.user.id });
// Remove profile
await Profile.findOneAndRemove({ user: req.user.id });
// Remove user
await User.findOneAndRemove({ _id: req.user.id });
res.json({ msg: 'User deleted' });
} catch (err) {
//...