神戸学院大学 経営学部 林坂ゼミ

React 入門トップページ

« 戻る 次へ »

React 入門

JavaScript コメント掲示板アプリの開発

プログラムの全体像

前のページまでで API からコメントを取得して一覧表示・個別表示したり,編集,削除ができる React のフロントエンドアプリケーションが完成しました.これまで断片的に示していたコードもあるのでここではプロジェクトのほぼ全体のソースコードを示しておきます.なお,同じような機能を実装する方法は他にも様々考えられるので,一つの例として捉えてください.

public/index.html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React Comment アプリケーション</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

src/index.jsimport React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import routes from './routers/routes';
import './index.css';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={routes} />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

src/index.cssbody {
  margin: 0;
  color: #333;
}

.container {
  margin: 20px 20px;
}

h1 {
  margin: 20px 0;
}

article {
  margin: 20px 0;
  border: 1px solid #ccc;
  border-radius: 8px;
  padding: 16px;
}

article .title {
  font-size: 1.2em;
  font-weight: bold;
}

article .body {
  font-size: 0.9em;
  line-height: 1.5em;
}

.textInput {
  width: 100%;
  padding: 4px;
  margin: 0 0 8px;
  box-sizing: border-box;
}

.deleteButton {
  text-align: right;
}

.commentsHeader {
  font-size: 0.8em;
}

.infinityScroll {
  text-align: center;
  margin-bottom: 20px;
}

src/routers/routes.jsimport { Route, createBrowserRouter, createRoutesFromElements } from 'react-router-dom';
import CommentListPage from '../components/CommentListPage';
import CommentShowPage from '../components/CommentShowPage';
import CommentEditPage from '../components/CommentEditPage';
import NotFoundPage from '../components/NotFoundPage';

const routes = createBrowserRouter(
  createRoutesFromElements(
    <Route>
      <Route path="/" element={<CommentListPage />} />
      <Route path="/show/:commentId" element={<CommentShowPage />} />
      <Route path="/edit/:commentId" element={<CommentEditPage />} />
      <Route path="*" element={<NotFoundPage />} />
    </Route>
  )
);

export default routes;

src/components/CommentListPage.jsimport React from 'react'
import { useState, useEffect } from 'react';
import axios from 'axios';
import Comment from './Comment';
import CreateForm from './CreateForm';

