1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者】React で掲示板サイトを作成してみた。

1
Posted at

はじめに

プログラムを学び始めたのでアウトプットとして書いてみようと思いました。
TechTrainのReact掲示板課題の指示通りに作成してみました。

使用技術

・React
・Javascript

こだわったポイント

1. スレッドを開いたときロード中に「ロード中です」と表示されるようにする。

2. ブラウザバックをしたときに1ページ目に戻らないようにした。

3. 左クリックでアクセスしても動作不良を起こさない。

メインページ

上部

スクリーンショット 2026-03-21 142703.png

下部

スクリーンショット 2026-03-21 142833.png

スレッド作成画面

スクリーンショット 2026-03-21 144507.png

スレッド内

スクリーンショット 2026-03-23 173350.png

コード

App.jsx

import './App.css'
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './Home';
import ThreadsNew from './ThreadsNew';
import ThreadPostList from './ThreadPostList';


function App() {

  return (
    <div>
      <BrowserRouter>
        <Routes>
          <Route path='/' element={<Home />} />
          <Route path='/threads/new' element={<ThreadsNew />} />
          <Route path='/threads/:thread_id' element={<ThreadPostList />} />
        </Routes>
      </BrowserRouter>
    </div>
  )

}
export default App;
main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
Home.jsx
import Header from "./Header";
import Threads from "./Threads";


const Home = () => {

    return (
        <div>
            <Header />
            <Threads />
        </div>
    )

}
export default Home;
Header.jsx
import { Link } from 'react-router-dom';
import './Header.css'
// @ts-check

const Header = () => {
    return (
        <div className='Header'>
            <p className='pageTitle'>掲示板サイト</p>
            <Link to='/threads/new'>
                <button className='create'>スレッドを作成作成</button>
            </Link>
        </div>
    );
}

export default Header;
ThreadPostList.jsx
import { useEffect, useState } from "react"
import { useLocation, useParams } from "react-router-dom";
import './ThreadPostList.css';


const ThreadPostList = () => {

    //ロード確認用
    const [load, setLoad] = useState(true);

    //スレッド投稿用
    const [thread, setThread] = useState("");

    //スレッド内投稿一覧用
    const [threadPost, setThreadPost] = useState([]);
    const { thread_id } = useParams();
    const location = useLocation();
    const searchParam = new URLSearchParams(location.search);
    const threadTitle = searchParam.get("title");


    //スレッド取得
    const getThread = async () => {
        setLoad(true);
        try {
            const response = await fetch(`https://railway.bulletinboard.techtrain.dev/threads/${thread_id}/posts`);
            const result = await response.json();
            setThreadPost(result.posts);
        } catch (error) {
            console.log('エラーが出て');
        } finally {
            setLoad(false);
        };

    };

    useEffect(() => {
        getThread();
    }, [thread_id]);


    //投稿用
    const PostThread = async (e) => {
        e.preventDefault();
        await fetch(`https://railway.bulletinboard.techtrain.dev/threads/${thread_id}/posts`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                post: thread,
            }),
        });
        setThread("");
        getThread();
    };

    return (
        <div className="threadPostList">
            <div className="thread">
                <p className="TitleInThread">{threadTitle}</p>

                {load ? (
                    <p>ロード中です</p>
                ) : (
                    threadPost.length === 0 ? (
                        <p className="noPost">投稿がありません。❗</p>
                    ) : (
                        threadPost.map((item) => (
                            <div key={item.id} className="textThread">
                                <p>{item.post}</p>
                            </div >
                        ))
                    )
                )}
            </div>
            <div className="postForm">
                <form onSubmit={PostThread}>
                    <textarea type="text" value={thread} onChange={(e) => setThread(e.target.value)} />
                    <button type="submit"
                        disabled={thread.trim() === ""}>
                        投稿する
                    </button>
                </form>
            </div>
        </div>
    );

}

export default ThreadPostList;

スレッド内にアクセス時の待ち時間に「ロード中」と表示されるようにsetLoadを作成した。

    const location = useLocation();
    const searchParam = new URLSearchParams(location.search);
    const threadTitle = searchParam.get("title");

