state のリフトアップ - React
ユーザインターフェース構築のための JavaScript ライブラリ
https://ja.legacy.reactjs.org/docs/lifting-state-up.html
はじめに
公式ドキュメントを参照してみる
状態管理とコンポーネント設計の課題
状態のリフトアップ(Lifting State Up)とは
なぜリフトアップが必要なのか
状態のリフトアップを使った実装例
アプリケーションの構成
コード例
親コンポーネント:App
検索バーコンポーネント:SearchBar
フィルターコンポーネント:Filter
リスト表示コンポーネント:ItemList
実装のポイント
サンプルコードにおける状態のリフトアップの適用
なぜこの設計が効果的か
状態のリフトアップによるメリットまとめ
コンポーネント設計の重要性
単一責任の原則
プレゼンテーションとロジックの分離
状態管理を親コンポーネントに集約する必要性
まとめ
おわりに
Reactを使ってアプリケーションを開発していると、コンポーネント間でデータを共有する必要が出てくることがあります。特に、複数のコンポーネントが同じデータに依存している場合、そのデータの管理方法が重要になります。
この記事では、状態のリフトアップ(Lifting State Up)というReactの基本概念と、コンポーネント設計における状態管理の重要性について、自分の学習経験をもとにまとめてみたいと思います。実際のコード例も交えながら、これらの概念を理解していきましょう。
しばしば、いくつかのコンポーネントが同一の変化するデータを反映する必要がある場合があります。そんなときは最も近い共通の祖先コンポーネントへ共有されている state をリフトアップすることを推奨します。
公式ドキュメントではこのように記述されています。
Reactでは、状態(State)はコンポーネント内で管理されます。しかし、複数のコンポーネントが同じ状態に依存している場合、どのように状態を共有すればよいのでしょうか。
例えば、以下のようなシナリオを考えてみます。
この場合、検索バーやフィルタリング用のコンポーネントと、リスト表示用のコンポーネントがそれぞれ別々に存在しますが、同じデータ(検索キーワードやフィルタ条件)を必要とします。
このような状況で有効なのが、状態のリフトアップという考え方です。状態のリフトアップとは、共通の状態を最も近い共通の親コンポーネントに移動させることで、複数の子コンポーネント間で状態を共有する方法です。
それでは、具体的なコード例を通して、状態のリフトアップを理解していきましょう。
App
)SearchBar
)Filter
)ItemList
)App
// App.js
import React from 'react'
import SearchBar from './SearchBar'
import Filter from './Filter'
import ItemList from './ItemList'
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
searchQuery: '',
selectedFilters: [],
items: [
{ id: 1, name: 'リンゴ', category: 'フルーツ' },
{ id: 2, name: 'バナナ', category: 'フルーツ' },
{ id: 3, name: 'キャベツ', category: '野菜' },
{ id: 4, name: 'ニンジン', category: '野菜' },
{ id: 5, name: 'チーズ', category: '乳製品' },
{ id: 6, name: 'ヨーグルト', category: '乳製品' },
]
}
}
setSearchQuery = (query) => {
this.setState({ searchQuery: query })
}
setSelectedFilters = (filters) => {
this.setState({ selectedFilters: filters })
}
render() {
const filteredItems = this.state.items.filter(item => {
const matchesSearch = item.name.toLowerCase().includes(this.state.searchQuery.toLowerCase())
const matchesFilter =
this.state.selectedFilters.length === 0 || this.state.selectedFilters.includes(item.category)
return matchesSearch && matchesFilter
})
return (
<div style={{ padding: '20px' }}>
<h1>商品一覧</h1>
<SearchBar
searchQuery={this.state.searchQuery}
setSearchQuery={this.setSearchQuery}
/>
<Filter
selectedFilters={this.state.selectedFilters}
setSelectedFilters={this.setSelectedFilters}
/>
<ItemList items={filteredItems} />
</div>
)
}
}
export default App;
SearchBar
// SearchBar.js
import React from 'react'
class SearchBar extends React.Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
handleChange(e) {
this.props.setSearchQuery(e.target.value)
}
render() {
return (
<input
type="text"
placeholder="検索キーワードを入力"
value={this.props.searchQuery}
onChange={this.handleChange}
style={{ marginBottom: '10px', padding: '5px' }}
/>
)
}
}
export default SearchBar
Filter
// Filter.js
import React from 'react'
class Filter extends React.Component {
categories = ['フルーツ', '野菜', '乳製品']
handleCheckboxChange (category) {
const { selectedFilters, setSelectedFilters } = this.props;
if (selectedFilters.includes(category)) {
setSelectedFilters(selectedFilters.filter(item => item !== category));
} else {
setSelectedFilters([...selectedFilters, category])
}
}
render() {
return (
<div style={{ marginBottom: '10px' }}>
{this.categories.map(category => (
<label key={category} style={{ marginRight: '10px' }}>
<input
type="checkbox"
checked={this.props.selectedFilters.includes(category)}
onChange={() => this.handleCheckboxChange(category)}
/>
{category}
</label>
))}
</div>
)
}
}
export default Filter
ItemList
// ItemList.js
import React from 'react'
class ItemList extends React.Component {
render() {
const { items } = this.props
if (items.length === 0) {
return <p>該当する商品がありません。</p>
}
return (
<ul style={{ listStyleType: 'none', padding: 0 }}>
{items.map(item => (
<li key={item.id} style={{ marginBottom: '5px' }}>
{item.name} ({item.category})
</li>
))}
</ul>
)
}
}
export default ItemList
これらのコードをファイルに記載するとブラウザ上では以下のように表示されます。
*実装の際はcodesandboxというツールを使用しました。
App
で状態を管理:searchQuery
とselectedFilters
という状態を親コンポーネントで管理しています。SearchBar
とFilter
に状態と状態を更新する関数を渡しています。ItemList
に渡しています。このサンプルコードでは、状態のリフトアップを以下のように適用しています:
App
コンポーネントでsearchQuery
とselectedFilters
の状態を定義SearchBar
とFilter
)に渡すSearchBar
とFilter
は自身で状態を持たず、親から渡されたprops
を使用App
コンポーネント内で状態を使用してアイテムをフィルタリングItemList
コンポーネントに渡して表示App
コンポーネントで一元管理されているため、アプリケーション全体で一貫したデータを使用できます。App
コンポーネントで行われ、その状態が子コンポーネントに渡されるという明確な流れがあります。これにより、データの変更と影響を追跡しやすくなります。SearchBar
、Filter
、ItemList
の各コンポーネントは、内部で状態を持たないため、異なる状況や他のプロジェクトでも再利用しやすくなっています。App
コンポーネントに集中しているため、デバッグやテストが行いやすくなります。Reactでは、コンポーネントの設計がアプリケーションの品質に大きく影響します。状態のリフトアップを活用しつつ、以下のポイントを意識すると良いでしょう。
各コンポーネントは一つの責任を持つように設計します。これにより、コンポーネントの再利用性が高まり、テストもしやすくなります。
今回の例では、SearchBar
、Filter
、ItemList
がプレゼンテーションコンポーネントに相当し、App
がコンテナコンポーネントとなっています。
私自身、実務で開発を進める中で、状態管理が各コンポーネントに分散していると、データの同期やバグの原因追跡が難しくなることを実感しました。状態を親コンポーネントに集約することで、これらの問題を解決でき、開発効率が向上しました。
Reactにおける状態管理とコンポーネント設計の重要性について、自分の学習経験をもとにまとめてみました。状態のリフトアップはReactの基本的な概念ですが、実際に手を動かして実装してみると、その効果を実感できます。
この記事が、同じようにReactを学んでいる方や、状態管理に悩んでいる方の参考になれば幸いです。
READY TO FASHIONでは一緒に働くエンジニアを募集しています!
ファッションに興味があるエンジニアのご応募お待ちしております!