Reactのthisが分からない|関数コンポーネントで解決

Reactのthisキーワードで悩んでいる方へ。クラスコンポーネントのthisの問題を関数コンポーネントで解決する方法を詳しく解説します。

Learning Next 運営
64 分で読めます

みなさん、Reactでthisキーワードが分からなくて困ったことはありませんか?

「thisの参照先が分からない」 「クラスコンポーネントのthisでエラーが出る」 「関数コンポーネントでthisは使えるの?」

このような疑問を持ったことはありませんか?

実は、関数コンポーネントを使えば、thisの問題は全て解決できるんです。 この記事では、Reactのthisの悩みを根本から解決する方法をお教えします。

クラスコンポーネントでの複雑なthisから、シンプルな関数コンポーネントまで。 具体的なコード例と一緒に、分かりやすく解説していきますね。

thisって何?基本から理解しよう

まずは、JavaScriptのthisキーワードについて基本を確認しましょう。

難しそうに見えますが、仕組みが分かれば大丈夫です。

JavaScriptでのthisの動作

// 1. 普通に呼び出したとき
console.log(this); // ブラウザでは window オブジェクト

// 2. オブジェクトのメソッドとして呼び出したとき
const person = {
    name: "太郎",
    greet: function() {
        console.log(this.name); // "太郎" が表示される
    }
};

person.greet(); // thisは person オブジェクトを参照

// 3. 関数として取り出して呼び出したとき
const greetFunction = person.greet;
greetFunction(); // undefined(厳密モードの場合)

このコードを見てください。 同じ関数でも、呼び出し方によってthisが変わるんです。

これがJavaScriptのthisの特徴なんですね。

thisの参照先が決まる仕組み

thisの参照先は、関数がどのように呼び出されたかで決まります。

function showThis() {
    console.log(this);
}

// 1. 関数として呼び出し
showThis(); // window オブジェクト

// 2. オブジェクトのメソッドとして呼び出し
const obj = {
    method: showThis
};
obj.method(); // obj オブジェクト

// 3. call で呼び出し
const customObj = { name: "カスタム" };
showThis.call(customObj); // customObj オブジェクト

同じ関数なのに、呼び出し方で結果が変わりますよね。

これがthisの混乱の原因なんです。

アロー関数のthis

アロー関数では、thisの動作が少し違います。

const obj = {
    name: "太郎",
    
    // 通常の関数
    regularFunction: function() {
        console.log(this.name); // "太郎"
    },
    
    // アロー関数
    arrowFunction: () => {
        console.log(this.name); // undefined
    }
};

obj.regularFunction(); // "太郎"
obj.arrowFunction();   // undefined

アロー関数は、外側のthisをそのまま使うという特徴があります。

この基本を理解すると、Reactでのthisの問題も見えてきますよ。

Reactクラスコンポーネントでthisが大変な理由

Reactのクラスコンポーネントでは、thisが原因で多くの問題が起こります。

実際のコードで見てみましょう。

よくあるthisのエラー

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }
    
    render() {
        return (
            <div>
                <p>カウント: {this.state.count}</p>
                <button onClick={this.handleClick}>
                    クリック
                </button>
            </div>
        );
    }
    
    handleClick() {
        // エラーが発生!
        console.log(this); // undefined
        this.setState({ count: this.state.count + 1 });
    }
}

このコードを実行すると、エラーが出てしまいます。

ボタンをクリックすると「Cannot read property 'setState' of undefined」というエラーが表示されるんです。

なぜthisがundefinedになるの?

問題の原因を詳しく見てみましょう。

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        
        // この時点では this は正しく設定されている
        console.log(this); // MyComponent のインスタンス
    }
    
    render() {
        // render メソッド内でも this は正常
        console.log(this); // MyComponent のインスタンス
        
        return (
            <button onClick={this.handleClick}>
                クリック
            </button>
        );
    }
    
    handleClick() {
        // イベントハンドラーでは this が undefined!
        console.log(this); // undefined
        this.setState({ count: this.state.count + 1 });
    }
}

Reactのイベントシステムでは、関数が以下のように呼び出されます。

// React内部での動作(簡略化)
const component = new MyComponent();
const handleClick = component.handleClick;

// 関数として呼び出されるため、thisはundefined
handleClick();

つまり、メソッドがオブジェクトから切り離されて呼び出されるんです。

だからthisがundefinedになってしまうんですね。

従来の解決方法:bindを使う