このコードによりURLからタイトルを取得してきている。

Threads.jsx
import { useEffect, useState } from "react";
import './Threads.css'
import { Link, useSearchParams } from "react-router-dom";
// @ts-check


const Threads = () => {

    const [threads, setThreads] = useState([]);
    //URLのoffset取得用
    const [searchParams, setSearchParams] = useSearchParams();
    const offset = Number(searchParams.get("offset")) || 0;
    //ページ数
    const count = (offset / 10) + 1;

    useEffect(() => {
        const getThreads = async () => {
            const url = `https://railway.bulletinboard.techtrain.dev/threads?offset=${offset}`;
            const response = await fetch(url);
            const result = await response.json();

            setThreads(result);
        };

        getThreads();
        window.scrollTo(0, 0);

    }, [offset]);



    const clickOffset = (newOffset) => {
        setSearchParams({ offset: newOffset });
    }


    return (
        <div className="threads">
            <button onClick={() => clickOffset(0)} className="latestButton">
                最新ページへ
            </button>
            {
                threads.map((thread) => (
                    <Link to={`/threads/${thread.id}?title=${thread.title}`} key={thread.id} className='title'
                    >
                        {thread.title}
                    </Link>
                ))
            }
            <div className="pagesButton">
                <button onClick={() => {
                    clickOffset(Math.max(0, offset - 10));
                }} disabled={offset === 0}>
                    前に
                </button>
                <button onClick={() => {
                    clickOffset(offset + 10);
                }}>
                    次に
                </button>
            </div>
            <p className="pages">{count} ページ</p>
        </div >
    );
}

export default Threads;
    const [searchParams, setSearchParams] = useSearchParams();
    const offset = Number(searchParams.get("offset")) || 0;
    //ページ数
    const count = (offset / 10) + 1;

URLからoffsetを取得してページを表示している。
パラメータからページを取得することで、ブラウザバックなどをしたときにページが初めからにならないようにしている。

ThreadsNew.jsx
import { useState } from "react";
import './ThreadsNew.css';
import { useNavigate } from "react-router-dom";


const ThreadsNew = () => {


    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");


    const navigate = useNavigate();

    const postThread = async (e) => {
        e.preventDefault();

        const threadResponse = await fetch('https://railway.bulletinboard.techtrain.dev/threads', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                title: title,
            }),
        });

        const threadData = await threadResponse.json();
        const threadId = await threadData.id;

        await fetch(`https://railway.bulletinboard.techtrain.dev/threads/${threadId}/posts`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                post: content,
            }),
        });
        alert('スレッドを作成しました')
        navigate('/')
    }




    return (
        <div>
            <h1>新規スレッドを作成</h1>
            <div className="thread">
                <form onSubmit={postThread}>
                    <label className="thread-title">タイトル:
                        <input className="title-info" type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
                    </label>
                    <br />
                    <label htmlFor="">内容
                        <textarea name="Content" value={content} rows={5} onChange={(e) => setContent(e.target.value)}></textarea>
                    </label>
                    <br />
                    <button type="submit"
                        disabled={title.trim() === "" || content.trim() === ""}>登録する</button>
                </form>
            </div>
        </div>
    );
}

export default ThreadsNew;
 <button type="submit"
                        disabled={title.trim() === "" || content.trim() === ""}>登録する</button>

ここで何も書いていないときに登録するボタンを押せないようにしている。
また、.trim() によって空白のみの投稿も防いでいる。

学んだこと

APIを使用して投稿するときのコードの書き方について知ることができた。
URLパラメータを取得する際にuseSearchParamsを使用する必要がある。
Reactでのweb製作方法。
Qiitaの使い方。

今後の課題

見た目がシンプルすぎるのでもっと良いデザインやCSSについて学ぶ必要がある。
Qiitaの書き方をもっと上手に書けるようになりたい。
コードの書き方やjsxの正しい分け方などを今後学んでいきたい。

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?