はじめに
本日は、ユーザー登録、ログイン、ログアウトの機能を持つ認証アプリの基本的な構成を初心者向けに解説します。
Reactでは、ユーザーの認証状態を管理するためにReact ContextやReduxなどの状態管理ライブラリを使用するのが一般的です。
初心者には、Reactの状態管理やファイル構造、データフローに関する概念を理解することは初めての課題かもしれません。
そのため、React ContextやReduxのような状態管理ライブラリを使うことが一般的ですが、これらは初心者にとっては少し複雑に感じるかもしれません。
React Contextフックを使った状態管理では、通常よりもファイル構造が複雑になることがあります。
また、Reduxは専用の状態管理ライブラリであり、それ自体を学習する必要があります。
これらのライブラリの使用は、初めてのReactプロジェクトにとっては必須ではありません。
初心者の方にとっては、Reactの基本的な状態管理手法であるStateとPropsを使用することでも十分な場合があります。
ただし、プロジェクトが複雑化したり、大規模なアプリケーションを開発する場合は、状態管理ライブラリの使用を検討することが重要です。
その際は、React ContextやReduxのドキュメントやチュートリアルを参考にしながら、少しずつ学習を進めると良いでしょう。
状態管理はReact開発の重要な要素ですが、最初は少し複雑に感じるかもしれません。
しかし、実際のプロジェクトで経験を積みながら学習していくことで、より深い理解とスキルの向上が期待できます。
そこで、この記事では初心者の方にもわかりやすい方法で、シンプルなユーザー認証アプリを例に解説していきます。
React ContextやReduxライブラリを使わずに状態を管理する方法を紹介します。
React.jsを使用してシンプルなユーザー認証アプリを作成するためには、いくつかのステップが必要です。
本日の具体的な手順としては、以下のような流れで進めます。
まず、React Routerを使用してルーティングを設定します。
アプリケーションの親コンポーネント(Appコンポーネント)でBrowserRouterコンポーネントを使用し、ルーティング機能を有効にします。
Routesコンポーネント内に個々のルート(Route)を定義し、各Routeコンポーネントにはpath属性とelement属性を指定します。path属性は表示するパスを指定し、element属性にはそのパスに対応するコンポーネントを指定します。
次に、メニューナビゲーションを作成します。
nav要素内にリンクを配置し、ルーティングされた各ページへのナビゲーションを提供します。Linkコンポーネントを使用して各ページへのリンクを作成します。
各ページのコンポーネントとして、Home、Login、Register、Dashboardコンポーネントを作成します。それぞれのコンポーネントは、対応するルートのelement属性に指定されたときに表示されます。
また、テーマの設定も行います。
useStateフックを使用してテーマを管理し、それぞれのステート変数に基づくクラスを親要素に追加することでCSSスタイルを適用します。
具体的な子コンポーネントの手順としては、まず新規登録(Register)です。
ユーザーがフォームに入力した情報(email、password、nickname)を取得し、fetchData関数を使用してAPIエンドポイントに対してPOSTリクエストを送信し、ユーザー情報を登録します。
登録が成功した場合、トークン(nickname)をローカルストレージに保存します。新規登録ボタンがクリックされた時にこの関数を呼び出し、登録処理を行います。登録後はログインページにリダイレクトさせます。
次にログイン(Login)です。
ユーザーがフォームに入力したメールアドレスとパスワードを取得します。
入力された情報が有効な場合、トークン(例: "example_token")をローカルストレージに保存します。Login関数は、ログインボタンがクリックされた時に実行され、入力された情報の検証とトークンの保存を行います。
ログイン成功後、ダッシュボードページにリダイレクトさせます。
まず、localStorage.getItem('token') を使用してトークンの有無を確認します。
トークンが存在する場合、トークンに関連付けられたニックネームを表示します。
また、Logout関数はログアウトボタンがクリックされた時に実行され、トークンとニックネームをローカルストレージから削除します。
ログアウト後はログインページにリダイレクトさせます。
これらの手順によって、Reactの状態管理をシンプルに実装し、ユーザーの認証状態を管理することができます。
React ContextやReduxを使用しない場合でも、このような基本的な手法でユーザーの認証状態を管理することができます。
以下のような実装になります。
プロジェクト作成
まず、CRA(Create-React-App)でReactアプリケーションを作成し、必要なパッケージをインストールします。
ここでは、React Router v6を使用するために、react-router-domパッケージをインストールします。
npx create-react-app auth-app cd auth-app npm install react-router-dom
React Routerを使用して、ユーザー登録、ログイン、ログアウトなどの各機能に対応するルートを設定できます。
これにより、異なる画面や機能にユーザーをナビゲートすることができます。
React Routerを使用したページ遷移を行う場合、それはSPA(Single-Page Application)アプリケーションと見なすことができます。
次に、アプリケーションのディレクトリ構造を設定します。
一般的な構造として以下のようにします。
src ├── components │ ├── Home.js │ ├── Login.js │ ├── Register.js │ └── Dashboard.js ├── App.js └── index.js ├── App.css └── index.module.css
Home.jsは認証前のホームページを表示し、Login.jsはログイン画面、Register.jsは新規ユーザー登録画面、Dashboard.jsは認証後のダッシュボード画面を表します。
筆記時点でのReactおよびReact Routerのバージョンは以下です。
// package.json "dependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" },
スタイリング
まず、軽いスタイリングの変更を加えましょう。
以下は、App.cssファイルの修正内容です。
body { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } .app { display: flex; flex-direction: column; align-items: center; min-height: 100vh; /* 共通のスタイル */ } .app.light { background-color: #f1f1f1; /* ライトテーマのスタイル */ } .app.dark { background-color: #333; color: #fff; /* ダークテーマのスタイル */ } nav { display: flex; justify-content: center; align-items: center; width: 100%; padding: 20px; background-color: #f1f1f1; } nav ul { display: flex; justify-content: center; align-items: center; list-style: none; padding: 0; } nav li { margin-right: 10px; } nav li:last-child { margin-right: 0; }
これにより、CSS Modulesを使用してスタイルをインポートし、固有のスタイルクラスが生成されます。生成されたクラスは、子コンポーネントの要素に適用されます。
次に、index.cssファイルの名前をindex.module.cssに変更します。
nav { margin-bottom: 20px; } .box { border: 1px solid #ccc; padding: 20px; border-radius: 4px; width: 300px; margin: 0 auto; background-color: #f1f1f1; color: #333; } button { background-color: #4f46e5; font-weight: bold; border-radius: 9999px; padding: 1rem 2rem; color: #fff; margin-left: auto; display: flex; align-self: flex-end; }
index.jsファイルに移動し、インポートのパスを正しく変更しましょう。
// index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.module.css'; //変更 import App from './App'; import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <App /> </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();
CSSのスタイリングは、個別にカスタマイズしていただいても構いません。
注意点は、プロジェクトが小規模で単純な構造を持つ場合、CSSファイルを無理に分ける必要はありません。
ただ、コンポーネントごとにCSSファイルを分けることで、ブラウザは必要なスタイルのみをダウンロードし、レンダリングすることができます。不要なスタイルの読み込みを避けることで、パフォーマンスの向上が期待できます。
また、分割されたCSSファイルは、他のコンポーネントやモジュールで再利用することができます。共通のスタイルを別のファイルにまとめることで、重複を避け、効率的なスタイルの共有が可能になります。
そして、変更や修正が必要な場合、該当するCSSファイルを探しやすくなります。つまり可読性と保守性の向上に繋がります。
CSSファイルを分けるかどうかは、実際はプロジェクトの要件や開発チームの好みによって異なります。プロジェクトの規模や複雑さ、保守性やパフォーマンスの要求に応じて、適切なアプローチを選択していくようにしてください。
親コンポーネント
既存のApp.jsを以下のようにします。
import './App.css'; import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; import Home from './components/Home'; import Login from './components/Login'; import Register from './components/Register'; import Dashboard from './components/Dashboard'; import { useState } from 'react'; function App() { const [theme] = useState('dark'); // 例としてテーマを設定 return ( <Router> <div className={`app ${theme}`}> <nav> <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/login">Login</Link> </li> <li> <Link to="/register">Register</Link> </li> <li> <Link to="/dashboard">Dashboard</Link> </li> </ul> </nav> <Routes> <Route path="/" element={<Home />} /> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </div> </Router> ); } export default App;
ルートコンポーネントは、Reactアプリケーションのエントリーポイントであり、他のコンポーネントを含む、ルーティングや共通の状態管理などのアプリケーションのトップレベルの設定を行います。
App コンポーネント内の<Router>
コンポーネントは、React Routerパッケージを使用してルーティングを設定するために必要です。
<nav>
要素内の<Link>
コンポーネントは、アプリケーションのナビゲーションメニューを提供するためのリンクを表示します。
<Routes>
コンポーネントでは、<Route>
コンポーネントを含めるためのコンテナであり、各<Route>
コンポーネントは、特定のパスにマッチした場合に表示されるコンポーネントを定義します。
したがって、Appコンポーネントはアプリケーションのルートコンポーネントであり、他のコンポーネントを含んだ、ルーティングの設定を行います。
これにより、各パスに対応するコンポーネントが適切に表示されます。
次に、各コンポーネントを実装していきます。
子コンポーネントの実装
src
フォルダ直下に新しくcomponents
フォルダを作成しこの中に子コンポーネントファイルを格納していきます。
// components/Home.js const Home = () => { return ( <div><h1>Home</h1></div> ) } export default Home;
上記の、Homeコンポーネントは、アプリケーションのホームページまたはトップレベルのコンテンツを表示するためのコンポーネントです。
通常、アプリケーションの初期表示やルートパス ( / )に対応するページとして使用されます。
Homeコンポーネントは単純な見出し要素<h1>Home</h1>
を返すだけですが、実際のアプリケーションでは、ホームページのデザインやコンテンツを適切にレンダリングするために、より多くのコンテンツやコンポーネントを追加することが一般的です。
アプリケーションの要件に応じて、Homeコンポーネントを適切に拡張してホームページの表示や機能を実装してください。
ログイン状態の保持
// components/Login.js import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styles from '../index.module.css'; const Login = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const navigate = useNavigate(); const handleLogin = () => { // ログインの処理を実装する if (email.trim() !== '' && password.trim() !== '') { // ログイン成功の場合、トークンを保存するなどの処理を行う console.log("ログインに成功しました。"); // 例: トークンをローカルストレージに保存する localStorage.setItem('token', 'example_token'); // ダッシュボードページにリダイレクトする navigate('/dashboard'); } else { // ログイン失敗の場合の処理を行う(例: エラーメッセージの表示) console.log('ログインに失敗しました'); } }; return ( <div className={styles['box']}> <h2>Login</h2> <form> <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} /> <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} /> <button type="button" onClick={handleLogin}> Login </button> </form> </div> ); } export default Login;
上記のコードでは、useStateフックを使って、emailとpasswordの状態を管理しています。これらの状態は入力フィールドの値とバインドされます。
useNavigateフックを使って、React Routerのnavigate関数を取得します。これにより、ログイン成功後にダッシュボードページにリダイレクトすることができます。
handleLogin関数は、ログインボタンがクリックされたときに実行されます。
入力されたメールアドレスとパスワードが空でない場合に、ログイン成功とみなし、トークンをローカルストレージに保存し、ダッシュボードページにリダイレクトします。
空の場合はログイン失敗とみなされ、エラーメッセージが表示されます。
JSX内では、<input>
要素を使ってメールアドレスとパスワードの入力フィールドを表示し、onChangeイベントハンドラを使って入力値を更新します。
ログインボタンにはonClickイベントハンドラを設定し、クリックされたときにhandleLogin関数が呼び出されます。
ローカルストレージの確認方法は、Google chromeのデベロッパーツールを開いたら、上部のタブから「Application」(アプリケーション)を選択します。
左側のナビゲーションパネルで、「Storage」(ストレージ)をクリックします。
「Local Storage」(ローカルストレージ)を選択します。
右側のパネルには、現在のドメインのローカルストレージに保存されているキーと値の一覧が表示されます。
ここで、ローカルストレージに保存されているキーと値の詳細を確認したり、必要に応じて編集したりすることができます。
しかし、実際のアプリケーションでは、バックエンドの認証サービスやデータベースとの連携が必要であり、入力された情報を検証するなどの処理を行う必要があります。
また、ログインが成功した場合には、セッションやトークンの管理方法を適切に実装する必要があります。
この例では、単純化のためにローカルストレージにトークンを保存していますが、実際のアプリケーションではセキュリティ上の考慮が必要です。
ローカルストレージに関する詳細は当記事の以下を参考にしてみてください。
ユーザー情報の保存
以下では、関数コンポーネントとして実装する新規登録機能を持つコンポーネントです。
// components/Register.js import { useState, useEffect } from 'react'; import styles from '../index.module.css'; import { useNavigate } from 'react-router-dom'; const Register = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [nickname, setNickname] = useState(''); const navigate = useNavigate(); useEffect(() => { // マウント時の処理などを記述 fetchData(); }, []); const fetchData = async () => { try { const formData = { email, password, nickname // nicknameをフォームデータに含める }; const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }; const apiUrl = 'https://jsonplaceholder.typicode.com/users'; const response = await fetch(apiUrl, requestOptions); if (!response.ok) { throw new Error('Registration failed'); } const data = await response.json(); console.log('Registration successful!', data); // ここでトークンを保存するなどの処理を行う localStorage.setItem('nickname', JSON.stringify(nickname)); // nicknameを文字列としてローカルストレージに保存 } catch (error) { console.error('Registration failed:', error); // エラーメッセージを表示するなどの処理を行う } }; const handleRegister = () => { // Registerボタンがクリックされたときの処理を記述 fetchData(); navigate('/login'); // 登録後にログインページにリダイレクト }; return ( <div className={styles['box']}> <h2>Register(新規登録)</h2> <form> <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} /> <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} /> <input type="text" placeholder="Nickname" value={nickname} onChange={(e) => setNickname(e.target.value)} // nicknameの入力値を更新する /> <button type="button" onClick={handleRegister}> Register </button> </form> </div> ); } export default Register;
上記のコードでは、useStateフックを使って、email、password、nicknameの状態を管理しています。これらの状態は入力フィールドの値とバインドされます。
useEffectフックを使って、コンポーネントがマウントされた時に実行される処理を記述します。ここではfetchData関数が呼び出されます。
fetchData関数は、登録フォームのデータを含むformDataオブジェクトを作成し、それをJSON形式に変換してリクエストのbodyに設定します。その後、fetch関数を使ってAPIにリクエストを送信し、レスポンスを受け取ります。
レスポンスが正常な場合は、トークンの保存などの処理を行います。また、localStorage.setItemを使用して、nicknameを文字列としてローカルストレージに保存します。
登録ボタンがクリックされたときの処理はhandleRegister関数で行われます。fetchData関数が呼び出され、登録後に/login
へのリダイレクトが行われます。
JSX内では、<input>
要素を使ってメールアドレス、パスワード、ニックネームの入力フィールドを表示し、onChangeイベントハンドラを使って入力値を更新します。
登録ボタンにはonClickイベントハンドラを設定し、クリックされたときにhandleRegister関数が呼び出されます。
このコンポーネントは、新規登録フォームを表示し、ユーザーがメールアドレス、パスワード、ニックネームを入力して登録することができます。
登録が成功すると、トークンやニックネームの保存などの処理を行い、ログインページにリダイレクトします。
ただし、実際のアプリケーションでは、登録フォームのデータをAPIに送信してユーザーを登録する必要があります。
また、トークンやニックネームの保存方法もアプリケーションに応じてカスタマイズする必要があります。
エラーメッセージの表示やフィードバックなどの改善も検討してください。
ログアウト処理
以下は、関数コンポーネントとして実装されたダッシュボード機能を持つコンポーネントとなります。
// components/Dashboard.js import { useNavigate } from 'react-router-dom'; import styles from '../index.module.css'; const Dashboard = () => { const navigate = useNavigate(); const handleLogout = () => { // トークンの削除やセッションのクリアなど、ログアウトに関する処理を実装する // 例: ローカルストレージからトークンを削除 localStorage.removeItem('token'); localStorage.removeItem('nickname'); // ログアウト後にログインページにリダイレクトする navigate('/login'); console.log("ログアウトしました。") }; return ( <div className={styles['box']}> <h2>Dashboard</h2> {localStorage.getItem('token') && ( <p>Welcome, {localStorage.getItem('nickname')}!</p> )} <button type="button" onClick={handleLogout}> Logout </button> </div> ); } export default Dashboard;
上記のコードは、useNavigateフックを使って、リダイレクト機能を利用するためのnavigate関数を取得します。
handleLogout関数は、ログアウトボタンがクリックされたときの処理を実装しています。この関数では、トークンの削除やセッションのクリアなど、ログアウトに関連する処理を行います。
例として、localStorage.removeItemを使用して、ローカルストレージからトークンとニックネームを削除しています。
ログアウト後には、navigate関数を使ってログインページにリダイレクトします。'/login'
はリダイレクト先のURLです。
JSX内では、<p>
要素を使って、トークンが存在する場合に「Welcome, {nickname}!」と表示します。ニックネームはローカルストレージから取得して表示されます。
ログアウトボタンにはonClickイベントハンドラが設定されており、クリックされたときにhandleLogout関数が呼び出されます。
このコンポーネントは、ダッシュボードを表示し、ユーザーのログイン状態に応じて表示内容を制御します。
トークンがローカルストレージに存在する場合は、ユーザーのニックネームとログアウトボタンが表示され、ログアウトボタンがクリックされると、トークンとニックネームが削除され、ログインページにリダイレクトされます。
ただし、こちらの機能の実装でも、実際のアプリケーションでは、トークンの管理やログアウトに関する処理はアプリケーションの要件に応じてカスタマイズしていく必要があります。
また、ダッシュボードの表示内容もアプリケーションのデザインや要件に合わせて変更することができます。
注意点
各コンポーネント(Home、Login、Register、Dashboard)が正しくインポートされているか確認してください。
実際のファイル構造に基づいて各コンポーネントのパスを指定してインポートする必要があります。
ルーティングの設定が正しいか確認するようにしてください。
例えば、本日のコードであればルートパス / に対してはHomeコンポーネント、/loginパスに対してはLoginコンポーネント、/registerパスに対してはRegisterコンポーネント、/dashboardパスに対してはDashboardコンポーネントが指定されています。
これらのコンポーネントが正しく設定されていることを確認してください。
また、ブラウザで正しいバージョンのreact-router-domパッケージがインストールされているか確認してください。
ここでのコードはReact Router v6を想定しているため、対応するバージョンのパッケージを使用する必要があります。
バージョンの違いについては以下を参照ください。
また、React Routerを使用すると、URLのパスに基づいて異なるコンポーネントを表示することができます。ユーザーがリンクをクリックすると、React RouterはURLを変更し、対応するコンポーネントをレンダリングします。
このプロセスは、クライアントサイドでのページ遷移を実現するため、SPAアプリケーションの一部として見なされます。
SPAアプリケーションは、ユーザーエクスペリエンスの向上やレスポンス性の向上などの利点を提供する一方で、初回のロード時に大きなJavaScriptバンドルをダウンロードする必要があるため、初回のロード時間が長くなるという欠点もありますのでご注意ください。
この、SPAアプリケーションの初回ロード時間の長さという欠点を解決するためには、SSR(Server-Side Rendering)またはSSG(Static Site Generation)を使用することが一般的です。
以下で解説していますので参照ください。
SSRとSSGは、SPAアプリケーションの初回ロード時間を短縮し、パフォーマンスを向上させる方法として有効です。どちらを選択するかは、アプリケーションの要件やユーザーエクスペリエンスの目標に基づいて決定する必要があります。
また、本日は単純化にするためにCSS Modulesおよび通常のCSSでのスタイリングを行いましたが、Reactでは、ダークモードの設定や他のスタイリングの実装において、CSSフレームワークの使用は便利な選択肢です。
Tailwind CSSなどのCSSフレームワークは、既存のスタイルやコンポーネントを活用し、簡単にカスタマイズできるため、手動でスタイリングを行うよりも効率的に実装することができます。
ただし、今回のように自身でスタイリングを実装することも完全に可能です。
プロジェクトの要件やスタイリングのニーズに応じて、CSSフレームワークを使用するか、独自のスタイリングを実装するかを選択することが重要です。
これらの点を確認して、必要な修正や追加を行ってください。
最後に
この例では、単純なトークンの保存とリダイレクトの実装を行いましたが、実際のアプリケーションではセキュリティの観点からさまざまな対策が必要です。
安全なトークンの生成方法や、バックエンドサーバーとの通信における認証の実装など、セキュリティに関する要素を考慮する必要があります。
この記事で、React.jsのフックを使用してユーザー認証機能を持つシンプルなアプリを作成し、認証フロー、プライベートルートの保護、トークンの保存についてあなたは学びました。
これらの機能を実装する際には、テンプレートコードを参考にし、必要に応じてアプリケーションに合わせてカスタマイズや拡張を行ってください。
安全なトークンの生成方法については、乱数生成やハッシュ関数の使用などが考慮されるべきです。また、バックエンドサーバーとの通信における認証は、HTTPSプロトコルの使用やトークンの有効期限の管理などが重要です。
以上が、React.jsを使用してユーザー認証機能を持つシンプルなアプリの作成と、認証フロー、プライベートルートの保護、トークンの保存についての解説でした。
なお、今後リファクタリングの記事も投稿いたします。その記事では、React Contextを使用した状態管理コードに変更する方法について解説します。
最後までお読みいただき、ありがとうございました。
この記事が役に立ったら、ブックマークと共有をしていただけると幸いです。