クラスコンポーネントでは、bindを使ってthisを固定します。

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        
        // bindでthisを固定
        this.handleClick = this.handleClick.bind(this);
    }
    
    handleClick() {
        console.log(this); // MyComponent のインスタンス
        this.setState({ count: this.state.count + 1 });
    }
    
    render() {
        return (
            <button onClick={this.handleClick}>
                クリック
            </button>
        );
    }
}

この方法で動くようになります。

でも、メソッドが増えるたびにbindが必要になって大変ですよね。

アロー関数を使った解決方法

アロー関数を使う方法もあります。

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
    }
    
    // アロー関数で定義
    handleClick = () => {
        console.log(this); // MyComponent のインスタンス
        this.setState({ count: this.state.count + 1 });
    }
    
    render() {
        return (
            <button onClick={this.handleClick}>
                クリック
            </button>
        );
    }
}

アロー関数なら、外側のthisをそのまま使ってくれます。

この方法の方が少し楽ですが、まだ複雑ですよね。

クラスコンポーネントの問題点

クラスコンポーネントには、thisに関する複数の問題があります。

実際に見てみましょう。

1. コードが複雑になってしまう

class ComplexComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            name: '',
            email: '',
            message: ''
        };
        
        // たくさんのbindが必要
        this.handleNameChange = this.handleNameChange.bind(this);
        this.handleEmailChange = this.handleEmailChange.bind(this);
        this.handleMessageChange = this.handleMessageChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleReset = this.handleReset.bind(this);
    }
    
    handleNameChange(e) {
        this.setState({ name: e.target.value });
    }
    
    handleEmailChange(e) {
        this.setState({ email: e.target.value });
    }
    
    handleMessageChange(e) {
        this.setState({ message: e.target.value });
    }
    
    handleSubmit(e) {
        e.preventDefault();
        // 送信処理
    }
    
    handleReset() {
        this.setState({ name: '', email: '', message: '' });
    }
    
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <input 
                    value={this.state.name}
                    onChange={this.handleNameChange}
                />
                <input 
                    value={this.state.email}
                    onChange={this.handleEmailChange}
                />
                <textarea 
                    value={this.state.message}
                    onChange={this.handleMessageChange}
                />
                <button type="submit">送信</button>
                <button type="button" onClick={this.handleReset}>リセット</button>
            </form>
        );
    }
}

メソッドが増えるたびに、constructorでbindが必要になります。

これだと、コードがどんどん長くなってしまいますね。

2. 初心者には理解が困難

// なぜこれはエラーになるの?
class ConfusingComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
    }
    
    // この書き方だとエラーになる
    handleClick() {
        this.setState({ count: this.state.count + 1 });
    }
    
    // なぜアロー関数だと動くの?
    handleClick2 = () => {
        this.setState({ count: this.state.count + 1 });
    }
    
    render() {
        return (
            <div>
                {/* エラーになる */}
                <button onClick={this.handleClick}>エラー</button>
                
                {/* 動く */}
                <button onClick={this.handleClick2}>動く</button>
                
                {/* これも動く */}
                <button onClick={() => this.handleClick()}>動く</button>
            </div>
        );
    }
}

初心者の方は「なぜ同じような書き方なのに結果が違うの?」と混乱してしまいます。

JavaScriptのthisの仕組みを理解していないと、なかなか理解できませんよね。

3. パフォーマンスの問題も

class PerformanceIssue extends React.Component {
    constructor(props) {
        super(props);
        this.state = { items: [] };
    }
    
    handleItemClick(itemId) {
        console.log("アイテムクリック:", itemId);
    }
    
    render() {
        return (
            <div>
                {this.state.items.map(item => (
                    <div 
                        key={item.id}
                        // 毎回新しい関数が作成される
                        onClick={() => this.handleItemClick(item.id)}
                    >
                        {item.name}
                    </div>
                ))}
            </div>
        );
    }
}

インラインのアロー関数を使うと、毎回新しい関数が作成されてしまいます。

これはパフォーマンスに悪影響を与える可能性があるんです。

4. テストも大変

class TestingDifficulty extends React.Component {
    constructor(props) {
        super(props);
        this.state = { value: '' };
        this.handleChange = this.handleChange.bind(this);
    }
    
    handleChange(e) {
        this.setState({ value: e.target.value });
    }
    
    render() {
        return (
            <input 
                value={this.state.value}
                onChange={this.handleChange}
            />
        );
    }
}

// テストコードも複雑
test('input change handling', () => {
    const wrapper = mount(<TestingDifficulty />);
    const instance = wrapper.instance();
    
    // thisの束縛を考慮したテストが必要
    instance.handleChange({ target: { value: 'test' } });
    
    expect(wrapper.state('value')).toBe('test');
});