const CommentListPage = () => {

  const [results, setResults] = useState({
    "count":10,
    "next":"http://127.0.0.1:8000/comments/?page=2",
    "previous":null,
    "results":[
      {
        "id":9999,
        "title":"ダミー",
        "body":"ダミーの本文",
        "updated_at":"2023-11-21T11:20:00"
      },
      {
        "id":10000,
        "title":"ダミーのタイトル",
        "body":"ダミーです",
        "updated_at":"2023-11-21T11:10:00"
      }
    ]
  });

  // const [isPrevButtonDisabled, setPrevButtonDisabled] = useState(true);
  // const [isNextButtonDisabled, setNextButtonDisabled] = useState(false);

  const handleCommentsListChange = (url) => {
    axios.get(url)
        .then(res => setResults(res.data))
        .catch(err => console.log(err.message));
  }

  useEffect(() => {
    const url = "http://127.0.0.1:8000/comments/";
    axios.get(url)
        .then(res => setResults(res.data))
        .catch(err => console.log(err.message));
  }, []);

  const handleDeleteButtonClick = (commentId) => {
    if (!window.confirm("削除しますか?")) {
      return;
    }
    const newComments = [...results.results].filter((comment) => {
      return comment.id !== commentId;
    });
    const newResults = {
      "count": results.count - 1, // コメント数は1減らす
      "previous": results.previous,
      "next" : results.next,
      "results": newComments
    };
    setResults(newResults);
    const url = `http://127.0.0.1:8000/comments/${commentId}/`;
    axios
      .delete(url)
      .then(res => {
        if (res.status === 204) {
          handleCommentsListChange('http://127.0.0.1:8000/comments/');
        } else {
          console.log("Delete失敗");
        }
      })
      .catch(err => console.log(err.message));
  };

  const commentItems = results.results.map((comment) => {
    return (
      <Comment
        key={comment.id}
        comment={comment}
        onDelete={handleDeleteButtonClick}
      />
    )
  });

  const handelCreateFormSubmit = async (title, body) => {
    const newComments = [...results.results];  // スプレッド構文でコメントの配列だけを取り出す
    newComments.unshift({   // unshift で先頭に追加,push では最後に追加
      id: Date.now(),
      title: title,
      body: body,
      updated_at: "2024-02-25T15:30:00",
    });
    const newResults = {
      "count": results.count + 1, // コメント数は1増やす
      "previous": results.previous,
      "next" : results.next,
      "results": newComments,   // これがコメントの配列
    };
    setResults(newResults); // 画面を更新

    // API に POST でリクエストする
    let result = false;
    const url = "http://127.0.0.1:8000/comments/";
    await axios
      .post(url, {
        title: title,
        body: body,
      })
      .then(res => {
        // console.log(res.data);
        handleCommentsListChange(url);
        result = true;
      })
      .catch(err => {
        setResults(results); // 画面を元に戻す(元の配列で更新)
        alert("投稿エラー!未入力または文字数超過です.");
      });
    return result;
  }

  // const handlePrevPageButton = () => {
  //   if (results.previous === null) {
  //     return;
  //   }
  //   axios.get(results.previous)
  //     .then(res => setResults(res.data))
  //     .catch(err => console.log(err.message));
  // };

  // const handleNextPageButton = () => {
  //   if (results.next === null) {
  //     return;
  //   }
  //   axios.get(results.next)
  //     .then(res => setResults(res.data))
  //     .catch(err => console.log(err.message));
  // };

  // useEffect(() => {
  //   setPrevButtonDisabled(results.previous === null);
  //   setNextButtonDisabled(results.next === null);
  // }, [results]);

  const handleInfinityScrollButton = () => {
    axios.get(results.next)
      .then(res => {
        const prevComments = [...results.results]; // 表示済のコメント配列
        const moreComments = [...res.data.results]; // 新たに読み込んだコメント配列
        const newResults = {
          "count": res.data.count,
          "previous": null,       // 前のページは null のまま
          "next" : res.data.next, // 次のURLを更新
          "results": prevComments.concat(moreComments),   // 配列を連結
        };
        setResults(newResults); // 画面を更新

      })
      .catch(err => console.log(err.message));
  }

  return (
    <div className="container">
      <h1>コメント一覧</h1>

      <div className="commentsHeader">
        <div>コメント総数:{results.count}</div>
        {/* <div>
          <button
            onClick={handlePrevPageButton}
            disabled={isPrevButtonDisabled}
          >前のページ</button>
          <button
            onClick={handleNextPageButton}
            disabled={isNextButtonDisabled}
          >次のページ</button>
        </div> */}
      </div>

      {commentItems}

      <div className="infinityScroll">
        <button
         onClick={handleInfinityScrollButton}
        >
          次を読み込む
        </button>
      </div>

      <CreateForm
        onSubmit={handelCreateFormSubmit}
      />
    </div>
  )
}

export default CommentListPage

src/components/Comment.jsimport React from 'react'
import { Link } from 'react-router-dom';

const Comment = (props) => {

  const handleClickDeleteButton = () => {
    props.onDelete(props.comment.id);
  };

  return (
    <article>
      <div className="title">
        <Link to={`/show/${props.comment.id}/`}>
          {props.comment.title}
        </Link>
      </div>
      <div className="body">
        {props.comment.body}
      </div>
      <div className="deleteButton">
        <button
          onClick={handleClickDeleteButton}
        >
          削除
        </button>
      </div>
    </article>
  )
}

export default Comment

src/components/CreateForm.jsimport React from 'react'
import { useState, useRef } from 'react';

const CreateForm = (props) => {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const inputTitleRef = useRef(null);

  const handleTitleChange = (event) => {
    setTitle(event.currentTarget.value);
  };

  const handleBodyChange = (event) => {
    setBody(event.currentTarget.value);
  };

  const handleSubmit = async (event) => {
    event.preventDefault(); // ページ遷移しないように
    const result = await props.onSubmit(title, body);
    if (result) {
      setTitle(''); // 入力フォームを空に戻す
      setBody('');  // 入力フォームを空に戻す
    }
    inputTitleRef.current.focus();  // フォーカスを当てる
  }

  return (
    <>
      <hr />
      <article>
        <h2>コメントの新規投稿</h2>
        <form onSubmit={handleSubmit}>
          <div>
            <label>
              タイトル:
              <input
                type="text"
                className="textInput"
                value={title}
                onChange={handleTitleChange}
                ref={inputTitleRef}
              />
            </label>
          </div>
          <div>
            <label>
              本文:
              <input
                type="text"
                className="textInput"
                value={body}
                onChange={handleBodyChange}
              />
            </label>
          </div>
          <div>
            <button>コメントの追加</button>
          </div>
        </form>
      </article>
    </>
  )
}

