留意点
- 初心者の覚書です。
- 自分の環境で動くように参考にしたコードを適当に修正している。
- Windows10 64bit , PowerShellなどで動かしている。
- 見栄えを若干よくする為にbootstrap4を利用している。
- 解説はアバウトな言い回しで厳密ではない。誤解を含む可能性あり。
Treeのデータ管理
ノードのオブジェクトは
ノード
{
id : "ノードの識別ID",
counter: "カウンターの値(初期値0)",
childIds: "子ノードの配列"
}
のような形式
Treeは idをプロパティ、値を対応するノードオブジェクトにして管理している。id 0 がルートなので、そこから子を辿ると、木構造になる。
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')
)
実行結果
ReduxのサンプルのTree View https://t.co/8qgbCtNcGJ @YouTubeさんから
— tkarasuma (@tkarasuma) 2018年6月15日
【2018/06/16 追記】
GitHubに誤ったソースコードがアップロードされていたのを修正