テストでも、thisの束縛を考慮する必要があります。

これだと、テストコードも複雑になってしまいますよね。

でも大丈夫です!関数コンポーネントなら、これらの問題が全て解決されます。

関数コンポーネントでthisの悩みを解決

関数コンポーネントを使えば、thisの問題が一切なくなります。

実際に見てみましょう。

基本的な関数コンポーネント

import React, { useState } from 'react';

function MyComponent() {
    const [count, setCount] = useState(0);
    
    const handleClick = () => {
        setCount(count + 1);
    };
    
    return (
        <div>
            <p>カウント: {count}</p>
            <button onClick={handleClick}>
                クリック
            </button>
        </div>
    );
}

このコードを見てください。

thisが一切出てきませんよね!

代わりにuseStateを使って状態管理をしています。

なぜthisの問題が起こらないの?

関数コンポーネントでは、thisを使わないからです。

function FunctionComponent() {
    const [state, setState] = useState({ value: '' });
    
    // thisは一切使わない
    const handleChange = (e) => {
        setState({ value: e.target.value });
    };
    
    // thisの束縛を考える必要なし
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log(state.value);
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input 
                value={state.value}
                onChange={handleChange}
            />
            <button type="submit">送信</button>
        </form>
    );
}

すべて関数内の変数や関数として定義します。

thisの束縛を考える必要が全くないんです。

複雑なフォームも簡単に

import React, { useState } from 'react';

function ContactForm() {
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        message: ''
    });
    
    // bindも this も不要
    const handleChange = (field) => (e) => {
        setFormData({
            ...formData,
            [field]: e.target.value
        });
    };
    
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('送信データ:', formData);
    };
    
    const handleReset = () => {
        setFormData({
            name: '',
            email: '',
            message: ''
        });
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input 
                value={formData.name}
                onChange={handleChange('name')}
                placeholder="名前"
            />
            <input 
                value={formData.email}
                onChange={handleChange('email')}
                placeholder="メールアドレス"
            />
            <textarea 
                value={formData.message}
                onChange={handleChange('message')}
                placeholder="メッセージ"
            />
            <button type="submit">送信</button>
            <button type="button" onClick={handleReset}>リセット</button>
        </form>
    );
}

先ほどのクラスコンポーネントと比べてみてください。

bindが一切ないので、とてもすっきりしていますよね。

コードも短くなって、理解しやすくなりました。

もっとシンプルな書き方

さらにシンプルに書くこともできます。

import React, { useState } from 'react';

function SimpleForm() {
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        message: ''
    });
    
    // 汎用的なchangeハンドラー
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));
    };
    
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('送信データ:', formData);
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input 
                name="name"
                value={formData.name}
                onChange={handleChange}
                placeholder="名前"
            />
            <input 
                name="email"
                value={formData.email}
                onChange={handleChange}
                placeholder="メールアドレス"
            />
            <textarea 
                name="message"
                value={formData.message}
                onChange={handleChange}
                placeholder="メッセージ"
            />
            <button type="submit">送信</button>
        </form>
    );
}

一つのhandleChange関数で、すべての入力を処理できます。

name属性を使って、どの項目が変更されたかを判断しているんです。

とても効率的な書き方ですよね!

クラスから関数コンポーネントへの移行方法

既存のクラスコンポーネントを関数コンポーネントに変更する方法を学びましょう。

ステップバイステップで進めていきます。

基本的な移行パターン

まず、シンプルなカウンターコンポーネントから見てみましょう。

// 移行前: クラスコンポーネント
class CounterClass extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
        this.handleIncrement = this.handleIncrement.bind(this);
        this.handleDecrement = this.handleDecrement.bind(this);
    }
    
    handleIncrement() {
        this.setState({ count: this.state.count + 1 });
    }
    
    handleDecrement() {
        this.setState({ count: this.state.count - 1 });
    }
    
    render() {
        return (
            <div>
                <p>カウント: {this.state.count}</p>
                <button onClick={this.handleIncrement}>+</button>
                <button onClick={this.handleDecrement}>-</button>
            </div>
        );
    }
}

これを関数コンポーネントに変更すると、こうなります。

// 移行後: 関数コンポーネント
function CounterFunction() {
    const [count, setCount] = useState(0);
    
    const handleIncrement = () => {
        setCount(count + 1);
    };
    
    const handleDecrement = () => {
        setCount(count - 1);
    };
    
    return (
        <div>
            <p>カウント: {count}</p>
            <button onClick={handleIncrement}>+</button>
            <button onClick={handleDecrement}>-</button>
        </div>
    );
}

