From 17036d2ebdcea2a8c7f884c070a816c0f01d4754 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 16 Jul 2019 12:11:35 +0200 Subject: [PATCH] refactor(state): use useReducer --- src/EdgeRenderer/index.js | 6 +- src/GraphContext/index.js | 100 +++---------------------- src/GraphView/index.js | 23 +++--- src/NodeRenderer/NodeTypes/wrapNode.js | 12 +-- src/NodeRenderer/index.js | 6 +- src/index.js | 6 +- src/state/actions.js | 57 ++++++++++++++ src/state/index.js | 74 ++++++++++++++++++ 8 files changed, 171 insertions(+), 113 deletions(-) create mode 100644 src/state/actions.js create mode 100644 src/state/index.js diff --git a/src/EdgeRenderer/index.js b/src/EdgeRenderer/index.js index c9d4931d..86a70d66 100644 --- a/src/EdgeRenderer/index.js +++ b/src/EdgeRenderer/index.js @@ -34,16 +34,16 @@ class EdgeRenderer extends PureComponent { return ( - {({ transform, edges, nodes }) => ( + {({ state }) => ( - {edges.map(e => renderEdge(e, nodes))} + {state.edges.map(e => renderEdge(e, state.nodes))} )} diff --git a/src/GraphContext/index.js b/src/GraphContext/index.js index d1c4da3a..09513dbd 100644 --- a/src/GraphContext/index.js +++ b/src/GraphContext/index.js @@ -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 { zoomIdentity } from 'd3-zoom'; 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 Provider = (props) => { const { - nodes: initNodes, - edges: initEdges, onNodeClick, children } = props; - const [width, setWidth] = useState(0); - 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 }); + const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { 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) { return Object.assign(propNode, existingNode); } + return propNode; }); - const nodesChanged = !isEqual(nodes, nextNodes); - const edgesChanged = !isEqual(edges, props.edges); + const nodesChanged = !isEqual(state.nodes, nextNodes); + const edgesChanged = !isEqual(state.edges, props.edges); if (nodesChanged) { - setNodes(nextNodes); + dispatch(setNodes(nextNodes)); } 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 = { - width, - height, - updateSize, - d3ZoomState, - initD3ZoomState, - nodes, - setNodes, - getNodes, - edges, - setEdges, - updateNodeData, - updateNodePos, - transform, - updateTransform, onNodeClick, - fitView + state, + dispatch }; return ( diff --git a/src/GraphView/index.js b/src/GraphView/index.js index 4de395f5..627724ca 100644 --- a/src/GraphView/index.js +++ b/src/GraphView/index.js @@ -6,6 +6,7 @@ import ReactSizeMe from 'react-sizeme'; import { GraphContext } from '../GraphContext'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; +import { updateTransform, updateSize, initD3, fitView } from '../state/actions'; const GraphView = (props) => { const zoomNode = useRef(null); @@ -19,42 +20,42 @@ const GraphView = (props) => { return false; } - graphContext.updateTransform(event.transform); + graphContext.dispatch(updateTransform(event.transform)); props.onMove(); }); const selection = select(zoomNode.current).call(zoom); - graphContext.initD3ZoomState({ zoom, selection, initialised: true }); + graphContext.dispatch(initD3({ zoom, selection })); }, []); useEffect( - () => graphContext.updateSize(props.size), + () => graphContext.dispatch(updateSize(props.size)), [props.size.width, props.size.height] ); useEffect(() => { - if (graphContext.d3ZoomState.initialised) { + if (graphContext.state.d3Initialised) { props.onLoad({ - nodes: graphContext.nodes, - edges: graphContext.edges, - fitView: graphContext.fitView + nodes: graphContext.state.nodes, + edges: graphContext.state.edges, + fitView: () => graphContext.dispatch(fitView()) }); } - }, [graphContext.d3ZoomState.initialised]); + }, [graphContext.state.d3Initialised]); useEffect(() => { props.onChange({ - nodes: graphContext.nodes, - edges: graphContext.edges, + nodes: graphContext.state.nodes, + edges: graphContext.state.edges, }); }) return (
- +
); diff --git a/src/NodeRenderer/NodeTypes/wrapNode.js b/src/NodeRenderer/NodeTypes/wrapNode.js index 6c8b72bc..5a71646a 100644 --- a/src/NodeRenderer/NodeTypes/wrapNode.js +++ b/src/NodeRenderer/NodeTypes/wrapNode.js @@ -2,19 +2,20 @@ import React, { useEffect, useRef, useContext } from 'react'; import ReactDraggable from 'react-draggable'; import { GraphContext } from '../../GraphContext'; +import { updateNodeData, updateNodePos } from '../../state/actions'; export default NodeComponent => (props) => { const { position, data, onNodeClick } = props; const { id, __width, __height } = data; const nodeElement = useRef(null); const graphContext = useContext(GraphContext); - const { x, y, k } = graphContext.transform; + const [ x, y, k ] = graphContext.state.transform; useEffect(() => { const bounds = nodeElement.current.getBoundingClientRect(); 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 offsetY = e.clientY - position.y - y; - graphContext.updateNodeData(id, { __offsetX: offsetX, __offsetY: offsetY }); + graphContext.dispatch(updateNodeData(id, { __offsetX: offsetX, __offsetY: offsetY })); }} onDrag={(e, d) => { const { __offsetX = 0, __offsetY = 0 } = data; - graphContext.updateNodePos(id, { + graphContext.dispatch(updateNodePos(id, { x: e.clientX - x - __offsetX, y: e.clientY - y - __offsetY - }); + })); }} scale={k} > @@ -47,7 +48,6 @@ export default NodeComponent => (props) => { ref={nodeElement} style={{ transform: `translate(${position.x}px,${position.y}px)` }} onClick={() => onNodeClick(data)} - // style={{ transform: `translate(${nodePosition.x}px,${nodePosition.y}px) scale(${k})` }} >
diff --git a/src/NodeRenderer/index.js b/src/NodeRenderer/index.js index 69d4cc3f..f10b97ae 100644 --- a/src/NodeRenderer/index.js +++ b/src/NodeRenderer/index.js @@ -25,14 +25,14 @@ class NodeRenderer extends PureComponent { render() { return ( - {({ transform, nodes, onNodeClick }) => ( + {({ onNodeClick, state }) => (
- {nodes.map(d => this.renderNode(d, onNodeClick))} + {state.nodes.map(d => this.renderNode(d, onNodeClick))}
)}
diff --git a/src/index.js b/src/index.js index 9cc90c61..3dc5a514 100644 --- a/src/index.js +++ b/src/index.js @@ -10,11 +10,13 @@ class ReactGraph extends PureComponent { render() { const { style, onNodeClick, children, onLoad, onMove, onChange, elements - } = this.props; + } = this.props; + + const { nodes, edges } = separateElements(elements); return (
- + { + 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 }; +}; diff --git a/src/state/index.js b/src/state/index.js new file mode 100644 index 00000000..660b777f --- /dev/null +++ b/src/state/index.js @@ -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; + } +}; \ No newline at end of file