Pass redux store to react-dom/server's renderToString() method

Viewed 659

I have a regular React app (CRA). In one part of the app I use Mapbox to display a map, and to render a popup I need to pass in a string of the html content to render. Instead of passing in the raw html, I still want to use React to render the popup, so I do this:

const renderedContent = renderToString(
    <Popup
      port={port}
      poiType={poiType}
      activeView={activeView}
      isPortsOnly={isPortsOnly}
      locations={locations}
    />
  )

Get the html string from rendering the popup component. This is still client side, the docs says it's fine to use renderToString() for these use cases on client side as well.


new Popup()
  .setLngLat(coordinates)
  .setHTML(renderedContent)
  .addTo(self.map)

Create a popup with the html string.


This works as expected. Now my problem: I wan't to use redux from within my Popup component. Both dispatch an event, and read the global state. How can I do this? Can I actually do this?


What I've tried

If I try to just use redux within the Popup component, I get the following error message:

Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>

2 Answers

Well this was interesting! I've made a sandbox with the following code, scroll for some explanations:

Popup.js

import React, { useRef, useEffect } from "react";
import { connect } from "react-redux";
import { Popup } from "mapbox-gl";

import { zoomAction } from "./ActionCreators";

const CustomPopup = ({ zoomAction, lng, lat, zoom, map }) => {
  const popUpRef = useRef(null);

  useEffect(() => {
    if (popUpRef.current && map) new Popup().setLngLat([lng, lat]).setDOMContent(popUpRef.current).addTo(map);
  }, [popUpRef, map, lng, lat]);

  const zoomIn = () => {
    zoomAction();
    map.zoomIn(zoom);
  };

  return (
    <div ref={popUpRef}>
      <h1>Hello World!</h1>
      <button onClick={zoomIn}>zoom in (currently {zoom})</button>
    </div>
  );
};

const mapStateToProps = (state) => {
  return { ...state };
};

const mapDispatchToProps = { zoomAction };

export default connect(mapStateToProps, mapDispatchToProps)(CustomPopup);

MapGl.js

import React, { useMemo, useState } from "react";
import MapboxGl from "mapbox-gl";
import { connect } from "react-redux";

import "mapbox-gl/dist/mapbox-gl.css";
import "./MapGl.css";

import Popup from "./Popup";

MapboxGl.accessToken = "pk.eyJ1IjoibmljZXk3MjMxMSIsImEiOiJja2hrdGE4MHkwMGxxMndteDRucGIyMDVqIn0.MKokCfCrE8VskLRUNGO0hw";

const MapGl = ({ zoom, lng, lat }) => {
  const [isLoaded, setIsLoaded] = useState(false);
  const map = useMemo(() => {
    if (isLoaded)
      return new MapboxGl.Map({
        container: document.querySelector(".mapContainer"),
        style: "mapbox://styles/mapbox/streets-v11",
        center: [lng, lat],
        zoom: zoom
      });
  }, [isLoaded]);

  return (
    <div>
      <div ref={() => setIsLoaded(true)} className="mapContainer" />
      {map && <Popup map={map} />}
    </div>
  );
};

const mapStateToProps = (state) => {
  return { ...state };
};

export default connect(mapStateToProps)(MapGl);

MapGl.css

.mapContainer {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
}

App.js

import React from "react";

import "./App.css";

import MapGl from "./MapGl";

export default () => {
  return <MapGl />;
};

Index.js

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";

import "./index.css";

import store from "./Store";
import App from "./App";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Store.js

import { createStore } from "redux";

const initialState = {
  lng: -96,
  lat: 37.8,
  zoom: 3
};

function rootReducer(state = initialState, action) {
  switch (action.type) {
    case "zoom":
      return { ...state, zoom: state.zoom + 1 };
    default:
      return state;
  }
}

let store = createStore(rootReducer);

export default store;

ActionCreators.js

export const zoomAction = () => ({
  type: "zoom"
});

First thing is to replace the setHtml with setDOMContent which takes an existing DOM node instead of a string. Secondly the use of renderToString is tied to server-side rendering which is situational. In this example I make a popup with a button that dispatches a zoom action to update the store as well as syncing the map object itself. You can see the new zoom value applied to the map and the store value updated inside the popup.

Try to create a function in place where your Popup controller is, which dispatches something and pass it to that component. I mean something like:

const doSomething = () => {
  dispatch(do());
};

const renderedContent = renderToString(
  <Popup
    port={port}
    poiType={poiType}
    activeView={activeView}
    isPortsOnly={isPortsOnly}
    locations={locations}
    onDoSomething={doSomething}
  />
)
Related