Reputation: 34489
I'm trying to create a reusable component, which alters it's behaviour - essentially rendering to either SVG or Canvas. I'm currently trying to do this by wrapping it up in a HoC (however I'm trying to use React hooks) so this is all falling a bit flat on it's face.
Edit with extra info
With the current approach (HoC returning a function) I get the following error:
Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it.
If I remove the function call within the HoC's:
React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object
I've seen an example of converting HoC's to React hooks, but I'm struggling to work out how I might convert this - if it's even possible and wondering if anyone can give me a pointer? I might have tried to architect this the wrong way, as it feels like quite a complex use case.
So I've these 2 HoC's at the moment which I feel like I need to refactor somehow to use hooks called withSVG
and withCanvas
. They setup different DOM to be represented and importantly one has a function called renderLoop
which needs to be called from the Scatter component below.
Slightly quirky, is because the Scatter
is just business logic now (leveraging dependencies in the useEffect), it doesn't actually need to return any DOM, because it's just manipulating the DOM of the parent HoC. I'm expecting many more components like Scatter
in the future however so don't want to move this logic into the HoCs (if they're even the right way of doing this).
const duration = 2000;
const withSVG = ({ width, height, ...props }) => WrappedComponent => {
const layer = useRef(null);
return (props) => (
<g width={width} height={height} ref={layer}>
<WrappedComponent {...props} layer={layer} />
</g>
);
};
const withCanvas = ({ width, height, ...props }) => WrappedComponent => {
const layer = useRef(null);
const canvas = useRef(null);
const renderLoop = getRenderLoop(canvas.current, width, height);
return (props) => (
<React.Fragment>
<custom ref={layer}/>
<canvas width={width} height={height} ref={canvas}></canvas>
<WrappedComponent {...props} layer={layer} renderLoop={renderLoop} />
</React.Fragment>
);
};
const Scatter = ({ data, layer, renderLoop }) => {
useEffect(() => {
if (!layer.current) { return; }
// D3 data join
const join = d3
.select(layer.current)
.selectAll("circle")
.data(data, d => d.key);
// Shrink the circles to 0 size
const exit = join.exit()
.transition("radius")
.duration(duration)
.attr("r", 0)
.remove();
const enter = join.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 0)
.style("fill", d => d.color);
const update = enter
.merge(join)
.transition("radius")
.duration(duration)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 30)
.style("fill", d => d.color);
if (renderLoop) {
renderLoop(exit, update);
}
}, [layer, data]);
return null;
};
const CanvasScatter = withCanvas(Scatter);
const SVGScatter = withSVG(Scatter);
// index.js
const width = 400;
const height = 400;
const App = () => {
const [data, setData] = useState([
{
key: 1,
x: 50,
y: 50,
color: "red"
},
{
key: 2,
x: 150,
y: 150,
color: "blue"
},
]);
setTimeout(() => {
setData([
{
key: 1,
x: 100,
y: 100,
color: "orange"
},
{
key: 3,
x: 150,
y: 50,
color: "green"
},
]);
}, 3000);
return (
<div>
<svg width={width} height={height}>
<SVGScatter width={width} height={height} data={data} />
</svg>
<CanvasScatter width={width} height={height} data={data} />
</div>
);
// return <div>Hello React,Webpack 4 & Babel 7!</div>;
};
ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="root"></div>
Upvotes: 1
Views: 100
Reputation: 202864
I think you have malformed your Higher Order Components. It appears you've defined them to consume some props, then a component to wrap.
const withSVG = ({ width, height, ...props }) => WrappedComponent => {...}
but you invoke them as expected
const SVGScatter = withSVG(Scatter);
Here the Scatter
component is consumed first and the HOC tries to destructure values from it and returns a function to consume a component, which is undefined
. I think this causes the error you are seeing.
Based on how you use the decorated components
<SVGScatter width={width} height={height} data={data} />
<CanvasScatter width={width} height={height} data={data} />
I think you meant to destructure width
and height
from the inner component, the one that is returned by the HOC.
const withSVG = WrappedComponent => ({ width, height, ...props }) => {
const layer = useRef(null);
return (
<g width={width} height={height} ref={layer}>
<WrappedComponent
layer={layer}
{...props} // <-- data passed here
/>
</g>
);
};
const withCanvas = WrappedComponent => ({ width, height, ...props }) => {
const layer = useRef(null);
const canvas = useRef(null);
const renderLoop = getRenderLoop(canvas.current, width, height);
return (
<React.Fragment>
<custom ref={layer}/>
<canvas width={width} height={height} ref={canvas}></canvas>
<WrappedComponent
layer={layer}
renderLoop={renderLoop}
{...props} // <-- data passed here
/>
</React.Fragment>
);
};
Upvotes: 2