変更のポイントは以下の通りです:

  1. classを関数に変更
  2. this.stateをuseStateに変更
  3. this.setStateをsetCount等に変更
  4. bindやthisを全て削除

とても簡潔になりましたね!

ライフサイクルメソッドの移行

データ取得をするコンポーネントの移行例を見てみましょう。

// 移行前: クラスコンポーネント
class DataFetcherClass extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null,
            loading: true,
            error: null
        };
    }
    
    componentDidMount() {
        this.fetchData();
    }
    
    componentDidUpdate(prevProps) {
        if (prevProps.userId !== this.props.userId) {
            this.fetchData();
        }
    }
    
    fetchData = async () => {
        try {
            this.setState({ loading: true });
            const response = await fetch(`/api/users/${this.props.userId}`);
            const data = await response.json();
            this.setState({ data, loading: false });
        } catch (error) {
            this.setState({ error, loading: false });
        }
    }
    
    render() {
        if (this.state.loading) return <div>読み込み中...</div>;
        if (this.state.error) return <div>エラー: {this.state.error.message}</div>;
        if (!this.state.data) return <div>データがありません</div>;
        
        return <div>{this.state.data.name}</div>;
    }
}

これを関数コンポーネントに移行すると、こうなります。

// 移行後: 関数コンポーネント
function DataFetcherFunction({ userId }) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        const fetchData = async () => {
            try {
                setLoading(true);
                const response = await fetch(`/api/users/${userId}`);
                const data = await response.json();
                setData(data);
                setLoading(false);
            } catch (error) {
                setError(error);
                setLoading(false);
            }
        };
        
        fetchData();
    }, [userId]); // userIdが変更されたときに再実行
    
    if (loading) return <div>読み込み中...</div>;
    if (error) return <div>エラー: {error.message}</div>;
    if (!data) return <div>データがありません</div>;
    
    return <div>{data.name}</div>;
}

ポイントはuseEffectの使い方です。

第二引数の配列で、どの値が変わったときに再実行するかを指定します。

[userId]と書くことで、userIdが変わったときだけデータを再取得するんです。

複雑な状態管理の移行

複数の状態を持つコンポーネントの移行例です。

// 移行前: クラスコンポーネント
class UserManagerClass extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            users: [],
            selectedUserId: null,
            filter: '',
            sortBy: 'name'
        };
        this.handleUserSelect = this.handleUserSelect.bind(this);
        this.handleFilterChange = this.handleFilterChange.bind(this);
        this.handleSortChange = this.handleSortChange.bind(this);
    }
    
    handleUserSelect(userId) {
        this.setState({ selectedUserId: userId });
    }
    
    handleFilterChange(e) {
        this.setState({ filter: e.target.value });
    }
    
    handleSortChange(e) {
        this.setState({ sortBy: e.target.value });
    }
    
    render() {
        const filteredUsers = this.state.users.filter(user =>
            user.name.toLowerCase().includes(this.state.filter.toLowerCase())
        );
        
        const sortedUsers = filteredUsers.sort((a, b) => {
            if (this.state.sortBy === 'name') {
                return a.name.localeCompare(b.name);
            } else if (this.state.sortBy === 'email') {
                return a.email.localeCompare(b.email);
            }
            return 0;
        });
        
        return (
            <div>
                <input 
                    value={this.state.filter}
                    onChange={this.handleFilterChange}
                    placeholder="フィルター"
                />
                <select 
                    value={this.state.sortBy}
                    onChange={this.handleSortChange}
                >
                    <option value="name">名前順</option>
                    <option value="email">メール順</option>
                </select>
                <ul>
                    {sortedUsers.map(user => (
                        <li 
                            key={user.id}
                            onClick={() => this.handleUserSelect(user.id)}
                        >
                            {user.name} ({user.email})
                        </li>
                    ))}
                </ul>
            </div>
        );
    }
}

関数コンポーネントに移行すると、このようになります。

