refactor(state): use useReducer
This commit is contained in:
@@ -34,16 +34,16 @@ class EdgeRenderer extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Consumer>
|
<Consumer>
|
||||||
{({ transform, edges, nodes }) => (
|
{({ state }) => (
|
||||||
<svg
|
<svg
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
className="react-graph__edges"
|
className="react-graph__edges"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
transform={`translate(${transform.x},${transform.y}) scale(${transform.k})`}
|
transform={`translate(${state.transform[0]},${state.transform[1]}) scale(${state.transform[2]})`}
|
||||||
>
|
>
|
||||||
{edges.map(e => renderEdge(e, nodes))}
|
{state.edges.map(e => renderEdge(e, state.nodes))}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+12
-88
@@ -1,123 +1,47 @@
|
|||||||
import React, { createContext, useState, useEffect } from 'react';
|
import React, { createContext, useState, useEffect, useReducer } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { zoomIdentity } from 'd3-zoom';
|
|
||||||
import isEqual from 'lodash.isequal';
|
import isEqual from 'lodash.isequal';
|
||||||
|
|
||||||
import { getBoundingBox } from '../graph-utils';
|
import { reducer, initialState } from '../state';
|
||||||
|
import { setNodes, setEdges } from '../state/actions';
|
||||||
|
|
||||||
export const GraphContext = createContext({});
|
export const GraphContext = createContext({});
|
||||||
|
|
||||||
export const Provider = (props) => {
|
export const Provider = (props) => {
|
||||||
const {
|
const {
|
||||||
nodes: initNodes,
|
|
||||||
edges: initEdges,
|
|
||||||
onNodeClick,
|
onNodeClick,
|
||||||
children
|
children
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [width, setWidth] = useState(0);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const [height, setHeight] = useState(0);
|
|
||||||
const [d3ZoomState, initD3ZoomState] = useState({
|
|
||||||
zoom: null, selection: null, initialised: false
|
|
||||||
});
|
|
||||||
const [nodes, setNodes] = useState(initNodes);
|
|
||||||
const [edges, setEdges] = useState(initEdges);
|
|
||||||
const [transform, setTransform] = useState({ x: 0, y: 0, k: 1 });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextNodes = props.nodes.map(propNode => {
|
const nextNodes = props.nodes.map(propNode => {
|
||||||
const existingNode = nodes.find(n => n.data.id === propNode.data.id);
|
const existingNode = state.nodes.find(n => n.data.id === propNode.data.id);
|
||||||
|
|
||||||
if (existingNode) {
|
if (existingNode) {
|
||||||
return Object.assign(propNode, existingNode);
|
return Object.assign(propNode, existingNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return propNode;
|
return propNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodesChanged = !isEqual(nodes, nextNodes);
|
const nodesChanged = !isEqual(state.nodes, nextNodes);
|
||||||
const edgesChanged = !isEqual(edges, props.edges);
|
const edgesChanged = !isEqual(state.edges, props.edges);
|
||||||
|
|
||||||
if (nodesChanged) {
|
if (nodesChanged) {
|
||||||
setNodes(nextNodes);
|
dispatch(setNodes(nextNodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (edgesChanged) {
|
if (edgesChanged) {
|
||||||
setEdges(props.edges);
|
dispatch(setEdges(props.edges));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateNodeData = (nodeId, updateData) => {
|
|
||||||
const updatedNodes = nodes.map((n) => {
|
|
||||||
if (n.data.id === nodeId) {
|
|
||||||
n.data = {
|
|
||||||
...n.data,
|
|
||||||
...updateData
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
|
|
||||||
setNodes(updatedNodes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateNodePos = (nodeId, pos) => {
|
|
||||||
const updatedNodes = nodes.map((n) => {
|
|
||||||
if (n.data.id === nodeId) {
|
|
||||||
n.position = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
|
|
||||||
setNodes(updatedNodes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTransform = (nextTransform) => {
|
|
||||||
setTransform({
|
|
||||||
k: Math.round(nextTransform.k * 1000) / 1000,
|
|
||||||
x: nextTransform.x,
|
|
||||||
y: nextTransform.y
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fitView = () => {
|
|
||||||
const bounds = getBoundingBox(nodes);
|
|
||||||
const k = Math.min(width, height) / Math.max(bounds.width, bounds.height);
|
|
||||||
const boundsCenterX = bounds.x + (bounds.width / 2);
|
|
||||||
const boundsCenterY = bounds.y + (bounds.height / 2);
|
|
||||||
const translate = [(width / 2) - (boundsCenterX * k), (height / 2) - (boundsCenterY * k)];
|
|
||||||
const initialTransform = zoomIdentity.translate(translate[0], translate[1]).scale(k);
|
|
||||||
|
|
||||||
d3ZoomState.selection.call(d3ZoomState.zoom.transform, initialTransform);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSize = (size) => {
|
|
||||||
setWidth(size.width);
|
|
||||||
setHeight(size.height);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNodes = () => {
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const graphContext = {
|
const graphContext = {
|
||||||
width,
|
|
||||||
height,
|
|
||||||
updateSize,
|
|
||||||
d3ZoomState,
|
|
||||||
initD3ZoomState,
|
|
||||||
nodes,
|
|
||||||
setNodes,
|
|
||||||
getNodes,
|
|
||||||
edges,
|
|
||||||
setEdges,
|
|
||||||
updateNodeData,
|
|
||||||
updateNodePos,
|
|
||||||
transform,
|
|
||||||
updateTransform,
|
|
||||||
onNodeClick,
|
onNodeClick,
|
||||||
fitView
|
state,
|
||||||
|
dispatch
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+12
-11
@@ -6,6 +6,7 @@ import ReactSizeMe from 'react-sizeme';
|
|||||||
import { GraphContext } from '../GraphContext';
|
import { GraphContext } from '../GraphContext';
|
||||||
import NodeRenderer from '../NodeRenderer';
|
import NodeRenderer from '../NodeRenderer';
|
||||||
import EdgeRenderer from '../EdgeRenderer';
|
import EdgeRenderer from '../EdgeRenderer';
|
||||||
|
import { updateTransform, updateSize, initD3, fitView } from '../state/actions';
|
||||||
|
|
||||||
const GraphView = (props) => {
|
const GraphView = (props) => {
|
||||||
const zoomNode = useRef(null);
|
const zoomNode = useRef(null);
|
||||||
@@ -19,42 +20,42 @@ const GraphView = (props) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
graphContext.updateTransform(event.transform);
|
graphContext.dispatch(updateTransform(event.transform));
|
||||||
|
|
||||||
props.onMove();
|
props.onMove();
|
||||||
});
|
});
|
||||||
|
|
||||||
const selection = select(zoomNode.current).call(zoom);
|
const selection = select(zoomNode.current).call(zoom);
|
||||||
|
|
||||||
graphContext.initD3ZoomState({ zoom, selection, initialised: true });
|
graphContext.dispatch(initD3({ zoom, selection }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => graphContext.updateSize(props.size),
|
() => graphContext.dispatch(updateSize(props.size)),
|
||||||
[props.size.width, props.size.height]
|
[props.size.width, props.size.height]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (graphContext.d3ZoomState.initialised) {
|
if (graphContext.state.d3Initialised) {
|
||||||
props.onLoad({
|
props.onLoad({
|
||||||
nodes: graphContext.nodes,
|
nodes: graphContext.state.nodes,
|
||||||
edges: graphContext.edges,
|
edges: graphContext.state.edges,
|
||||||
fitView: graphContext.fitView
|
fitView: () => graphContext.dispatch(fitView())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [graphContext.d3ZoomState.initialised]);
|
}, [graphContext.state.d3Initialised]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.onChange({
|
props.onChange({
|
||||||
nodes: graphContext.nodes,
|
nodes: graphContext.state.nodes,
|
||||||
edges: graphContext.edges,
|
edges: graphContext.state.edges,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="react-graph__renderer">
|
<div className="react-graph__renderer">
|
||||||
<NodeRenderer />
|
<NodeRenderer />
|
||||||
<EdgeRenderer width={graphContext.width} height={graphContext.height} />
|
<EdgeRenderer width={graphContext.state.width} height={graphContext.state.height} />
|
||||||
<div className="react-graph__zoomnode" ref={zoomNode} />
|
<div className="react-graph__zoomnode" ref={zoomNode} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ import React, { useEffect, useRef, useContext } from 'react';
|
|||||||
import ReactDraggable from 'react-draggable';
|
import ReactDraggable from 'react-draggable';
|
||||||
|
|
||||||
import { GraphContext } from '../../GraphContext';
|
import { GraphContext } from '../../GraphContext';
|
||||||
|
import { updateNodeData, updateNodePos } from '../../state/actions';
|
||||||
|
|
||||||
export default NodeComponent => (props) => {
|
export default NodeComponent => (props) => {
|
||||||
const { position, data, onNodeClick } = props;
|
const { position, data, onNodeClick } = props;
|
||||||
const { id, __width, __height } = data;
|
const { id, __width, __height } = data;
|
||||||
const nodeElement = useRef(null);
|
const nodeElement = useRef(null);
|
||||||
const graphContext = useContext(GraphContext);
|
const graphContext = useContext(GraphContext);
|
||||||
const { x, y, k } = graphContext.transform;
|
const [ x, y, k ] = graphContext.state.transform;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bounds = nodeElement.current.getBoundingClientRect();
|
const bounds = nodeElement.current.getBoundingClientRect();
|
||||||
|
|
||||||
if (__width !== bounds.width || __height !== bounds.height) {
|
if (__width !== bounds.width || __height !== bounds.height) {
|
||||||
graphContext.updateNodeData(id, { __width: bounds.width, __height: bounds.height });
|
graphContext.dispatch(updateNodeData(id, { __width: bounds.width, __height: bounds.height }));
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -30,15 +31,15 @@ export default NodeComponent => (props) => {
|
|||||||
const offsetX = e.clientX - position.x - x;
|
const offsetX = e.clientX - position.x - x;
|
||||||
const offsetY = e.clientY - position.y - y;
|
const offsetY = e.clientY - position.y - y;
|
||||||
|
|
||||||
graphContext.updateNodeData(id, { __offsetX: offsetX, __offsetY: offsetY });
|
graphContext.dispatch(updateNodeData(id, { __offsetX: offsetX, __offsetY: offsetY }));
|
||||||
}}
|
}}
|
||||||
onDrag={(e, d) => {
|
onDrag={(e, d) => {
|
||||||
const { __offsetX = 0, __offsetY = 0 } = data;
|
const { __offsetX = 0, __offsetY = 0 } = data;
|
||||||
|
|
||||||
graphContext.updateNodePos(id, {
|
graphContext.dispatch(updateNodePos(id, {
|
||||||
x: e.clientX - x - __offsetX,
|
x: e.clientX - x - __offsetX,
|
||||||
y: e.clientY - y - __offsetY
|
y: e.clientY - y - __offsetY
|
||||||
});
|
}));
|
||||||
}}
|
}}
|
||||||
scale={k}
|
scale={k}
|
||||||
>
|
>
|
||||||
@@ -47,7 +48,6 @@ export default NodeComponent => (props) => {
|
|||||||
ref={nodeElement}
|
ref={nodeElement}
|
||||||
style={{ transform: `translate(${position.x}px,${position.y}px)` }}
|
style={{ transform: `translate(${position.x}px,${position.y}px)` }}
|
||||||
onClick={() => onNodeClick(data)}
|
onClick={() => onNodeClick(data)}
|
||||||
// style={{ transform: `translate(${nodePosition.x}px,${nodePosition.y}px) scale(${k})` }}
|
|
||||||
>
|
>
|
||||||
<NodeComponent {...props} />
|
<NodeComponent {...props} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ class NodeRenderer extends PureComponent {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Consumer>
|
<Consumer>
|
||||||
{({ transform, nodes, onNodeClick }) => (
|
{({ onNodeClick, state }) => (
|
||||||
<div
|
<div
|
||||||
className="react-graph__nodes"
|
className="react-graph__nodes"
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${transform.x}px,${transform.y}px) scale(${transform.k})`
|
transform: `translate(${state.transform[0]}px,${state.transform[1]}px) scale(${state.transform[2]})`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{nodes.map(d => this.renderNode(d, onNodeClick))}
|
{state.nodes.map(d => this.renderNode(d, onNodeClick))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Consumer>
|
</Consumer>
|
||||||
|
|||||||
+4
-2
@@ -10,11 +10,13 @@ class ReactGraph extends PureComponent {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
style, onNodeClick, children, onLoad, onMove, onChange, elements
|
style, onNodeClick, children, onLoad, onMove, onChange, elements
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const { nodes, edges } = separateElements(elements);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style} className="react-graph">
|
<div style={style} className="react-graph">
|
||||||
<Provider {...separateElements(elements)} onNodeClick={onNodeClick}>
|
<Provider nodes={nodes} edges={edges} onNodeClick={onNodeClick}>
|
||||||
<GraphView
|
<GraphView
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
onMove={onMove}
|
onMove={onMove}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
UPDATE_TRANSFORM, UPDATE_SIZE, SET_NODES, SET_EDGES,
|
||||||
|
UPDATE_NODE_DATA, UPDATE_NODE_POS, INIT_D3, FIT_VIEW
|
||||||
|
} from './index';
|
||||||
|
|
||||||
|
export const updateTransform = (transform) => {
|
||||||
|
return {
|
||||||
|
type: UPDATE_TRANSFORM,
|
||||||
|
payload: { transform: [transform.x, transform.y, transform.k] }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSize = (size) => {
|
||||||
|
return {
|
||||||
|
type: UPDATE_SIZE,
|
||||||
|
payload: size
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setNodes = (nodes) => {
|
||||||
|
return {
|
||||||
|
type: SET_NODES,
|
||||||
|
payload: { nodes }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setEdges = (edges) => {
|
||||||
|
return {
|
||||||
|
type: SET_EDGES,
|
||||||
|
payload: { edges }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateNodeData = (id, data) => {
|
||||||
|
return {
|
||||||
|
type: UPDATE_NODE_DATA,
|
||||||
|
payload: { id, data }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateNodePos = (id, pos) => {
|
||||||
|
return {
|
||||||
|
type: UPDATE_NODE_POS,
|
||||||
|
payload: { id, pos }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initD3 = ({ zoom, selection }) => {
|
||||||
|
return {
|
||||||
|
type: INIT_D3,
|
||||||
|
payload: { d3Zoom: zoom, d3Selection: selection, d3Initialised: true }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fitView = () => {
|
||||||
|
return { type: FIT_VIEW };
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { zoomIdentity } from 'd3-zoom';
|
||||||
|
import { getBoundingBox } from '../graph-utils';
|
||||||
|
|
||||||
|
export const SET_EDGES = 'SET_EDGES';
|
||||||
|
export const SET_NODES = 'SET_NODES';
|
||||||
|
export const UPDATE_NODE_DATA = 'UPDATE_NODE_DATA';
|
||||||
|
export const UPDATE_NODE_POS = 'UPDATE_NODE_POS';
|
||||||
|
export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM';
|
||||||
|
export const UPDATE_SIZE = 'UPDATE_SIZE';
|
||||||
|
export const INIT_D3 = 'INIT_D3';
|
||||||
|
export const FIT_VIEW = 'FIT_VIEW';
|
||||||
|
|
||||||
|
export const initialState = {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
transform: [0, 0, 1],
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
|
||||||
|
d3Zoom: null,
|
||||||
|
d3Selection: null,
|
||||||
|
d3Initialised: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case UPDATE_NODE_DATA: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nodes: state.nodes.map((n) => {
|
||||||
|
if (n.data.id === action.payload.id) {
|
||||||
|
n.data = {
|
||||||
|
...n.data,
|
||||||
|
...action.payload.data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case UPDATE_NODE_POS: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
nodes: state.nodes.map((n) => {
|
||||||
|
if (n.data.id === action.payload.id) {
|
||||||
|
n.position = action.payload.pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case FIT_VIEW: {
|
||||||
|
const bounds = getBoundingBox(state.nodes);
|
||||||
|
const k = Math.min(state.width, state.height) / Math.max(bounds.width, bounds.height);
|
||||||
|
const boundsCenterX = bounds.x + (bounds.width / 2);
|
||||||
|
const boundsCenterY = bounds.y + (bounds.height / 2);
|
||||||
|
const translate = [(state.width / 2) - (boundsCenterX * k), (state.height / 2) - (boundsCenterY * k)];
|
||||||
|
const initialTransform = zoomIdentity.translate(translate[0], translate[1]).scale(k);
|
||||||
|
|
||||||
|
state.d3Selection.call(state.d3Zoom.transform, initialTransform);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
case SET_NODES:
|
||||||
|
case SET_EDGES:
|
||||||
|
case UPDATE_TRANSFORM:
|
||||||
|
case INIT_D3:
|
||||||
|
case UPDATE_SIZE:
|
||||||
|
return { ...state, ...action.payload };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user