Search code examples
reactjstypescriptdeck.gl

React & Deck.GL: Add default props to each child component


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.

Attempt 1

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>
);

Attempt 2

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.

Solution?

I can imagine two types of solutions (and obviously, there may be others!):

  • correct method for checking the layer type & modifying child props depending on the type, or something similar - is this possible?

or

  • Some way to convince react/deck.gl that ScatterplotLayer will be a child of Deck.GL, even if it isn't in ScatterplotLayerWrapper. (This one seems less likely)

Solution

  • 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>
        );
    };