// 移行後: 関数コンポーネント
function UserManagerFunction() {
    const [users, setUsers] = useState([]);
    const [selectedUserId, setSelectedUserId] = useState(null);
    const [filter, setFilter] = useState('');
    const [sortBy, setSortBy] = useState('name');
    
    const handleUserSelect = (userId) => {
        setSelectedUserId(userId);
    };
    
    const handleFilterChange = (e) => {
        setFilter(e.target.value);
    };
    
    const handleSortChange = (e) => {
        setSortBy(e.target.value);
    };
    
    const filteredUsers = users.filter(user =>
        user.name.toLowerCase().includes(filter.toLowerCase())
    );
    
    const sortedUsers = filteredUsers.sort((a, b) => {
        if (sortBy === 'name') {
            return a.name.localeCompare(b.name);
        } else if (sortBy === 'email') {
            return a.email.localeCompare(b.email);
        }
        return 0;
    });
    
    return (
        <div>
            <input 
                value={filter}
                onChange={handleFilterChange}
                placeholder="フィルター"
            />
            <select 
                value={sortBy}
                onChange={handleSortChange}
            >
                <option value="name">名前順</option>
                <option value="email">メール順</option>
            </select>
            <ul>
                {sortedUsers.map(user => (
                    <li 
                        key={user.id}
                        onClick={() => handleUserSelect(user.id)}
                    >
                        {user.name} ({user.email})
                    </li>
                ))}
            </ul>
        </div>
    );
}

複雑な状態も、それぞれ個別のuseStateで管理できます。

bindが一切ないので、とてもすっきりしていますね。

関数コンポーネントの素晴らしいメリット

関数コンポーネントには、たくさんのメリットがあります。

実際に見てみましょう。

1. コードが短くなる

同じ機能でも、コードの量が大幅に減ります。

// クラスコンポーネント(28行)
class WelcomeClass extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isVisible: true
        };
        this.handleToggle = this.handleToggle.bind(this);
    }
    
    handleToggle() {
        this.setState({ isVisible: !this.state.isVisible });
    }
    
    render() {
        return (
            <div>
                {this.state.isVisible && (
                    <h1>ようこそ、{this.props.name}さん!</h1>
                )}
                <button onClick={this.handleToggle}>
                    {this.state.isVisible ? '非表示' : '表示'}
                </button>
            </div>
        );
    }
}

// 関数コンポーネント(16行)
function WelcomeFunction({ name }) {
    const [isVisible, setIsVisible] = useState(true);
    
    const handleToggle = () => {
        setIsVisible(!isVisible);
    };
    
    return (
        <div>
            {isVisible && (
                <h1>ようこそ、{name}さん!</h1>
            )}
            <button onClick={handleToggle}>
                {isVisible ? '非表示' : '表示'}
            </button>
        </div>
    );
}

行数が28行から16行になりました。

約半分になっているんです!

2. 理解しやすい

関数コンポーネントは直感的で分かりやすいです。

// クラスコンポーネント:複雑
class ComplexClass extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        // なぜbindが必要?
        this.handleClick = this.handleClick.bind(this);
    }
    
    handleClick() {
        // なぜthisが使える?
        this.setState({ count: this.state.count + 1 });
    }
    
    render() {
        return <button onClick={this.handleClick}>クリック</button>;
    }
}

// 関数コンポーネント:シンプル
function SimpleFunction() {
    const [count, setCount] = useState(0);
    
    const handleClick = () => {
        setCount(count + 1);
    };
    
    return <button onClick={handleClick}>クリック</button>;
}

関数コンポーネントなら、初心者の方でも理解しやすいです。

bindやthisの知識がなくても、すぐに書けるようになりますよ。

3. テストが簡単

関数コンポーネントのテストは、とても簡単です。

import { render, fireEvent } from '@testing-library/react';

test('counter functionality', () => {
    const { getByText } = render(<SimpleFunction />);
    const button = getByText('クリック');
    
    // シンプルなテスト
    fireEvent.click(button);
    
    // 結果確認も簡単
    expect(getByText('1')).toBeInTheDocument();
});

thisの束縛を考える必要がないので、テストコードもシンプルになります。

4. パフォーマンスの最適化

React.memoを使って、簡単にパフォーマンス最適化ができます。

const OptimizedComponent = React.memo(function MyComponent({ name, count }) {
    return (
        <div>
            <h1>{name}</h1>
            <p>カウント: {count}</p>
        </div>
    );
});

// 使用例
function ParentComponent() {
    const [otherState, setOtherState] = useState(0);
    
    return (
        <div>
            <OptimizedComponent name="太郎" count={5} />
            <button onClick={() => setOtherState(otherState + 1)}>
                他の状態を更新
            </button>
        </div>
    );
}

React.memoで囲むことで、propsが変わらない限り再レンダリングされません。

これにより、パフォーマンスが向上するんです。

5. カスタムフックで再利用

カスタムフックを作ることで、ロジックを再利用できます。

// 再利用可能なロジック
function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);
    
    const increment = () => setCount(count + 1);
    const decrement = () => setCount(count - 1);
    const reset = () => setCount(initialValue);
    
    return { count, increment, decrement, reset };
}

