LoginSignup
4
1

More than 5 years have passed since last update.

ReduxのサンプルのTree View

Last updated at Posted at 2018-06-15

留意点

  • 初心者の覚書です。
  • 自分の環境で動くように参考にしたコードを適当に修正している。
  • Windows10 64bit , PowerShellなどで動かしている。
  • 見栄えを若干よくする為にbootstrap4を利用している。
  • 解説はアバウトな言い回しで厳密ではない。誤解を含む可能性あり。

Treeのデータ管理

ノードのオブジェクトは

ノード
{
id : "ノードの識別ID",
counter: "カウンターの値(初期値0)",
childIds: "子ノードの配列"
}

のような形式

Treeは idをプロパティ、値を対応するノードオブジェクトにして管理している。id 0 がルートなので、そこから子を辿ると、木構造になる。

2018_0615_1434_14.jpg

Treeの初期化

generateTree.js
export default function generateTree() {

  const NUM = 10;

  // treeオブジェクトをルートとなる id 0 のオブジェクトで初期化
  let tree = {
    0: {
      id: 0,
      counter: 0,
      childIds: []
    }
  }

  for (let i = 1; i < NUM ; i++) {
    //i未満の整数をランダムに取り出し
    // 0以上1未満の乱数を二乗することで、上の方に表示されているノードを
    // 優先的に親ノードとしているか
    let parentId = Math.floor(Math.pow(Math.random(), 2) * i)
    // id が i のノード作成してtreeに追加
    tree[i] = {
      id: i,
      counter: 0,
      childIds: []
    }
    //idがparentIdの 子ノードに追加
    tree[parentId].childIds.push(i)
  }

  return tree
}

リデューサ

「//★★★ はじめの初期化のとき」というコメントのところについて。
ここの処理の意味がわからなくていろいろ調べてみたら、どうもページ読み込みのときに一度、
このリデューサが実行されているらしく、そのアクションにnodeIdが存在しないため、undefinedのidにundefinedのオブジェクトが登録されてしまうので必要になるらしいと判明。

reducers\index.js
import { INCREMENT, ADD_CHILD, REMOVE_CHILD, CREATE_NODE, DELETE_NODE } from '../actions'

//ノードのchildIdsの管理
const childIds = (state, action) => {
  switch (action.type) {
    // id追加
    case ADD_CHILD:
      return [...state, action.childId]
      //id削除
    case REMOVE_CHILD:
      return state.filter(id => id !== action.childId)
    default:
      return state
  }
}

const node = (state, action) => {
  switch (action.type) {
    // 新要素作成
    case CREATE_NODE:
      return {
        id: action.nodeId,
        counter: 0,
        childIds: []
      }
    //インクリメント
    case INCREMENT:
      return {
        ...state,
        counter: state.counter + 1
      }
    //それぞれの場合のchildIdsの変更
    case ADD_CHILD:
    case REMOVE_CHILD:
      return {
        ...state,
        childIds: childIds(state.childIds, action)
      }
    default:
      return state
  }
}
// idがnodeIdのオブジェクトより下層の子ノードのidを配列で取得する関数
// 再帰的な処理に注意
//acc は accumulatorの略か。配列のバスケット替わり。
const getAllDescendantIds = (state, nodeId) => (
  state[nodeId].childIds.reduce((acc, childId) => (
    [...acc, childId, ...getAllDescendantIds(state, childId)]
  ), [])
)
// ids配列に入っているidのオブジェクトを一括削除してから、stateを返す関数
const deleteMany = (state, ids) => {
  state = { ...state }
  ids.forEach(id => delete state[id])
  return state
}

export default (state = {}, action) => {
  const { nodeId } = action

  //★★★ はじめの初期化のとき
  //type: "@@redux/INITa.v.f.d.s.g" なるアクションが送られてきて
  // nodeIdは undefined であるため、次の処理がないと、
  //stateのノード管理オブジェクトに undefinedプロパティにundefiendの値が
  // 登録されてしまう。
  if (typeof nodeId === 'undefined') {
    return state
  }
// DELETE_NODE はこっち
  if (action.type === DELETE_NODE) {
    const descendantIds = getAllDescendantIds(state, nodeId)
    //stateから自身と下層のノードを一括削除
    return deleteMany(state, [nodeId, ...descendantIds])
  }
// CREATE_NODE, INCREMENT, ADD_CHILD, REMOVE_CHILD などはこっち
  return {
    ...state,
    [nodeId]: node(state[nodeId], action)
  }

}

