質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.81%

React / Reduxのカウンターコンポーネントを状態を分けて複製したい

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 557

ludient

score 20

経緯

ReactとReduxを使って状態管理できるカウンターコンポーネントを作りました。
このコンポーネントを複製して、カウンターを2つ表示し、それぞれカウント値を別々に保持したいと思ったのですが、単純にjsxにカウンターコンポーネントのを2つ記載しただけでは、2つのカウンターの数値が同期してしまい(Storeから同じ値を参照していると思うので)ただカウンターが2つ表示されただけ、という状態になってしまいました。

単純にコンポーネントやreducerなどをコピーして別の名前で定義すればできるのはわかるのですが、コンポーネントのマークアップや、カウンターのロジックは全く同じなのに、ファイルが倍になっていくそのやりかたは絶対にベストでないことは自明なのですが、こういう場合の最適な処理がわからず困っています。

知りたいこと

今回の目的としては、それぞれの独立した値を持つカウンターを2つ表示したいのです。
このようなことをしたい場合は、コードのどの部分を変更(ないしは複製?)するのが最適なのでしょうか?

また、そもそもこう書いてるのおかしいよ。みたいな箇所があれば指摘いただけると幸いです。

コード

// src/index.js

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import CounterContainer from './containers/Counter';
import reducer from './reducers'

const store = createStore(reducer);

render(
  <Provider store={store}>
    <CounterContainer />
    {/* ここにもう一つカウンターを設置したい */}
  </Provider>,
  document.getElementById('root')
);
// src/components/Counter.js

import React from 'react';

const Counter = ({ count, increment, decrement }) => {
  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </div>
  );
};

export default Counter
// src/containers/Counter.js

import Counter from '../components/Counter'
import { onIncrement, onDecrement } from '../actions'
import { connect } from 'react-redux';

const mapStateToProps = (state) => ({
    count: state.counterReducer.count
})

const mapDispatchToProps = (dispatch) => ({
  increment: () => dispatch(onIncrement()),
  decrement: () => dispatch(onDecrement())
})

export default connect(mapStateToProps, mapDispatchToProps)(Counter)
// src/action.index.js

export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

export const onIncrement = () => {
  return {
    type: INCREMENT
  }
}

export const onDecrement = () => {
  return {
    type: DECREMENT
  }
}
// src/reducers/counter.js

import { INCREMENT, DECREMENT } from '../actions'

export const counterReducer = (state = {count: 0}, action) => {
  switch (action.type) {
    case INCREMENT:
      return {
        ...state,
        count: state.count + 1
      }
    case DECREMENT:
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state
  }
}
export default counterReducer
// src/reducers/index.js

import { combineReducers } from 'redux'
import { counterReducer } from './counter'

const reducer = combineReducers({
  counterReducer,
})

export default reducer
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • jun68ykt

    2019/08/20 23:56

    こちらのご質問に回答しておりますが、いかがでしょうか? 回答にご不明な点などあれば、コメントからお知らせください。

    キャンセル

回答 1

checkベストアンサー

+1

こんにちは

今回の目的としては、それぞれの独立した値を持つカウンターを2つ表示したいのです。
このようなことをしたい場合は、コードのどの部分を変更(ないしは複製?)するのが最適なのでしょうか?

以下、最適な解かどうかは分かりませんが、一例として回答します。

1. カウンターを区別するためのprop追加

まず、個別のカウント値を参照、更新するカウンターを、コンポーネントではどのように区別するのかを考えます。この回答では、以下のように、CounterContainer に name というpropを追加することにします。

<CounterContainer name="foo" />
<CounterContainer name="bar" />

2. 複数のカウンターを保持できるようにredux state を修正

次に、上記の 1. で追加した prop nameごとに個別のカウント値を保持するように redux state を修正します。たとえば、name が "foo" のカウンターが 2、 "bar" のカウンターが 5 であるときの state をどのような形にすればよいかという問題もいろいろ案がありそうですが、ここでは単純に、以下の形式のオブジェクトで持つことにします。

{ foo: 2, bar: 5 }

この場合、初期状態は以下の2つが考えられます。

(1) foo と bar をプロパティとして持たせ、初期値を明示的に記載する。

const initialState = { foo: 0, bar: 0 }