// 複数のコンポーネントで再利用
function CounterA() {
    const { count, increment, decrement, reset } = useCounter(0);
    
    return (
        <div>
            <p>カウンターA: {count}</p>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
            <button onClick={reset}>リセット</button>
        </div>
    );
}

function CounterB() {
    const { count, increment, decrement, reset } = useCounter(10);
    
    return (
        <div>
            <p>カウンターB: {count}</p>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
            <button onClick={reset}>リセット</button>
        </div>
    );
}

同じロジックを複数のコンポーネントで使えるんです。

初期値を変えるだけで、違う挙動のカウンターが作れますね。

とても便利です!

実際のプロジェクトで使える例

実際の開発でよく使われるパターンをご紹介します。

すぐに使える実用的な例ばかりです。

ログインフォーム

ユーザー認証でよく使うログインフォームです。

import React, { useState } from 'react';

function LoginForm({ onLogin }) {
    const [formData, setFormData] = useState({
        email: '',
        password: ''
    });
    const [errors, setErrors] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);
    
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));
        
        // エラーをクリア
        if (errors[name]) {
            setErrors(prev => ({
                ...prev,
                [name]: ''
            }));
        }
    };
    
    const validateForm = () => {
        const newErrors = {};
        
        if (!formData.email) {
            newErrors.email = 'メールアドレスを入力してください';
        } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
            newErrors.email = '有効なメールアドレスを入力してください';
        }
        
        if (!formData.password) {
            newErrors.password = 'パスワードを入力してください';
        } else if (formData.password.length < 6) {
            newErrors.password = 'パスワードは6文字以上で入力してください';
        }
        
        return newErrors;
    };
    
    const handleSubmit = async (e) => {
        e.preventDefault();
        
        const validationErrors = validateForm();
        if (Object.keys(validationErrors).length > 0) {
            setErrors(validationErrors);
            return;
        }
        
        setIsSubmitting(true);
        
        try {
            await onLogin(formData);
        } catch (error) {
            setErrors({ submit: error.message });
        } finally {
            setIsSubmitting(false);
        }
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label htmlFor="email">メールアドレス</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                />
                {errors.email && <span className="error">{errors.email}</span>}
            </div>
            
            <div>
                <label htmlFor="password">パスワード</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    value={formData.password}
                    onChange={handleChange}
                />
                {errors.password && <span className="error">{errors.password}</span>}
            </div>
            
            {errors.submit && <div className="error">{errors.submit}</div>}
            
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? 'ログイン中...' : 'ログイン'}
            </button>
        </form>
    );
}

このログインフォームには、以下の機能が含まれています:

  1. バリデーション:メールアドレスとパスワードの検証
  2. エラー表示:入力エラーの表示
  3. 送信中状態:ボタンの無効化とテキスト変更
  4. エラークリア:入力時の自動エラークリア

thisを使わずに、すべて実装できています。

データ一覧表示

ユーザー一覧などでよく使う、データ表示コンポーネントです。

import React, { useState, useEffect } from 'react';

function UserList() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [searchTerm, setSearchTerm] = useState('');
    const [sortBy, setSortBy] = useState('name');
    const [sortOrder, setSortOrder] = useState('asc');
    
    useEffect(() => {
        const fetchUsers = async () => {
            try {
                setLoading(true);
                const response = await fetch('/api/users');
                if (!response.ok) {
                    throw new Error('ユーザーの取得に失敗しました');
                }
                const data = await response.json();
                setUsers(data);
            } catch (error) {
                setError(error.message);
            } finally {
                setLoading(false);
            }
        };
        
        fetchUsers();
    }, []);
    
    const handleSearchChange = (e) => {
        setSearchTerm(e.target.value);
    };
    
    const handleSort = (field) => {
        if (sortBy === field) {
            setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
        } else {
            setSortBy(field);
            setSortOrder('asc');
        }
    };
    
    const filteredUsers = users.filter(user =>
        user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
        user.email.toLowerCase().includes(searchTerm.toLowerCase())
    );
    
    const sortedUsers = filteredUsers.sort((a, b) => {
        const aValue = a[sortBy];
        const bValue = b[sortBy];
        
        if (sortOrder === 'asc') {
            return aValue.localeCompare(bValue);
        } else {
            return bValue.localeCompare(aValue);
        }
    });
    
    if (loading) {
        return <div>読み込み中...</div>;
    }
    
    if (error) {
        return <div>エラー: {error}</div>;
    }
    
    return (
        <div>
            <div>
                <input
                    type="text"
                    placeholder="ユーザーを検索..."
                    value={searchTerm}
                    onChange={handleSearchChange}
                />
            </div>
            
            <table>
                <thead>
                    <tr>
                        <th onClick={() => handleSort('name')}>
                            名前 {sortBy === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
                        </th>
                        <th onClick={() => handleSort('email')}>
                            メール {sortBy === 'email' && (sortOrder === 'asc' ? '↑' : '↓')}
                        </th>
                        <th onClick={() => handleSort('role')}>
                            役割 {sortBy === 'role' && (sortOrder === 'asc' ? '↑' : '↓')}
                        </th>
                    </tr>
                </thead>
                <tbody>
                    {sortedUsers.map(user => (
                        <tr key={user.id}>
                            <td>{user.name}</td>
                            <td>{user.email}</td>
                            <td>{user.role}</td>
                        </tr>
                    ))}
                </tbody>
            </table>
        </div>
    );
}