アクション

すべてのアクションで nodeIdが使われいる。

actions\index.js
export const INCREMENT = 'INCREMENT'
export const CREATE_NODE = 'CREATE_NODE'
export const DELETE_NODE = 'DELETE_NODE'
export const ADD_CHILD = 'ADD_CHILD'
export const REMOVE_CHILD = 'REMOVE_CHILD'

export const increment = (nodeId) => ({
  type: INCREMENT,
  nodeId
})

let nextId = 0

//ユーザによって新規ノードの追加
export const createNode = () => ({
  type: CREATE_NODE,
  nodeId: `new_${nextId++}`
})
// id指定でノード削除 その子ノードも孫ノードなども下層は再帰的に削除
export const deleteNode = (nodeId) => ({
  type: DELETE_NODE,
  nodeId
})
// nodeIdの親のchildIdsの配列にchildIdを追加
export const addChild = (nodeId, childId) => ({
  type: ADD_CHILD,
  nodeId,
  childId
})
// 子ノード削除 nodeIdの親のchildIdsから childeIdを削除
export const removeChild = (nodeId, childId) => ({
  type: REMOVE_CHILD,
  nodeId,
  childId
})

Nodeコンテナをつくる

NodeをラップにするConnectedNodeコンポーネントには idとparentIdをownPropsとしてわたす。
ただし、ルートはidだけ。
また、Propsには state[ownProps.id] を渡す。
Nodeコンポーネントの内部では、ConnectedNodeコンポーネントを再帰的に呼び出し。

containers\Node.js
import React from 'react'
import { Component } from 'react'
import { connect } from 'react-redux'
import * as actions from '../actions'

export class Node extends Component {
  // カウンターのインクリメントのクリックイベント
  handleIncrementClick = () => {
    const { increment, id } = this.props
    increment(id)
  }
// 子ノード追加のクリックイベント
  handleAddChildClick = e => {
    e.preventDefault()

    const { addChild, createNode, id } = this.props
    const childId = createNode().nodeId
    // 親ノードのchildIdsに追加
    addChild(id, childId)
  }
//親ノードから指定したidの子ノードを削除
  handleRemoveClick = e => {
    e.preventDefault()

    const { removeChild, deleteNode, parentId, id } = this.props
    //自身が属する親ノードのchildIdsから自身のidを削除
    removeChild(parentId, id)
    //自身のオブジェクトを削除
    deleteNode(id)
  }
// 子ノードを表示する関数
// 自身(Node)をラップするConnectedNodeコンポーネントを再帰的に呼び出し
  renderChild = childId => {
    const { id } = this.props
    return (
      <li key={childId}>
        <ConnectedNode id={childId} parentId={id} />
      </li>
    )
  }

  render() {
    const { id, counter, parentId, childIds } = this.props
    return (
      <div>
        <p className={"d-inline-block bg-info text-white  rounded-left"}>🌺{id}</p>
        <p className={"d-inline-block bg-dark text-white mr-1 px-3 rounded-right"}>{counter}</p>
        <button className={'btn btn-primary mr-1 mb-1 p-0'} onClick={this.handleIncrementClick}>
          増加
        </button>
        {typeof parentId !== 'undefined' &&
          <button className={'btn btn-danger mr-1 mb-1 p-0'} onClick={this.handleRemoveClick}>
            ← 要素の削除
          </button>
        }
        <ul>
          {childIds.map(this.renderChild)}
          <li key="add">
            <p className={"d-inline-block alert alert-success p-0 rounded"} style={{cursor:"pointer"}}  onClick={this.handleAddChildClick}>🌺子要素追加</p>
          </li>
        </ul>
      </div>
    )
  }
}

function mapStateToProps(state, ownProps) {
  return state[ownProps.id]
}

const ConnectedNode = connect(mapStateToProps, actions)(Node)
export default ConnectedNode

エントリーポイント

index.js
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import reducer from './reducers'
import generateTree from './generateTree'
import Node from './containers/Node'

const tree = generateTree()
const store = createStore(reducer, tree)


console.log(tree);

store.subscribe(() =>
  console.log(store.getState())
);


render(
  <Provider store={store}>
    <Node id={0} />
  </Provider>,
  document.getElementById('root')
)

実行結果

ソースコード

【2018/06/16 追記】

GitHubに誤ったソースコードがアップロードされていたのを修正

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1