ReactのuseStateが分からない|初心者向け完全ガイド
React useStateの基本から実践的な使い方まで初心者向けに詳しく解説。状態管理の概念、更新方法、よくあるエラーと解決方法を実践的なサンプルコードとともに紹介します。
みなさん、Reactを学び始めて「useStateって何?」と困ったことはありませんか?
「なぜ普通の変数だと画面が更新されないの?」 「useStateの使い方がよく分からない」 「状態管理って難しそう...」
こんな風に思った方も多いでしょう。
実は、useStateはReact開発において最も重要な機能の一つなんです。 この記事では、useStateの基本から実践的な使い方まで、初心者の方にも分かりやすく解説していきますよ。
状態管理の概念から、よくあるエラーの解決方法まで、実際のコード例と一緒に学んでいきましょう。
useStateって何?基本を理解しよう
まず、「そもそもuseStateって何?」という疑問から解決していきましょう。
状態管理の基本的な仕組み
useStateは、コンポーネントが値を覚えておくための仕組みです。
簡単に言うと、コンポーネントの「記憶力」を与えてくれる機能ですね。
// 普通の変数だと、こんな問題が起こります
const BadExample = () => {
let count = 0; // 普通の変数
const handleClick = () => {
count = count + 1; // 値は変わるが...
console.log(count); // コンソールには表示される
// でも画面は更新されない!
};
return (
<div>
<p>カウント: {count}</p> {/* 常に0のまま */}
<button onClick={handleClick}>増やす</button>
</div>
);
};
この例を見てみましょう。
let count = 0
で変数を宣言しています。
ボタンをクリックするとcount
の値は確かに増えます。
でも、画面には反映されません。 なぜでしょうか?
Reactでは、普通の変数が変わっただけでは画面を更新してくれないからなんです。
次に、useStateを使った例を見てみましょう。
import React, { useState } from 'react';
const GoodExample = () => {
const [count, setCount] = useState(0); // useStateを使用
const handleClick = () => {
setCount(count + 1); // setCountで値を更新
// 画面も自動的に更新される!
};
return (
<div>
<p>カウント: {count}</p> {/* 値がちゃんと表示される */}
<button onClick={handleClick}>増やす</button>
</div>
);
};
今度はuseState(0)
を使っています。
この書き方のポイントを説明しますね。
const [count, setCount] = useState(0)
の部分です。
ここで2つのものを受け取っています。
count
: 現在の値setCount
: 値を更新するための関数
setCount(count + 1)
で値を更新すると、Reactが「あ、値が変わったな」と気づいて、自動的に画面を更新してくれます。
これがuseStateの基本的な仕組みです。
useStateの基本的な書き方
useStateの書き方にはパターンがあります。
// 基本的な文法
const [状態の値, 更新関数] = useState(初期値);
// 具体的な例をいくつか見てみましょう
const [count, setCount] = useState(0); // 数値
const [name, setName] = useState(''); // 文字列
const [isVisible, setIsVisible] = useState(false); // 真偽値
const [items, setItems] = useState([]); // 配列
const [user, setUser] = useState({}); // オブジェクト
この書き方は「分割代入」と呼ばれるJavaScriptの機能です。
useStateは配列を返してくれます。 その配列の1番目が「現在の値」、2番目が「更新関数」です。
// useStateの中身を詳しく見ると...
const stateArray = useState(0); // [0, function]
const currentValue = stateArray[0]; // 現在の値
const updateFunction = stateArray[1]; // 更新関数
// これを短縮したのが通常の書き方
const [value, setValue] = useState(0);
関数の名前は「set + 変数名」にするのが一般的です。
例えば、count
ならsetCount
、name
ならsetName
という感じですね。
数値を扱ってみよう
それでは、実際にuseStateを使って数値を扱ってみましょう。
シンプルなカウンター
まずは基本的なカウンターから始めます。
const SimpleCounter = () => {
const [count, setCount] = useState(0);
// 値を増やす
const increment = () => {
setCount(count + 1);
};
// 値を減らす
const decrement = () => {
setCount(count - 1);
};
// 値をリセット
const reset = () => {
setCount(0);
};
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>シンプルカウンター</h2>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>
現在の値: {count}
</p>
<div>
<button onClick={decrement}>-1</button>
<button onClick={reset} style={{ margin: '0 10px' }}>
リセット
</button>
<button onClick={increment}>+1</button>
</div>
</div>
);
};
このコードを詳しく見ていきましょう。
まず、状態の宣言部分です。
const [count, setCount] = useState(0);
初期値を0
に設定しています。
次に、値を変更する関数を作っています。
const increment = () => {
setCount(count + 1);
};
setCount
に新しい値を渡すことで、状態を更新しています。
count + 1
で現在の値に1を足した値を設定しますね。
同じように、減らす関数とリセット関数も作っています。
const decrement = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
これらの関数をボタンのonClick
に設定すると、クリックで値が変わります。
より安全な更新方法
実は、状態を更新するときにより安全な方法があります。
const SafeCounter = () => {
const [count, setCount] = useState(0);
// 前の値を使った更新(推奨方法)
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>安全なカウンター</h2>
<p style={{ fontSize: '24px' }}>{count}</p>
<button onClick={decrement}>-1</button>
<button onClick={increment}>+1</button>
</div>
);
};
ここで注目したいのは、更新の仕方です。
setCount(prevCount => prevCount + 1);
この書き方では、関数をsetCount
に渡しています。
この関数は前の値(prevCount
)を受け取って、新しい値を返します。
なぜこの方法が良いのでしょうか?
実は、Reactの状態更新は非同期で処理されることがあります。
そのため、直接count
を参照すると、古い値を使ってしまう可能性があるんです。
関数形式で書くと、常に最新の値を使って更新できるので安全ですね。
文字列を扱ってみよう
次は、文字列の状態管理を見てみましょう。
入力フォームの作成
文字列の状態管理でよく使われるのが、入力フォームです。
const TextInput = () => {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (name && message) {
alert(`${name}さんからのメッセージ: ${message}`);
// フォームをリセット
setName('');
setMessage('');
}
};
return (
<div style={{ padding: '20px', maxWidth: '500px' }}>
<h2>メッセージフォーム</h2>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label>
名前:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="お名前を入力"
style={{
width: '100%',
padding: '8px',
marginTop: '5px'
}}
/>
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
メッセージ:
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="メッセージを入力"
rows={4}
style={{
width: '100%',
padding: '8px',
marginTop: '5px'
}}
/>
</label>
</div>
<button
type="submit"
disabled={!name || !message}
style={{
padding: '10px 20px',
backgroundColor: (!name || !message) ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px'
}}
>
送信
</button>
</form>
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f8f9fa' }}>
<h3>プレビュー</h3>
<p><strong>名前:</strong> {name || '(未入力)'}</p>
<p><strong>メッセージ:</strong> {message || '(未入力)'}</p>
<p><strong>文字数:</strong> {message.length} 文字</p>
</div>
</div>
);
};
このコードの重要な部分を見てみましょう。
まず、2つの状態を定義しています。
const [name, setName] = useState('');
const [message, setMessage] = useState('');
どちらも文字列なので、初期値は空文字(''
)に設定しています。
次に、入力フィールドとの連携部分です。
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
value={name}
で現在の状態を表示します。
onChange
で入力値が変わったときに状態を更新しますね。
e.target.value
は、入力フィールドに入力された値を取得します。
テキストエリアも同じような仕組みです。
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
フォーム送信の処理も見てみましょう。
const handleSubmit = (e) => {
e.preventDefault();
if (name && message) {
alert(`${name}さんからのメッセージ: ${message}`);
// フォームをリセット
setName('');
setMessage('');
}
};
e.preventDefault()
でページのリロードを防いでいます。
入力チェックをして、問題なければアラートを表示します。
最後に状態をリセットして、フォームを空にしています。
真偽値を扱ってみよう
boolean値(true/false)の状態管理も見てみましょう。
表示切り替えの実装
真偽値は、何かの表示/非表示を切り替えるときによく使います。
const ToggleExample = () => {
const [isVisible, setIsVisible] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const toggleVisibility = () => {
setIsVisible(!isVisible);
};
const toggleDarkMode = () => {
setIsDarkMode(!isDarkMode);
};
const simulateLoading = () => {
setIsLoading(true);
// 3秒後にローディングを終了
setTimeout(() => {
setIsLoading(false);
}, 3000);
};
const containerStyle = {
padding: '20px',
backgroundColor: isDarkMode ? '#333' : 'white',
color: isDarkMode ? 'white' : 'black',
transition: 'all 0.3s ease'
};
return (
<div style={containerStyle}>
<h2>表示切り替えの例</h2>
<div style={{ marginBottom: '20px' }}>
<button onClick={toggleVisibility}>
{isVisible ? '隠す' : '表示する'}
</button>
{isVisible && (
<div style={{
marginTop: '10px',
padding: '15px',
backgroundColor: isDarkMode ? '#555' : '#f0f0f0'
}}>
<p>🎉 この内容が表示されました!</p>
<p>ボタンで表示/非表示を切り替えられます。</p>
</div>
)}
</div>
<div style={{ marginBottom: '20px' }}>
<label>
<input
type="checkbox"
checked={isDarkMode}
onChange={(e) => setIsDarkMode(e.target.checked)}
style={{ marginRight: '8px' }}
/>
ダークモード
</label>
</div>
<div>
<button
onClick={simulateLoading}
disabled={isLoading}
style={{
padding: '10px 20px',
backgroundColor: isLoading ? '#ccc' : '#28a745',
color: 'white',
border: 'none'
}}
>
{isLoading ? 'ローディング中...' : 'データを読み込む'}
</button>
{isLoading && (
<div style={{ marginTop: '10px' }}>
<p>データを読み込んでいます...</p>
</div>
)}
</div>
</div>
);
};
このコードの面白い部分を見てみましょう。
まず、3つの真偽値を管理しています。
const [isVisible, setIsVisible] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false);
const [isLoading, setIsLoading] = useState(false);
それぞれ異なる目的で使っています。
表示切り替えの部分です。
const toggleVisibility = () => {
setIsVisible(!isVisible);
};
!isVisible
で現在の値を反転させています。
trueならfalseに、falseならtrueになりますね。
条件付きレンダリングも重要なポイントです。
{isVisible && (
<div>
<p>🎉 この内容が表示されました!</p>
</div>
)}
isVisible
がtrueのときだけ、divが表示されます。
これを「条件付きレンダリング」と呼びます。
チェックボックスとの連携も見てみましょう。
<input
type="checkbox"
checked={isDarkMode}
onChange={(e) => setIsDarkMode(e.target.checked)}
/>
checked={isDarkMode}
で現在の状態をチェックボックスに反映します。
e.target.checked
でチェックボックスの状態を取得しますね。
配列を扱ってみよう
配列の状態管理は少し複雑ですが、とても便利です。
Todoリストの作成
配列を使った典型的な例として、Todoリストを作ってみましょう。
const TodoList = () => {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const [nextId, setNextId] = useState(1);
// Todoを追加
const addTodo = () => {
if (inputValue.trim()) {
const newTodo = {
id: nextId,
text: inputValue,
completed: false,
createdAt: new Date().toLocaleString()
};
setTodos([...todos, newTodo]); // 新しい配列を作成
setInputValue('');
setNextId(nextId + 1);
}
};
// Todoを削除
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// Todoの完了状態を切り替え
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
return (
<div style={{ padding: '20px', maxWidth: '600px' }}>
<h2>Todoリスト</h2>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="新しいタスクを入力"
style={{
padding: '10px',
width: '300px',
border: '1px solid #ddd'
}}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button
onClick={addTodo}
style={{
padding: '10px 20px',
marginLeft: '10px',
backgroundColor: '#007bff',
color: 'white',
border: 'none'
}}
>
追加
</button>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
padding: '12px',
marginBottom: '8px',
backgroundColor: todo.completed ? '#f8f9fa' : 'white',
border: '1px solid #dee2e6'
}}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
style={{ marginRight: '12px' }}
/>
<div style={{ flex: 1 }}>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<div style={{ fontSize: '12px', color: '#6c757d' }}>
作成日時: {todo.createdAt}
</div>
</div>
<button
onClick={() => deleteTodo(todo.id)}
style={{
padding: '6px 12px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none'
}}
>
削除
</button>
</li>
))}
</ul>
{todos.length === 0 && (
<div style={{
textAlign: 'center',
padding: '40px',
color: '#6c757d'
}}>
<p>📝 まだタスクがありません</p>
<p>上のフォームから新しいタスクを追加してみましょう</p>
</div>
)}
</div>
);
};
このコードの重要なポイントを解説しますね。
まず、配列の状態を定義しています。
const [todos, setTodos] = useState([]);
初期値は空の配列([]
)です。
Todo追加の処理を詳しく見てみましょう。
const addTodo = () => {
if (inputValue.trim()) {
const newTodo = {
id: nextId,
text: inputValue,
completed: false,
createdAt: new Date().toLocaleString()
};
setTodos([...todos, newTodo]); // スプレッド演算子で新しい配列作成
}
};
ここで重要なのはsetTodos([...todos, newTodo])
の部分です。
...todos
はスプレッド演算子と呼ばれます。
既存の配列をコピーして、最後に新しい要素を追加しています。
元の配列を直接変更してはいけません。 必ず新しい配列を作成する必要があります。
削除処理も見てみましょう。
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
filter
メソッドで、指定されたID以外の要素だけを残した新しい配列を作成しています。
更新処理はもう少し複雑です。
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
map
メソッドで各要素をチェックしています。
指定されたIDの要素だけcompleted
を反転させて、他はそのまま返しています。
配列のレンダリングも重要なポイントです。
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
</li>
))}
map
メソッドで配列の各要素をJSXに変換しています。
key={todo.id}
は、Reactが効率的に更新するために必要です。
オブジェクトを扱ってみよう
オブジェクトの状態管理も見てみましょう。
ユーザープロフィール管理
複雑なデータ構造を管理する例として、ユーザープロフィールを作ってみます。
const UserProfile = () => {
const [user, setUser] = useState({
name: '',
email: '',
age: '',
bio: '',
preferences: {
theme: 'light',
notifications: true,
language: 'ja'
},
skills: []
});
const [newSkill, setNewSkill] = useState('');
// 基本情報の更新
const updateBasicInfo = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
// 設定の更新
const updatePreference = (key, value) => {
setUser(prevUser => ({
...prevUser,
preferences: {
...prevUser.preferences,
[key]: value
}
}));
};
// スキルを追加
const addSkill = () => {
if (newSkill.trim() && !user.skills.includes(newSkill.trim())) {
setUser(prevUser => ({
...prevUser,
skills: [...prevUser.skills, newSkill.trim()]
}));
setNewSkill('');
}
};
// スキルを削除
const removeSkill = (skillToRemove) => {
setUser(prevUser => ({
...prevUser,
skills: prevUser.skills.filter(skill => skill !== skillToRemove)
}));
};
return (
<div style={{ padding: '20px', maxWidth: '800px' }}>
<h2>ユーザープロフィール管理</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px' }}>
{/* 基本情報 */}
<div>
<h3>基本情報</h3>
<div style={{ marginBottom: '15px' }}>
<label>
名前:
<input
type="text"
value={user.name}
onChange={(e) => updateBasicInfo('name', e.target.value)}
style={{
width: '100%',
padding: '8px',
marginTop: '5px'
}}
/>
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
メールアドレス:
<input
type="email"
value={user.email}
onChange={(e) => updateBasicInfo('email', e.target.value)}
style={{
width: '100%',
padding: '8px',
marginTop: '5px'
}}
/>
</label>
</div>
</div>
{/* 設定とスキル */}
<div>
<h3>設定</h3>
<div style={{ marginBottom: '15px' }}>
<label>
テーマ:
<select
value={user.preferences.theme}
onChange={(e) => updatePreference('theme', e.target.value)}
style={{
width: '100%',
padding: '8px',
marginTop: '5px'
}}
>
<option value="light">ライト</option>
<option value="dark">ダーク</option>
</select>
</label>
</div>
<h3>スキル</h3>
<div style={{ marginBottom: '15px' }}>
<input
type="text"
value={newSkill}
onChange={(e) => setNewSkill(e.target.value)}
placeholder="新しいスキルを追加"
style={{
padding: '8px',
marginRight: '10px'
}}
onKeyPress={(e) => e.key === 'Enter' && addSkill()}
/>
<button
onClick={addSkill}
style={{
padding: '8px 16px',
backgroundColor: '#28a745',
color: 'white',
border: 'none'
}}
>
追加
</button>
</div>
<div>
{user.skills.map(skill => (
<span
key={skill}
style={{
display: 'inline-block',
padding: '4px 8px',
margin: '2px',
backgroundColor: '#007bff',
color: 'white',
borderRadius: '12px',
fontSize: '12px',
cursor: 'pointer'
}}
onClick={() => removeSkill(skill)}
title="クリックで削除"
>
{skill} ×
</span>
))}
</div>
</div>
</div>
<div style={{ marginTop: '30px', padding: '20px', backgroundColor: '#f8f9fa' }}>
<h3>プロフィールプレビュー</h3>
<pre style={{ fontSize: '14px', whiteSpace: 'pre-wrap' }}>
{JSON.stringify(user, null, 2)}
</pre>
</div>
</div>
);
};
このコードの重要な部分を見てみましょう。
複雑なオブジェクト構造の初期化です。
const [user, setUser] = useState({
name: '',
email: '',
age: '',
bio: '',
preferences: {
theme: 'light',
notifications: true,
language: 'ja'
},
skills: []
});
オブジェクトの中にオブジェクトや配列が含まれています。
基本情報の更新方法を見てみましょう。
const updateBasicInfo = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
...prevUser
で既存のプロパティをコピーしています。
[field]: value
で指定されたプロパティだけを更新しますね。
ネストしたオブジェクトの更新はもう少し複雑です。
const updatePreference = (key, value) => {
setUser(prevUser => ({
...prevUser,
preferences: {
...prevUser.preferences,
[key]: value
}
}));
};
preferences
オブジェクト全体を新しく作り直しています。
既存の設定をコピーして、指定されたキーだけを更新しますね。
これは少し複雑ですが、オブジェクトを直接変更してはいけないというReactのルールを守るために必要です。
よくある間違いと解決方法
useStateを使っていて、よくある間違いとその解決方法を見てみましょう。
状態を直接変更してしまう問題
最もよくある間違いがこれです。
const StateModificationExample = () => {
const [user, setUser] = useState({ name: '', age: 0 });
const [items, setItems] = useState([]);
// ❌ 間違い: 状態を直接変更
const badUpdate = () => {
user.name = '新しい名前'; // これは動かない
user.age = 25; // これも動かない
// setUserを呼んでいないので再レンダリングされない
};
const badAddItem = () => {
items.push('新しいアイテム'); // これも動かない
// setItemsを呼んでいないので再レンダリングされない
};
// ✅ 正しい: setterを使って更新
const goodUpdate = () => {
setUser({
...user, // 既存のプロパティをコピー
name: '新しい名前',
age: 25
});
};
const goodAddItem = () => {
setItems([...items, '新しいアイテム']); // 新しい配列を作成
};
return (
<div>
<h3>状態更新の正しい方法</h3>
<p>名前: {user.name}</p>
<p>年齢: {user.age}</p>
<p>アイテム数: {items.length}</p>
<div>
<button onClick={badUpdate}>❌ 間違った更新</button>
<button onClick={goodUpdate}>✅ 正しい更新</button>
</div>
</div>
);
};
間違った例を見てみましょう。
user.name = '新しい名前'; // これは動かない
この書き方では、オブジェクトのプロパティを直接変更しています。
でも、setUser
を呼んでいないので、Reactは変更に気づきません。
正しい方法はこちらです。
setUser({
...user, // 既存のプロパティをコピー
name: '新しい名前',
age: 25
});
スプレッド演算子で既存のプロパティをコピーして、新しいオブジェクトを作成しています。
非同期処理での問題
非同期処理では、また別の問題が起こることがあります。
const AsyncExample = () => {
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
// ❌ 間違い: 古い値を参照する可能性
const badAsyncUpdate = async () => {
setLoading(true);
// 複数回クリックされた場合、古いcountの値を使ってしまう
setTimeout(() => {
setCount(count + 1); // 古い値を参照
setLoading(false);
}, 1000);
};
// ✅ 正しい: 関数形式で最新の値を使用
const goodAsyncUpdate = async () => {
setLoading(true);
setTimeout(() => {
setCount(prevCount => prevCount + 1); // 最新の値を使用
setLoading(false);
}, 1000);
};
return (
<div>
<h3>非同期処理での状態更新</h3>
<p>カウント: {count}</p>
<p>状態: {loading ? 'ローディング中...' : '待機中'}</p>
<div>
<button
onClick={badAsyncUpdate}
disabled={loading}
>
❌ 間違った非同期更新
</button>
<button
onClick={goodAsyncUpdate}
disabled={loading}
>
✅ 正しい非同期更新
</button>
</div>
</div>
);
};
間違った例を見てみましょう。
setTimeout(() => {
setCount(count + 1); // 古い値を参照
}, 1000);
この場合、setTimeout
が実行される時点で、count
の値が古い可能性があります。
正しい方法はこちらです。
setTimeout(() => {
setCount(prevCount => prevCount + 1); // 最新の値を使用
}, 1000);
関数形式で書くことで、常に最新の値を使って更新できます。
実践的な活用例
最後に、実際のアプリケーションでよく使われるパターンを見てみましょう。
会員登録フォーム
フォームの状態管理は、実際の開発でとてもよく使われます。
const RegistrationForm = () => {
// フォームデータの状態
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
agreeToTerms: false
});
// エラーの状態
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// 入力値の更新
const handleInputChange = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
// エラーをクリア
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: ''
}));
}
};
// バリデーション
const validateForm = () => {
const newErrors = {};
if (!formData.username.trim()) {
newErrors.username = 'ユーザー名は必須です';
} else if (formData.username.length < 3) {
newErrors.username = 'ユーザー名は3文字以上で入力してください';
}
if (!formData.email.trim()) {
newErrors.email = 'メールアドレスは必須です';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = '有効なメールアドレスを入力してください';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'パスワードが一致しません';
}
if (!formData.agreeToTerms) {
newErrors.agreeToTerms = '利用規約に同意してください';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// フォーム送信
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
// API呼び出しのシミュレーション
await new Promise(resolve => setTimeout(resolve, 2000));
alert('登録が完了しました!');
// フォームリセット
setFormData({
username: '',
email: '',
password: '',
confirmPassword: '',
agreeToTerms: false
});
} catch (error) {
setErrors({ submit: 'エラーが発生しました。もう一度お試しください。' });
} finally {
setIsSubmitting(false);
}
};
return (
<div style={{ padding: '20px', maxWidth: '500px' }}>
<h2>会員登録</h2>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label>
ユーザー名 *
<input
type="text"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
style={{
width: '100%',
padding: '10px',
marginTop: '5px',
border: `1px solid ${errors.username ? '#dc3545' : '#ddd'}`
}}
/>
</label>
{errors.username && (
<div style={{ color: '#dc3545', fontSize: '14px', marginTop: '5px' }}>
{errors.username}
</div>
)}
</div>
<div style={{ marginBottom: '15px' }}>
<label>
メールアドレス *
<input
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
style={{
width: '100%',
padding: '10px',
marginTop: '5px',
border: `1px solid ${errors.email ? '#dc3545' : '#ddd'}`
}}
/>
</label>
{errors.email && (
<div style={{ color: '#dc3545', fontSize: '14px', marginTop: '5px' }}>
{errors.email}
</div>
)}
</div>
<div style={{ marginBottom: '20px' }}>
<label>
<input
type="checkbox"
checked={formData.agreeToTerms}
onChange={(e) => handleInputChange('agreeToTerms', e.target.checked)}
style={{ marginRight: '8px' }}
/>
利用規約に同意する *
</label>
{errors.agreeToTerms && (
<div style={{ color: '#dc3545', fontSize: '14px', marginTop: '5px' }}>
{errors.agreeToTerms}
</div>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
style={{
width: '100%',
padding: '12px',
backgroundColor: isSubmitting ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
fontSize: '16px'
}}
>
{isSubmitting ? '登録中...' : '登録する'}
</button>
</form>
</div>
);
};
このコードでは、複数の状態を組み合わせて使っています。
フォームデータ、エラー情報、送信状態を別々に管理していますね。
入力値の更新部分を見てみましょう。
const handleInputChange = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
汎用的な関数を作ることで、どのフィールドでも同じ関数が使えます。
バリデーション処理も重要なポイントです。
const validateForm = () => {
const newErrors = {};
if (!formData.username.trim()) {
newErrors.username = 'ユーザー名は必須です';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
エラーオブジェクトを作成して、問題があるフィールドだけにエラーメッセージを設定しています。
まとめ:useStateをマスターしよう
useStateについて、基本から実践的な使い方まで詳しく解説しました。
重要なポイントをまとめると
- useStateはコンポーネントの「記憶力」を提供してくれる
- 状態が変わると自動的に画面が更新される
- 配列の分割代入で値と更新関数を受け取る
- 状態は直接変更せず、必ずsetter関数を使う
- 配列やオブジェクトは新しいものを作成して更新する
基本的な使い方
- 数値、文字列、真偽値の管理
- 配列とオブジェクトの状態管理
- フォーム入力との連携
- 条件付きレンダリング
注意すべきポイント
- 状態の直接変更は避ける
- 非同期処理では関数形式の更新を使う
- 複雑な状態は適切に分割する
- エラーハンドリングも忘れずに
useStateは、React開発の基礎中の基礎です。 この記事で学んだ内容をしっかりと理解することで、より複雑なReactアプリケーションも作れるようになります。
まずは簡単なカウンターから始めて、徐々に複雑な状態管理に挑戦してみてください。
エラーが起きても大丈夫です! この記事の内容を参考にして、正しい使い方を身につけていきましょう。
ぜひ、useStateを使って素敵なReactアプリケーションを作ってくださいね!