2

I am refactoring an existing react-leaflet application that is written with NextJS and redux. I am introducing redux-sagas to clean up the api calling logic. In some of the sagas, I would like to use some leaflet geo functions:

function* handleGeocode(): Generator {
    if (window) {
        try {
            const response = yield call(axios.get, `nominatim-url-blah-blah`);

            const { lat, lon: lng } = response[0];

            const bounds = L.latLngBounds([ // Use leaflet function here
                [lat - 0.5, lng - 0.5],
                [lat + 0.5, lng + 0.5],
            ]);

                
            yield put(
                ActionCreators.requestDataInBounds(bounds))
            );
            
        } catch (e) { }
    }
}

function* watchHandleGeocode() {
    yield takeEvery(ActionTypes.REQ_GEOCODE, handleGeocode);
}

function* searchSagas() {
    yield all([fork(watchHandleGeocode),
}

The use of L.latLngBounds runs some leaflet code, which deep down, eventually needs access to the window object.

I am getting the error ReferenceError: window is not defined.

I know this has been an issue with react-leaflet before. I've read the question Leaflet with next.js, but that is not my issue. My actual Map component is already being imported using dynamic:

const Map = dynamic(() => import("../components/Map"), {
    ssr: false,
});

I am not having an issue with this. My issue arises as soon as I introduce leaflet-native code into the sagas. Its alost as if the sagas are all being run as soon as the saga middleware is used:

const sagaMiddleware = createSagaMiddleware();

function initStore(preloadedState) {
    const store = createStore(
        rootReducer,
        preloadedState,
        composeWithDevTools(applyMiddleware(sagaMiddleware))
    );

    return store;
}

export const initializeStore = (preloadedState = {}) => {
    let _store = store ?? initStore(preloadedState);

    // After navigating to a page with an initial Redux state, merge that state
    // with the current state in the store, and create a new store
    if (preloadedState && store) {
        _store = initStore({
            ...store.getState(),
            ...preloadedState,
        });
        // Reset the current store
        store = undefined;
    }

    // For SSG and SSR always create a new store
    if (typeof window === "undefined") return _store;

    if (typeof window !== "undefined") {
        sagaMiddleware.run(rootSaga);
    }

    // Create the store once in the client
    if (!store) store = _store;

    return _store;
};

export function useStore(initialState) {
    const store = useMemo(() => initializeStore(initialState), [initialState]);
    return store;
}

So despite my only running the sagaMiddleware if window !== "undefined", and the saga itself being wrapped inside of a if (window) statment, I am still getting this error. Its as if the code is being run and running the L.latLngBounds function, even though the action that calls the handleGeocode saga is not even currently being called anywhere in the code! As soon as I remove the call to L.latLngBounds, the error goes away.

What is going wrong here? How does NextJS run this code and even reach this type of error, if the action which runs this saga is not even being called? How can I rewire my sagas so that I can use native leaflet functions within them?

Seth Lutske
  • 9,154
  • 5
  • 29
  • 78
  • If I understand correctly, your `handleGeocode` function is totally separate from your `Map` component. It also imports Leaflet, but you try to guard its usage for client side only, by checking for `window`. But how is it imported? – ghybs Feb 15 '22 at 02:42
  • 1
    Maybe try to avoid using leaflet code inside the sagas and dispatch only lat lng or any other data received from async calls. Then inside the component(s) you want to consume them, use there the leaflet code to form the bounds using lat lng received from redux. – kboul Feb 15 '22 at 15:41
  • @kboul I was thinking about doing that, and I may have to. But being able to include some processing logic within the saga, which takes advantage of leaflet's great geo functions, would be really useful. I guess I don't understand why window is trying to be accessed even before this code sent to the client – Seth Lutske Feb 15 '22 at 18:23
  • @ghybs, it does not get imported anywhere. We link all redux sagas to the store by using the saga middleware, `applyMiddleware(sagaMiddleware)`, and the running that middleware, `sagaMiddleware.run(rootSaga)`, where rootSaga essentially contains all sagas in the app. One of those sagas is `watchHandleGeocode`, which listens for `ActionTypes.REQ_GEOCODE` to be fired from anywhere in the application. Sagas listen for that, then runs the code in `handleGeocode`. What I don't understand is why L.latLngBounds is being called at all at *compilation* time, instead of only once the action fires – Seth Lutske Feb 15 '22 at 18:30
  • @SethLutske Is it possible its not a javascript error, but a typescript error? Which would mean the code is not actually being run, its just the TS static analysis during compilation? – Martin Kadlec Feb 17 '22 at 07:36
  • @MartinKadlec, this seems unlikely. Its not my own typescript code that's erroring, but rather the internal leaflet code way down deep underneath a call to `L.latLngBounds`. Not sure what that would have to do with TS, or why TS would be upset about accessing the window object. Besides, my current set is very JS-oriented, compilation currently allows even the most heinous TS errors. – Seth Lutske Feb 17 '22 at 14:24
  • Try to replace `if (window)` with codition `if (typeof window !== undefined)` in `handleGeocode` – Fiodorov Andrei Feb 23 '22 at 11:42
  • @FiodorovAndrei, I have tried that, no change – Seth Lutske Feb 23 '22 at 14:28

1 Answers1

0

I found a way to do this - I'll drop this here in the rare event that someone else runs into this issue. In any saga that needs to call leaflet L.something methods, you can't import leaflet directly. However, you can pull this trick:

let L;

if (typeof window !== "undefined") {
    L = require("leaflet");
}

This is not ideal for a few reasons. You lose all intellisense for L, so you better know what you're doing. Also, if you import any functions that use L.something methods, you need to do the same thing. For example:

import { convertBoundsToCorners } from './some/utils';

function* handleGeocode(): Generator {
    try {
        const response = yield call(axios.get, `nominatim-url-blah-blah`);
        const { lat, lon: lng } = response[0];
        const bounds = L.latLngBounds([ 
            [lat - 0.5, lng - 0.5],
            [lat + 0.5, lng + 0.5],
        ]);

        const corners = convertBoundsToCorners(bounds);
 
        yield put(ActionCreators.requestDataInBounds(corners)));
            
    } catch (e) { }
}

In the imported method, if you use L.something methods, you have to do the masked import as well:

// utils.js

let L;

if (typeof window !== "undefined") {
    L = require("leaflet");
}

export const convertBoundsToCorners = bounds => {
  // do some stuff here that involves L.latLngBounds methods
}

So there are some clear disadvantages to this approach, but its the only one that I can get working without things crashing. Now I can use nice leaflet logic within my sagas!

This seems to be a universal way to include libraries that require access to the window object, as I had to do the same thing with device-uuid.

Seth Lutske
  • 9,154
  • 5
  • 29
  • 78
  • "_You lose all intellisense_": you may try to import only the type: `import type Leaflet from "leaflet"; let L: Leaflet;` – ghybs Feb 26 '22 at 00:39
  • @ghybs ah I was wondering what exactly the syntax was to type the whole leaflet library. Thanks! – Seth Lutske Feb 26 '22 at 05:25