~~~~~~~~~~ (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. Auth Reducer & Register Action
① TYPEを追加
export const SET_ALERT = 'SET_ALERT';
export const REMOVE_ALERT = 'REMOVE_ALERT';
+ export const REGISTER_SUCCESS = 'REGISTER_SUCCESS';
+ export const REGISTER_FAIL = 'REGISTER_FAIL';
② Auth Reducer
~~~ 詳細説明 ~~~
(1) localStorageは、cookieのようにデータをブラウザで永続的に保存できる仕組み。デフォルトでtokenを取得する、認証成功したらlocalStorageにtokenデータを保存する。認証失敗したらtokenを削除。
(2) 認証したら、null->trueに変わる。他でも認証判定でisAuthenticated
を使用する。
(3) 認証試みる直前はLoadingする設定。認証成功または失敗したらLoading終わる->false。
(4) 認証前はname,email,avatarはnullの状態にする。
(5) Auth
import { REGISTER_SUCCESS, REGISTER_FAIL } from '../actions/types';
const initialState = {
(1) token: localStorage.getItem('token'),
(2) isAuthenticated: null,
(3) loading: true,
(4) user: null,
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case REGISTER_SUCCESS:
(1) localStorage.setItem('token', payload.token);
return {
...state,
...payload,
(2) isAuthenticated: true,
(3) loading: false,
};
case REGISTER_FAIL:
(1) localStorage.removeItem('token');
return {
...state,
(1) token: null,
(2) isAuthenticated: false,
(3) loading: false,
};
default:
return state;
}
}
③ reducers/index.jsにauthを追加
import { combineReducers } from 'redux';
import alert from './alert';
+ import auth from './auth';
export default combineReducers({
alert,
+ auth,
});
④ Authアクション
~~~ 詳細説明 ~~~
(1)認証成功-> name,email,passowrdをJSON形式でAPIにPOST -> REGISTER_SUCCESSを発火。
*stringify=JSON形式に変換する
(2)payload:res.data
これはtoken
(3)認証失敗->setAlertアクション->REGITER_FAILを発火
*payloadは不要(auth reducerのREGITER_FAILにpayloadはない。)
*array.forEach(element => {}); (arrayはerrors、elementは今回対象のエラー(要は認証失敗エラー))
import axios from 'axios';
import { setAlert } from './alert';
import { REGISTER_SUCCESS, REGISTER_FAIL } from './types';
// Register User
export const register = ({ name, email, password }) => async (dispatch) => {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const body = JSON.stringify({ name, email, password });
(1) try {
const res = await axios.post('/api/users', body, config);
dispatch({
type: REGISTER_SUCCESS,
(2) payload: res.data, //token
});
} catch (err) {
const errors = err.response.data.errors;
(3) if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
}
dispatch({
type: REGISTER_FAIL,
});
}
};
④ Registerコンポーネントにregister関数を設置。
...
import { setAlert } from '../../actions/alert';
+ import { register } from '../../actions/auth';
import PropTypes from 'prop-types';
+ const Register = ({ setAlert, register }) => {
...
const onSubmit = async (e) => {
e.preventDefault();
if (password !== password2) {
setAlert('Passwords do not match', 'danger');
} else {
+ register({ name, email, password });
}
};
return (
...
Register.propTypes = {
setAlert: PropTypes.func.isRequired,
+ register: PropTypes.func.isRequired,
};
+ export default connect(null, { setAlert, register })(Register);
2. Load User & Set Auth Token
① ユーティリティフォルダ生成 -> setAuthTokenファイルを生成。
import axios from 'axios';
const setAuthToken = (token) => {
if (token) {
axios.defaults.headers.common['x-auth-token'] = token;
} else {
delete axios.defaults.headers.common['x-auth-token'];
}
};
export default setAuthToken;
② TYPEを追加。
...
+ export const USER_LOADED = 'USER_LOADED';
+ export const AUTH_ERROR = 'AUTH_ERROR';
③ Load Userアクションを追加
import axios from 'axios';
import { setAlert } from './alert';
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
+ USER_LOADED,
+ AUTH_ERROR,
} from './types';
+ import setAuthToken from '../utils/setAuthToken';
以下全て追加。
// Load User
export const loadUser = () => async (dispatch) => {
if (localStorage.token) {
try {
setAuthToken(localStorage.token);
const res = await axios.get('/api/auth');
dispatch({
type: USER_LOADED,
payload: res.data,
});
} catch (err) {
dispatch({
type: AUTH_ERROR,
});
}
} else {
dispatch({
type: AUTH_ERROR,
});
}
};
// Register User
...
④ Auth ReducerにUSER_LOADEDとAUTH_ERRORを設置。
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
+ USER_LOADED,
+ AUTH_ERROR,
} from '../actions/types';
const initialState = {
token: localStorage.getItem('token'),
isAuthenticated: null,
loading: true,
user: null,
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
+ case USER_LOADED:
+ return {
+ ...state,
+ isAuthenticated: true,
+ loading: false,
+ user: payload, //name,email,avatar etc.
+ };
case REGISTER_SUCCESS:
localStorage.setItem('token', payload.token);
return {
...state,
...payload,
isAuthenticated: true,
loading: false,
};
case REGISTER_FAIL:
+ case AUTH_ERROR:
localStorage.removeItem('token');
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
};
default:
return state;
}
}
⑤ ユーザーがサイトをロードした時にいつも行う動作
ユーザーがサイトをロードした時...
(1) Tokenの有無確認。あればlocalStorageにセット。
(2) loadUser
をマウント(userEffect)する。
(3) 「空の配列 ([]) を渡した場合、副作用内では props と state の値は常にその初期値のままになる。」
https://ja.reactjs.org/docs/hooks-effect.html
+ import React, { Fragment, useEffect } from 'react';
...
//Redux
import { Provider } from 'react-redux';
import store from './store';
+ import { loadUser } from './actions/auth';
+ import setAuthToken from './utils/setAuthToken';
import './App.css';
(2) useEffect(() => {
(1) setAuthToken(localStorage.token);
(2) store.dispatch(loadUser());
(3) }, []);
+ return (
<Provider store={store}>
...
</Provider>
);
+ };
export default App;
⑥ Authアクションのregister関数が成功した場合に、loadUser
を発火する。
...
// Register User
export const register = ({ name, email, password }) =>
...
try {
const res = await axios.post('/api/users', body, config);
dispatch({
type: REGISTER_SUCCESS,
payload: res.data, //token
});
+ dispatch(loadUser());
} catch (err) {
...
3. User Login
① アクションにType追加。
...
+ export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
+ export const LOGIN_FAIL = 'LOGIN_FAIL';
② register
関数をコピペして、login
関数を記述。
...
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
+ LOGIN_SUCCESS,
+ LOGIN_FAIL,
} from './types';
...
//以下Register関数をコピペ
// Login User
+ export const login = ({ email, password }) => async (dispatch) => {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
+ const body = JSON.stringify({ email, password });
try {
const res = await axios.post('/api/auth', body, config);
dispatch({
+ type: LOGIN_SUCCESS,
payload: res.data, //token
});
dispatch(loadUser());
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
}
dispatch({
+ type: LOGIN_FAIL,
});
}
};
③ AuthReducerにLOGIN_SUCCESSと_FAILを追加。
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
+ LOGIN_SUCCESS,
+ LOGIN_FAIL,
} from '../actions/types';
...
case REGISTER_SUCCESS:
+ case LOGIN_SUCCESS:
localStorage.setItem('token', payload.token);
return {
...state,
...payload,
isAuthenticated: true,
loading: false,
};
case REGISTER_FAIL:
case AUTH_ERROR:
+ case LOGIN_FAIL:
localStorage.removeItem('token');
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
};
default:
return state;
}
}
④ 認証成功時のRedirect
以下、components/auth/Register.jsも同様に追加
import React, { Fragment, useState } from 'react';
+ import { Link, Redirect } from 'react-router-dom';
...
+const Login = ({ login, isAuthenticated }) => {
...
+ // Redirect if logged in
+ if (isAuthenticated) {
+ return <Redirect to="/dashboard" />;
+ }
return (
...
Login.propTypes = {
login: PropTypes.func.isRequired,
+ isAuthenticated: PropTypes.bool,
};
+ const mapStateToProps = (state) => ({
+ isAuthenticated: state.auth.isAuthenticated,
+ });
+ export default connect(mapStateToProps, { login })(Login);
4. Logout & Navbar Links
① TYPEアクションに追加。
+ export const LOGOUT = 'LOGOUT';
② アクションとリデューサーにLOGOUTを追加。
//...
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
+ LOGOUT,
} from './types';
//...
+ // Logout / Clear Profile
+ export const logout = () => (dispatch) => {
+ dispatch({ type: LOGOUT });
+ };
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
+ LOGOUT,
} from '../actions/types';
//...
case REGISTER_FAIL:
case AUTH_ERROR:
case LOGIN_FAIL:
+ case LOGOUT:
localStorage.removeItem('token');
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
};
default:
return state;
}
}
③ Navbarの認証
(1) href="#!"
<-シャープとエクスクラメーションマークの順番に注意。
(2) Loadingしていない
場合、認証済ならauthLinks
、未認証ならguestLinks
・{!loading && ''}
の説明-> 「if not loading, then do ''」
・{XXX ? YYY : ZZZ}
の説明-> 「if XXXX, YYY else ZZZ」
(!loading
の疑問とその解決)
initialState(reducers/auth.js)がloading: true
なので、authLinks
とguestLinks
がNavbarに出てこないと思いきや、App.jsでloadUser
を発火(USER_LOADED or AUTH_ERROR)しているので、いづれにしてもloading:false
となる。
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { logout } from '../../actions/auth';
const Navbar = ({ auth: { isAuthenticated, loading }, logout }) => {
const authLinks = (
<ul>
<li>
(1) <a onClick={logout} href="#!">
<i className="fas fa-sign-out-alt" />{' '}
<span className="hide-sm">Logout</span>
</a>
</li>
</ul>
);
const guestLinks = (
<ul>
<li>
<a href="#!">Developers</a>
</li>
<li>
<Link to="/register">Register</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
</ul>
);
return (
<nav className="navbar bg-dark">
<h1>
<Link to="/">
<i className="fas fa-code" /> Refnote
</Link>
</h1>
(2) {!loading && (
<Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
)}
</nav>
);
};
Navbar.propTypes = {
logout: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
});
export default connect(mapStateToProps, { logout })(Navbar);