または、
(2) 空オブジェクト

const initialState = {}

考え方の違いでいうと、(1) は、 redux state のプロパティに無いような name (たとえば name="bazz" )のカウンターは無効なものとする(画面上では、ボタンを押せないようにする等)という考え方の案です。(2) は、redux state からは、カウンターの name prop に与える値に特に制限を設けないという考え方の案です。

どうちらもありな方向性ですが、ここでは (2) で進めます。

3. アクションクリエータの修正

アクションクリエータは onIncrement 、 onDecrementともに、引数として name を取り、これをアクションの中に持つように修正します。

export const onIncrement = name => {
  return {
    type: INCREMENT,
    name
  }
}

export const onDecrement = name => {
  return {
    type: DECREMENT,
    name
  }
}

4. リデューサーの修正

state に含まれる action.name の値を変更するように修正します。

    case INCREMENT:
      return {
        ...state,
        [action.name]: (state[action.name] || 0) + 1
      }
    case DECREMENT:
      return {
        ...state,
        [action.name]: (state[action.name] || 0) - 1
      }

上記で、 state[action.name] || 0 としているのは、初めは state[action.name] はundefined になり、 undefined に1を足したり、1を引いたりすると NaN になってしまうための対応です。言い換えると、 state[action.name] || 0  は、どんな name に対しても、カウンターの初期値は 0 であることを表しています。

5. コンテナの修正

次に React 側の修正です。まず、コンテナを作るときの mapStateToProps で、 name prop に対応したカウント値が返されるようにします。

const mapStateToProps = (state, ownProps) => ({
  count: state.counterReducer[ownProps.name]
})

mapDispatchToProps のほうは、increment と decrement を、引数 name を受け取ってアクションクリエータに渡すように修正します。

const mapDispatchToProps = (dispatch) => ({
  increment: name => dispatch(onIncrement(name)),
  decrement: name => dispatch(onDecrement(name))
})

6. カウンターコンポーネントの修正

受け取るpropsに name を追加し、 初期表示のときは mapStateToProps によって count には undefined が渡されてくるので、初期値の指定を追加します。

const Counter = ({ name, count=0, increment, decrement }) => {

+1ボタンと -1 ボタンのクリックハンドラで、 increment と decrement に引数として name を渡すように修正します。

<button onClick={() => { increment(name) }}>+1</button>
<button onClick={() => { decrement(name) }}>-1</button>

以上の修正で、とりあえず意図どおり、個別のカウント値を持つように CounterContainerを書けるようになったので、以下で試します。

render(
  <Provider store={store}>
    <CounterContainer name="foo" />
    <CounterContainer name="bar" />
  </Provider>,
  document.getElementById('root')
);

上記の修正手順 1.〜 6. の中での考えどころは、1.と 2. です。1.と2. が決まればその後のコード修正は(多くのファイルに手を入れなければならないものの)、やることの見えている作業レベルという感じですが、3.以降の作業のどこかで、redux state の設計がまずかったために、それが後々の作業を面倒にしていることに、作り始めてから気がつくことは、ままあります。その場合は 2. または 2.をやろうとした発端になった 1. にまで戻って見直します。

動作確認用のコード

上記の修正後のものを動作確認するため、作成したものを以下に上げました。

以下の手順で動かすことができると思います。

  • git clone https://github.com/jun68ykt/q206957.git
  • cd q206957
  • yarn install
  • yarn start

上記のレポジトリで、最初のコミットでは、ご質問に掲載のコードをコピペして各ファイルを作成して、その後の数回のコミットで、カウンターの複数対応の修正をしています。

以上、参考になれば幸いです。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/26 12:00

    完璧な回答をいただいていたのにレスが遅くなってしまい大変申し訳ありませんでした。
    知りたかったことがすべて書かれていて本当に目から鱗です。
    nameなどのpropsで場合分けするというところまでは何となくそんなそういうことをするべきなのかなという考えはあったのですが、実際にコードを見てみるとなるほどと思うことばかりです。
    特に、手順2の考え方に関する記述や、4のundefinedへの対策は本当に参考になりました。

    サンプルコードまで添付いただいて本当にありがとうございました!

    キャンセル

  • 2019/08/26 17:07

    どういたしまして!
    お役に立てたようで、よかったです。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.81%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る