LoginSignup
0
0

More than 3 years have passed since last update.

[MERN⑧] Dashboard & Profile Management

Last updated at Posted at 2020-12-02

~~~~~~~~~~ (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コンポーネントファイル生成

dashboard.js
//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コンポ)に遷移。

components/routing/PrivateRoute.js
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);
components/dashboard/Dashboard.js
//...
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追加

actions/types.js
+ export const GET_PROFILE = 'GET_PROFILE';
+ export const PROFILE_ERROR = 'PROFILE_ERROR';

② ProfileReducer生成

reducers/profile.js
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アクション

actions/profile.js
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コンポーネント

components/dashboard/Dashboard.js
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リンクを設置。

components/layout/Navbar.js
//...
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に遷移するように設定。

components/layout/Landing.js
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はインターネットから持ってくる。

components/layout/Spinner.js
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リデューサに設置。

actions/types.js
export const GET_PROFILE = 'GET_PROFILE';
export const PROFILE_ERROR = 'PROFILE_ERROR';
+ export const CLEAR_PROFILE = 'CLEAR_PROFILE';
actions/auth.js
//...
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 });
};
reducers/profile.js
+ 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設定されてない場合の設定。

components/dashboard/Dashboard.js
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で囲む -> classclassNameに変更
(5) ソーシャルメディアをトグル設定 'toggleSocialInputs'
(6)イベントハンドラーonChangeを定義(フォームの選択や入力欄に変更があった場合に指定の処理を行わせる)
 -> HooksのuseStateを更新するためのset更新関数を設定。(今回はsetFormData)
 -> inputタグに、value={プロパティ名}onChange={(e) => onChange(e)}を追加

components/profile-forms/CreateProifile.js
(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ルートを設置

src/App.js
//...
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とセットで使用。次の②参照。

actions/profile.js
//...
// 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)を使う。

components/profile-forms/CreateProfile.js
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'

components/dashboard/DashboardActions.js
//以下全て追加
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;
components/dashboard/Dashboard.js
//...
+ 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にしてやる。

components/profile-forms/EditProfile.js
(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のルート設定。

src/App.js
//...
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追加

actions/types.js
//...
+ export const DELETE_ACCOUNT = 'DELETE_ACCOUNT';

② profileアクションにdeleteAccount関数追加。

-> 安易にアカウント削除できないように確認ウィンドウの設置。

actions/profile.js
//...
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追加。

reducers/auth.js
//...
  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;
  }
}

④ アカウント削除ボタンを設置

components/dashboard/Dashboard.js
//...
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も全て削除する設定

routes/api/profile.js
//...
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) {
//...
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0