この一覧表示には、以下の機能があります:

  1. データ取得:API からユーザー情報を取得
  2. 検索機能:名前やメールでの絞り込み
  3. ソート機能:各列でのソート(昇順・降順)
  4. ローディング表示:データ取得中の表示
  5. エラー処理:エラー発生時の表示

すべて関数コンポーネントで実装できています。

ショッピングカート

ECサイトでよく使うショッピングカート機能です。

import React, { useState, useEffect } from 'react';

function ShoppingCart() {
    const [items, setItems] = useState([]);
    const [total, setTotal] = useState(0);
    
    useEffect(() => {
        const newTotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
        setTotal(newTotal);
    }, [items]);
    
    const addItem = (product) => {
        const existingItem = items.find(item => item.id === product.id);
        
        if (existingItem) {
            setItems(items.map(item =>
                item.id === product.id
                    ? { ...item, quantity: item.quantity + 1 }
                    : item
            ));
        } else {
            setItems([...items, { ...product, quantity: 1 }]);
        }
    };
    
    const removeItem = (productId) => {
        setItems(items.filter(item => item.id !== productId));
    };
    
    const updateQuantity = (productId, newQuantity) => {
        if (newQuantity <= 0) {
            removeItem(productId);
        } else {
            setItems(items.map(item =>
                item.id === productId
                    ? { ...item, quantity: newQuantity }
                    : item
            ));
        }
    };
    
    const clearCart = () => {
        setItems([]);
    };
    
    return (
        <div>
            <h2>ショッピングカート</h2>
            
            {items.length === 0 ? (
                <p>カートに商品がありません</p>
            ) : (
                <>
                    <div>
                        {items.map(item => (
                            <div key={item.id} className="cart-item">
                                <h3>{item.name}</h3>
                                <p>価格: ¥{item.price.toLocaleString()}</p>
                                <div>
                                    <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
                                        -
                                    </button>
                                    <span>数量: {item.quantity}</span>
                                    <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
                                        +
                                    </button>
                                </div>
                                <p>小計: ¥{(item.price * item.quantity).toLocaleString()}</p>
                                <button onClick={() => removeItem(item.id)}>
                                    削除
                                </button>
                            </div>
                        ))}
                    </div>
                    
                    <div className="cart-summary">
                        <h3>合計: ¥{total.toLocaleString()}</h3>
                        <button onClick={clearCart}>
                            カートをクリア
                        </button>
                        <button>
                            購入手続きへ
                        </button>
                    </div>
                </>
            )}
        </div>
    );
}

このショッピングカートの機能:

  1. 商品追加:既存商品は数量増加、新商品は追加
  2. 数量変更:+ / - ボタンで数量調整
  3. 商品削除:個別削除とカート全体クリア
  4. 合計計算:商品の合計金額を自動計算
  5. 自動更新:商品変更時の合計金額更新

これらすべてが、thisを使わずに実装されています。

関数コンポーネントの力強さを感じられますね!

よくある質問にお答えします

関数コンポーネントでのthisに関する疑問にお答えします。

初心者の方がよく持つ質問ばかりです。

Q1: 関数コンポーネントでthisを使えますか?

// 関数コンポーネントでthisを使おうとした場合
function MyComponent() {
    const [count, setCount] = useState(0);
    
    // これは動作しません
    this.handleClick = () => {
        setCount(count + 1);
    };
    
    // thisは定義されていません
    console.log(this); // undefined
    
    return <button onClick={this.handleClick}>クリック</button>;
}

答え:関数コンポーネントではthisは使用できません。

代わりに、関数内で直接変数や関数を定義します。

// 正しい書き方
function MyComponent() {
    const [count, setCount] = useState(0);
    
    const handleClick = () => {
        setCount(count + 1);
    };
    
    return <button onClick={handleClick}>クリック</button>;
}