export default CreateForm

src/components/CommentShowPage.jsimport React from 'react'
import { useState, useEffect } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'; // Link を追加
import axios from 'axios';

const CommentShowPage = () => {
  const params = useParams();

  const [data, setData] = useState(null);
  const url = `http://127.0.0.1:8000/comments/${params.commentId}/`;

  useEffect(() => {
    axios.get(url)
      .then(res => setData(res.data))
      .catch(err => console.log(err.message));
  }, []);

  const navigate = useNavigate();
  const handleBackClick = () => {
    navigate('/');
  };

  if (data === null) {
    return (
      <div className="container">
        <h1>コメント</h1>
        <article>
        </article>
      </div>
    );
  }

  return (
    <div className="container">
      <h1>コメント</h1>
      <article>
        <div>ID: {data.id}</div>
        <div className="title">Title: {data.title}</div>
        <div className="body">Body: {data.body}</div>
        <div>最終更新: {data.updated_at}</div>
        <div>
          <Link to={`/edit/${data.id}/`}>
            コメント情報の編集
          </Link>
        </div>
      </article>
      <button
        onClick={handleBackClick}
      >コメント一覧へ戻る</button>
    </div>
  )
}

export default CommentShowPage

src/componenst/CommentEditPage.jsimport React from 'react'
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
import EditForm from './EditForm';

const CommentEditPage = () => {
  const params = useParams();
  const [data, setData] = useState(null);
  const url = `http://127.0.0.1:8000/comments/${params.commentId}/`;

  console.log(url);

  useEffect(() => {
    axios.get(url)
      .then(res => setData(res.data))
      .catch(err => console.log(err.message));
  }, []);

  const navigate = useNavigate();
  const handleEditFormSubmit = (url, title, body) => {
    axios
      .put(url, {
        title: title,
        body: body,
      })
      .then(res => {
        if (res.status === 200) {
          console.log("PUT成功");
          navigate('/');
        } else {
          console.log("PUT失敗");
        }
      })
      .catch(err => {
        console.log(err.message);
        alert("投稿エラー!未入力または文字数超過です.");
      });
  };

  if (data === null) {
    return (
      <div className="container">
        <h1>コメントの編集</h1>
        <article>
          <p></p>
        </article>
      </div>
    );
  }

  return (
    <div className="container">
      <h1>コメントの編集</h1>
      <EditForm
        commentId={data.id}
        title={data.title}
        body={data.body}
        url={url}
        onSubmit={handleEditFormSubmit}
      />
    </div>
  )
}

export default CommentEditPage
src/components/EditForm.jsimport React from 'react'
import { useState } from 'react';

const EditForm = (props) => {
  const [title, setTitle] = useState(props.title);
  const [body, setBody] = useState(props.body);

  const handleTitleChange = (event) => {
    setTitle(event.currentTarget.value);
  };

  const handleBodyChange = (event) => {
    setBody(event.currentTarget.value);
  };

  const formSubmit = (event) => {
    event.preventDefault();
    props.onSubmit(props.url, title, body);
    console.log("EditForm : formSubmit");
  }

  return (
    <>
      <form onSubmit={formSubmit}>
        <div>
          <label>
            タイトル:
            <input
              type="text"
              className="textInput"
              value={title}
              onChange={handleTitleChange}
            />
          </label>
        </div>
        <div>
          <label>
            本文:
            <input
              type="text"
              className="textInput"
              value={body}
              onChange={handleBodyChange}
            />
          </label>
        </div>
        <div>
          <button>コメントの更新</button>
        </div>
      </form>
    </>
  )
}

export default EditForm

src/components/NotFoundPage.jsimport React from 'react'

const NotFoundPage = () => {
  return (
    <div className="container">
      <h1>404 Not Found</h1>
      <p>指定したページは見つかりませんでした</p>
    </div>
  )
}

export default NotFoundPage

目次に戻る