Skip to content

Commit f78d8ea

Browse files
committed
Port tree-view scenario
1 parent 4e754da commit f78d8ea

File tree

5 files changed

+279
-0
lines changed

5 files changed

+279
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
export const INCREMENT = 'INCREMENT'
2+
export const CREATE_NODE = 'CREATE_NODE'
3+
export const DELETE_NODE = 'DELETE_NODE'
4+
export const ADD_CHILD = 'ADD_CHILD'
5+
export const REMOVE_CHILD = 'REMOVE_CHILD'
6+
7+
export const increment = (nodeId) => ({
8+
type: INCREMENT,
9+
nodeId,
10+
})
11+
12+
let nextId = 0
13+
export const createNode = () => ({
14+
type: CREATE_NODE,
15+
nodeId: `new_${nextId++}`,
16+
})
17+
18+
export const deleteNode = (nodeId) => ({
19+
type: DELETE_NODE,
20+
nodeId,
21+
})
22+
23+
export const addChild = (nodeId, childId) => ({
24+
type: ADD_CHILD,
25+
nodeId,
26+
childId,
27+
})
28+
29+
export const removeChild = (nodeId, childId) => ({
30+
type: REMOVE_CHILD,
31+
nodeId,
32+
childId,
33+
})
34+
35+
function randomInteger(exclusiveMax) {
36+
return Math.floor(Math.random() * exclusiveMax)
37+
}
38+
39+
function getRandomElement(selector) {
40+
const elements = document.querySelectorAll(selector)
41+
const randomIndex = randomInteger(elements.length)
42+
const element = elements[randomIndex]
43+
return element
44+
}
45+
46+
function clickIncrement() {
47+
const incrementButton = getRandomElement('.increment')
48+
incrementButton.click()
49+
}
50+
51+
function clickAddChild() {
52+
const addChildButton = getRandomElement('.addChild')
53+
addChildButton.click()
54+
}
55+
56+
function clickDeleteNode() {
57+
const deleteNodeButton = getRandomElement('.deleteNode')
58+
deleteNodeButton.click()
59+
}
60+
61+
const odds = [
62+
{ action: clickIncrement, percent: 60 },
63+
{ action: clickAddChild, percent: 30 },
64+
{ action: clickDeleteNode, percent: 10 },
65+
]
66+
67+
export function doRandomAction() {
68+
const randomPercentage = randomInteger(100)
69+
70+
let currentPercentage = 0
71+
for (let entry of odds) {
72+
currentPercentage += entry.percent
73+
74+
if (randomPercentage < currentPercentage) {
75+
entry.action()
76+
break
77+
}
78+
}
79+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react'
2+
import { Component } from 'react'
3+
import { connect } from 'react-redux'
4+
import * as actions from '../actions'
5+
6+
export class Node extends Component {
7+
handleIncrementClick = () => {
8+
const { increment, id } = this.props
9+
increment(id)
10+
}
11+
12+
handleAddChildClick = e => {
13+
e.preventDefault()
14+
15+
const { addChild, createNode, id } = this.props
16+
const childId = createNode().nodeId
17+
addChild(id, childId)
18+
}
19+
20+
handleRemoveClick = e => {
21+
e.preventDefault()
22+
23+
const { removeChild, deleteNode, parentId, id } = this.props
24+
removeChild(parentId, id)
25+
deleteNode(id)
26+
}
27+
28+
renderChild = childId => {
29+
const { id } = this.props
30+
return (
31+
<li key={childId}>
32+
<ConnectedNode id={childId} parentId={id} />
33+
</li>
34+
)
35+
}
36+
37+
render() {
38+
const { counter, parentId, childIds, id } = this.props
39+
return (
40+
<div>
41+
Counter #{id}: {counter}
42+
{' '}
43+
<button className="increment" onClick={this.handleIncrementClick}>
44+
+
45+
</button>
46+
{' '}
47+
{typeof parentId !== 'undefined' &&
48+
<a href="#" className="deleteNode" onClick={this.handleRemoveClick} // eslint-disable-line jsx-a11y/href-no-hash
49+
style={{ color: 'lightgray', textDecoration: 'none' }}>
50+
Delete
51+
</a>
52+
}
53+
<ul>
54+
{childIds.map(this.renderChild)}
55+
<li key="add">
56+
<a href="#" className="addChild" // eslint-disable-line jsx-a11y/href-no-hash
57+
onClick={this.handleAddChildClick}
58+
>
59+
Add child
60+
</a>
61+
</li>
62+
</ul>
63+
</div>
64+
)
65+
}
66+
}
67+
68+
function mapStateToProps(state, ownProps) {
69+
return state[ownProps.id]
70+
}
71+
72+
const ConnectedNode = connect(mapStateToProps, actions)(Node)
73+
export default ConnectedNode
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export default function generateTree(numNodes = 1000) {
2+
let tree = {
3+
0: {
4+
id: 0,
5+
counter: 0,
6+
childIds: []
7+
}
8+
};
9+
10+
for (let i = 1; i < numNodes; i++) {
11+
let parentId = Math.floor(Math.pow(Math.random(), 2) * i);
12+
tree[i] = {
13+
id: i,
14+
counter: 0,
15+
childIds: []
16+
};
17+
tree[parentId].childIds.push(i);
18+
}
19+
20+
return tree;
21+
}

src/scenarios/tree-view/index.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, { useLayoutEffect } from 'react'
2+
import { render } from 'react-dom'
3+
import { Provider } from 'react-redux'
4+
import { createStore, combineReducers, AnyAction } from 'redux'
5+
// @ts-ignore
6+
import seedrandom from 'seedrandom'
7+
8+
import { renderApp } from '../../common'
9+
10+
import reducer from './reducers'
11+
import { doRandomAction } from './actions'
12+
import generateTree from './generateTree'
13+
import Node from './containers/Node'
14+
15+
seedrandom('test seed', { global: true })
16+
17+
const tree = generateTree(5000)
18+
const store = createStore(reducer, tree)
19+
20+
let maxUpdates = 3500,
21+
numUpdates = 0
22+
23+
function runUpdates() {
24+
doRandomAction()
25+
numUpdates++
26+
27+
if (numUpdates < maxUpdates) {
28+
setTimeout(runUpdates, 25)
29+
}
30+
}
31+
32+
const StockTickerApp = () => {
33+
useLayoutEffect(() => {
34+
setTimeout(runUpdates, 250)
35+
}, [])
36+
37+
return <Node id={0} />
38+
}
39+
40+
// @ts-ignore
41+
renderApp(StockTickerApp, store)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { INCREMENT, ADD_CHILD, REMOVE_CHILD, CREATE_NODE, DELETE_NODE } from '../actions'
2+
3+
const childIds = (state, action) => {
4+
switch (action.type) {
5+
case ADD_CHILD:
6+
return [ ...state, action.childId ]
7+
case REMOVE_CHILD:
8+
return state.filter(id => id !== action.childId)
9+
default:
10+
return state
11+
}
12+
}
13+
14+
const node = (state, action) => {
15+
switch (action.type) {
16+
case CREATE_NODE:
17+
return {
18+
id: action.nodeId,
19+
counter: 0,
20+
childIds: []
21+
}
22+
case INCREMENT:
23+
return {
24+
...state,
25+
counter: state.counter + 1
26+
}
27+
case ADD_CHILD:
28+
case REMOVE_CHILD:
29+
return {
30+
...state,
31+
childIds: childIds(state.childIds, action)
32+
}
33+
default:
34+
return state
35+
}
36+
}
37+
38+
const getAllDescendantIds = (state, nodeId) => (
39+
state[nodeId].childIds.reduce((acc, childId) => (
40+
[ ...acc, childId, ...getAllDescendantIds(state, childId) ]
41+
), [])
42+
)
43+
44+
const deleteMany = (state, ids) => {
45+
state = { ...state }
46+
ids.forEach(id => delete state[id])
47+
return state
48+
}
49+
50+
export default (state = {}, action) => {
51+
const { nodeId } = action
52+
if (typeof nodeId === 'undefined') {
53+
return state
54+
}
55+
56+
if (action.type === DELETE_NODE) {
57+
const descendantIds = getAllDescendantIds(state, nodeId)
58+
return deleteMany(state, [ nodeId, ...descendantIds ])
59+
}
60+
61+
return {
62+
...state,
63+
[nodeId]: node(state[nodeId], action)
64+
}
65+
}

0 commit comments

Comments
 (0)