thisを使わない方が、むしろ分かりやすいですよね。

Q2: インスタンスメソッドのような機能は作れますか?

// クラスコンポーネントのメソッド
class MyClass extends React.Component {
    someMethod() {
        return "何らかの処理";
    }
    
    render() {
        return <div>{this.someMethod()}</div>;
    }
}

// 関数コンポーネントでの代替
function MyFunction() {
    const someMethod = () => {
        return "何らかの処理";
    };
    
    return <div>{someMethod()}</div>;
}

答え:関数コンポーネントでは、コンポーネント内で関数を定義することで同様の機能を実現できます。

むしろ、thisを使わない分、シンプルになります。

Q3: 親コンポーネントから子のメソッドを呼べますか?

特殊なケースですが、forwardRefuseImperativeHandleを使えば可能です。

import React, { forwardRef, useImperativeHandle, useState, useRef } from 'react';

const MyComponent = forwardRef((props, ref) => {
    const [count, setCount] = useState(0);
    
    useImperativeHandle(ref, () => ({
        increment: () => setCount(count + 1),
        decrement: () => setCount(count - 1),
        reset: () => setCount(0),
        getCount: () => count
    }));
    
    return <div>カウント: {count}</div>;
});

// 使用例
function ParentComponent() {
    const componentRef = useRef();
    
    const handleClick = () => {
        componentRef.current.increment();
    };
    
    return (
        <div>
            <MyComponent ref={componentRef} />
            <button onClick={handleClick}>外部から増加</button>
        </div>
    );
}

答えforwardRefuseImperativeHandleを使用することで、親から子のメソッドを呼び出せます。

ただし、通常はpropsを使った方が良いケースがほとんどです。

Q4: 複数のコンポーネントで状態を共有するには?

Context APIを使用すれば、状態を共有できます。

import React, { createContext, useContext, useState } from 'react';

const StateContext = createContext();

function StateProvider({ children }) {
    const [sharedState, setSharedState] = useState({
        user: null,
        theme: 'light'
    });
    
    return (
        <StateContext.Provider value={{ sharedState, setSharedState }}>
            {children}
        </StateContext.Provider>
    );
}

function ComponentA() {
    const { sharedState, setSharedState } = useContext(StateContext);
    
    const updateUser = (user) => {
        setSharedState(prev => ({ ...prev, user }));
    };
    
    return (
        <div>
            <p>ユーザー: {sharedState.user?.name || 'ゲスト'}</p>
            <button onClick={() => updateUser({ name: '太郎' })}>
                ユーザー設定
            </button>
        </div>
    );
}

function ComponentB() {
    const { sharedState, setSharedState } = useContext(StateContext);
    
    const toggleTheme = () => {
        setSharedState(prev => ({
            ...prev,
            theme: prev.theme === 'light' ? 'dark' : 'light'
        }));
    };
    
    return (
        <div>
            <p>テーマ: {sharedState.theme}</p>
            <button onClick={toggleTheme}>
                テーマ切り替え
            </button>
        </div>
    );
}

答え:Context APIを使用することで、複数のコンポーネント間で状態を共有できます。

thisを使わずに、グローバルな状態管理が可能です。

まとめ

Reactのthisキーワードの問題と、関数コンポーネントでの解決方法について詳しく解説しました。

重要なポイントをまとめます

thisの問題

  • クラスコンポーネントではthisの束縛が複雑
  • bindやアロー関数の理解が必要
  • 初心者には理解が困難
  • コードが長くなりがち

関数コンポーネントの利点

  • thisを一切使わない
  • より直感的で理解しやすい
  • コードが短くなる
  • テストが簡単
  • パフォーマンス最適化が容易

移行のポイント

  • this.stateuseState
  • this.setStatesetCount
  • ライフサイクル → useEffect
  • bindは全て不要

実際の活用例

  • ログインフォーム
  • データ一覧表示
  • ショッピングカート
  • ユーザー管理

関数コンポーネントを使うことで、Reactのthisキーワードの複雑さから完全に解放されます。

初心者の方でも理解しやすく、保守しやすいコードが書けるようになります。

これからReactを学ぶ方へ

最初から関数コンポーネントを使用することを強くおすすめします。 thisの複雑な仕組みを覚える必要がなく、より効率的に学習できますよ。

既存プロジェクトの方へ

段階的にクラスコンポーネントから関数コンポーネントに移行していきましょう。 新しい機能は関数コンポーネントで作成することから始めてみてください。

ぜひ今日から関数コンポーネントを活用して、より快適なReact開発を体験してみてくださいね。

関連記事