I'm working on a configurable set of map layers with Deck.GL & React. I have a BaseMap
component that I'll pass layers of data to as react children.
Currently, I have this:
BaseMap:
export const BaseMap = ({ latitude = 0, longitude = 0, zoom = 4, children }) => {
const deckProps = {
initialViewState: { latitude, longitude, zoom },
controller: true
};
return (
<DeckGL {...deckProps}>
{children}
<StaticMap />
</DeckGL>
);
};
And it's used like this:
<BaseMap>
<ScatterplotLayer
data={scatterData}
getPosition={getPositionFn}
getRadius={1}
radiusUnits={'pixels'}
radiusMinPixels={1}
radiusMaxPixels={100}
filled={true}
getFillColor={[255, 255, 255]}
/>
<TextLayer
data={textData}
getPosition={getPositionFn}
getColor={[255, 0, 0]}
getText={getTextFn}
/>
</BaseMap>
This is okay, but I want to add default props to each child.
I've tried this in BaseMap
, but get the error cannot assign to read only property props of object #<Object>
:
...
return (
<DeckGL {...deckProps}>
{React.Children.map(children, (c) => {
const defaultProps = {
loaders: [CSVLoader]
}
c.props = { ...defaultProps, ...c.props };
return c;
})}
</DeckGL>
);
I've also tried creating a wrapper component for each type of layer, but get the error Cannot call a class as a function
:
wrapper:
export const ScatterplotLayerWrapper = (props) => {
const defaultScatterProps = {
loaders: [CSVLoader]
};
const scatterLayerProps = {
...defaultScatterProps,
...props
};
return <ScatterplotLayer {...scatterLayerProps} />;
};
used like this:
<BaseMap>
<ScatterplotLayerWrapper
data={scatterData}
getPosition={getPositionFn}
/>
</BaseMap>
I suspect the problem with this second attempt has something to do with the caveat here.
I can imagine two types of solutions (and obviously, there may be others!):
or
ScatterplotLayer
will be a child of Deck.GL
, even if it isn't in ScatterplotLayerWrapper
. (This one seems less likely)The confusion came from a mis-understanding of how deck.gl
's React component works & what those <ScatterplotLayer>
components really are (they're not react components).
Deck.gl's react component, DeckGL
, intercepts all children and determines if they are in fact "layers masquerading as react elements" (see code). It then builds layers from each of those "elements" and passes them back to DeckGL
's layers
property.
They look like react components, but really aren't. They can't be rendered on their own in a React context. They can't be rendered outside of the DeckGL
component at all, because they're still just plain deck.gl layers.
The solution here is to create a new map layer class just like you might in any other context (not a React component wrapping a layer). Docs for that are here.
class WrappedTextLayer extends CompositeLayer {
renderLayers() { // a method of `Layer` classes
// special logic here
return [new TextLayer(this.props)];
}
}
WrappedTextLayer.layerName = 'WrappedTextLayer';
WrappedTextLayer.defaultProps = {
getText: (): string => 'x',
getSize: (): number => 32,
getColor: [255, 255, 255]
};
export { WrappedTextLayer };
This new layer can then be used in the BaseMap
component (or the un-wrapped DeckGL
component`) like this:
<BaseMap>
<WrappedTextLayer
data={dataUrl}
getPosition={(d) => [d.longitude, d.latitude]}
/>
</BaseMap>
In addition, the exact same layer can be passed to DeckGL
as a layer prop:
<DeckGL
layers={[
new WrappedTextLayer({
data: dataUrl,
getPosition: (d) => [d.longitude, d.latitude]
})
]}
></DeckGL>
Modifying the BaseMap
component a little will allow it to accept layers either as JSX-like children, or via the layers
prop as well:
export const BaseMap = ({ latitude = 0, longitude = 0, zoom = 4, children, layers }) => {
const deckProps = {
initialViewState: { latitude, longitude, zoom },
controller: true,
layers
};
return (
<DeckGL {...deckProps}>
{children && !layers ? children : null}
<StaticMap />
</DeckGL>
);
};