DEV Community

Cover image for Building ArcGIS API for JavaScript Apps with NextJS
Rene Rubalcava
Rene Rubalcava

Posted on • Originally published at esri.com

Building ArcGIS API for JavaScript Apps with NextJS

React is a popular library for building web applications. However, it's only a library, not a complete framework. This is where something like NextJS becomes useful. NextJS is a complete React framework for building applications. It comes with a variety of features including routing, static site generation, and even built in API endpoints, so you can write server-side code in your application if you need it. It pairs great with the ArcGIS API for JavaScript.

You can get started with NextJS with the following command.

npx create-next-app@latest

For this application, we’re going to be looking at a service of global power plants. For the user experience, we want to display a list of power plants by type, and when the user clicks on a type of plant from a list, it will display a map of the power plants for that selected type.

You can find the source code for the application in this blog post on github.

API Route

To accomplish the first task of getting a list of the types of power plants, we can write an API route in a NodeJS environment. We can use the ArcGIS API for JavaScript in the route API to query the service and extract the values from the results.

import type { NextApiRequest, NextApiResponse } from "next";
import { executeQueryJSON } from "@arcgis/core/rest/query";

const PLANT_URL =
  "https://services1.arcgis.com/4yjifSiIG17X0gW4/arcgis/rest/services/PowerPlants_WorldResourcesInstitute/FeatureServer/0";

type Data = {
  types: string[];
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const query = {
    outFields: ["fuel1"],
    where: "1=1",
    returnDistinctValues: true,
    returnGeometry: false
  };
  const results = await executeQueryJSON(PLANT_URL, query);
  const values = results.features
    .map((feature) => feature.attributes["fuel1"])
    .filter(Boolean)
    .sort();
  res.status(200).json({ types: values });
}
Enter fullscreen mode Exit fullscreen mode

In this API route, we are going to query the feature service, limit the results only to the field for the primary type of power generated at the plant and extract that to a simple list. The best part of this is that this query is executed on the server, so there is no latency on the client to run this query.

Redux and Stores

To manage the application state, we can use Redux. If you’ve used Redux in the past, you might be thinking you need to set up a lot of boiler plate code for constants, actions, and reducers. The Redux toolkit helps to simplify this using slices with the createSlice() method. This will let you define the name of the slice, the initial state, and the reducers, or methods used to update the state. We can create one that will be used for our application.

import { createSlice } from '@reduxjs/toolkit'

export interface AppState {
    types: string[];
    selected?: string;
}

const initialState: AppState = {
    types: []
}

export const plantsSlice = createSlice({
    name: 'plants',
    initialState,
    reducers: {
        updateTypes: (state, action) => {
            state.types = action.payload
        },
        updateSelected: (state, action) => {
            state.selected = action.payload
        }
    },
})

export const { updateTypes, updateSelected} = plantsSlice.actions

export default plantsSlice.reducer
Enter fullscreen mode Exit fullscreen mode

With our slice and reducers defined, we can create a React store and hook to be used in our application for the reducer.

import { configureStore } from '@reduxjs/toolkit'
import plantsReducer from '../features/plants/plantsSlice'

const store = configureStore({
  reducer: {
      plants: plantsReducer
  },
})

export default store
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Enter fullscreen mode Exit fullscreen mode

In this case, the only reason we really need the custom hooks is to have proper TypeScript typings.

Layout

At this point, we can start thinking about how the application and pages will be displayed. We can start with a layout file.

import Head from 'next/head'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useAppSelector } from '../app/hooks'
import { useEffect, useState } from 'react'
import styles from './layout.module.css'

export default function Layout({ children }: any) {
    const router = useRouter()
    const selected = useAppSelector((state) => state.plants.selected)
    const [showPrevious, setShowPrevious] = useState(false)
    useEffect(() => {
        setShowPrevious(router.asPath.includes('/webmap'))
    }, [router])
    return (
        <>
            <Head>
                <title>Power Plants Explorer</title>
            </Head>
            <div className={styles.layout}>
                <header className={styles.header}>
                    {
                        showPrevious ?
                        <Link href="/">
                            <a>
                                <svg className={styles.link} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M14 5.25L3.25 16 14 26.75V20h14v-8H14zM27 13v6H13v5.336L4.664 16 13 7.664V13z"/><path fill="none" d="M0 0h32v32H0z"/></svg>
                            </a>
                        </Link>
                        : null
                    }
                    <div className={styles.container}>
                        <h3>Global Power Plants</h3>
                        {showPrevious  && selected ? <small className={styles.small}>({selected})</small> : null}
                    </div>
                </header>
                <main className={styles.main}>{children}</main>
            </div>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

The layout is going to define how the all pages are going to look. We are going to have a header on the page with a navigation button and a title. This will be visible on all the pages of our application. Then we can define a section of the layout that will be used for the varying content.

Router

This is also where we start looking at the provided router with NextJS. When we are on the page that displays the map, we want to add a back button to return to the list of power plants. The layout page creates a header, and a main element for the content.

We can use the layout in the global App for NextJS.

import '../styles/globals.css'
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'
import { Provider } from 'react-redux'
import store from '../app/store'

type NextPageWithLayout = NextPage & {
  getLayout?: (page: ReactElement) => ReactNode
}

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout = Component.getLayout ?? ((page) => page)
  return (
    <Provider store={store}>
      {getLayout(<Component {...pageProps} />)}
    </Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

It’s in this global app file that we can add the layout and provider for our Redux store. The global app will determine if there is a layout or not and apply it.

API

To fetch data from our routing API, we can use swr, which will provide a React hook that handles fetching data for us. It’s not required, but it’s a useful tool to help wrap a number data fetching capabilities, like caching and more.

import styles from '../../styles/Home.module.css'
import useSWR from 'swr'
import { useState, useEffect } from 'react'
import { useAppSelector, useAppDispatch } from '../../app/hooks'
import { updateTypes } from './plantsSlice'
import Loader from '../../components/loader'
import { useRouter } from 'next/router'

const fetcher = async (
    input: RequestInfo,
    init: RequestInit,
    ...args: any[]
  ) => {
        const res = await fetch(input, init)
        return res.json()
    }

const Plants = () => {
    const { data, error } = useSWR('/api/powerplants', fetcher)
    const types = useAppSelector((state) => state.plants.types)
    const dispatch = useAppDispatch()
    const [isLoading, setLoading] = useState(true)
    const router = useRouter()

    useEffect(() => {
        setLoading(true)
        if (data) {
            dispatch(updateTypes(data.types))
            setLoading(false)
        }
    }, [data, error, dispatch])

    if (isLoading)
        return (
            <div className={styles.loader}>
                <Loader />
            </div>
        )
    if (!types.length) return <p>No data</p>

    return (
        <ul className={styles.list}>
            {types.map((value, idx) => (
            <li
                className={styles.listItem}
                key={`${value}-${idx}`}
                onClick={() => router.push(`/webmap?type=${value}`)}
            >
                {value}
            </li>
            ))}
        </ul>
    )
}

export default Plants
Enter fullscreen mode Exit fullscreen mode

Pages

The plants component will fetch the list of power plants and display them. It will display a simple animated SVG loader while it loads the request. When a type of power plant is selected from the list, it will route to a page that displays the map and will filter the results to the selected type of power plant. Since the entry page for this application will display the list of power plants, we can use this Plants component in our index.tsx file.

import styles from '../styles/Home.module.css'
import Layout from '../components/layout'
import { ReactElement } from 'react'
import Plants from '../features/plants/plants'

const Home = () => {
  return (
    <div className={styles.container}>
      <Plants />
    </div>
  )
}

Home.getLayout = function getLayout(page: ReactElement) {
  return <Layout>{page}</Layout>
}

export default Home
Enter fullscreen mode Exit fullscreen mode

Our index.tsx file exposes a Home component that will be the home route for our application.

The next step is defining our webmap route for the application. This page will display our webmap and filter the results to only display the type of Power Plants that were selected from the list in the home page. To make this more configurable, we can also add a ?type= parameter to the URL string so we can share this link with other users later on.

import styles from '../styles/WebMap.module.css'
import Layout from '../components/layout'
import { ReactElement, useEffect, useRef } from 'react'
import { useRouter } from 'next/router'
import { useAppSelector, useAppDispatch } from '../app/hooks'
import { updateSelected } from '../features/plants/plantsSlice'

async function loadMap(container: HTMLDivElement, filter: string) {
    const { initialize } = await import('../data/mapping')
    return initialize(container, filter)
}

const WebMap = () => {
    const mapRef = useRef<HTMLDivElement>(null)
    const router = useRouter()
    const { type } = router.query
    const selected = useAppSelector((state) => state.plants.selected)
    const dispatch = useAppDispatch()

    useEffect(() => {
        dispatch(updateSelected(type))
    }, [type, dispatch])

    useEffect(() => {
        let asyncCleanup: Promise<(() => void)>
        if (mapRef.current && selected) {
            asyncCleanup = loadMap(mapRef.current, selected)
        }
        return () => {
            asyncCleanup && asyncCleanup.then((cleanup) => cleanup())
        }
    }, [mapRef, selected])

    return (
        <div className={styles.container}>
            <div className={styles.viewDiv} ref={mapRef}></div>
        </div>
    )
}

WebMap.getLayout = function getLayout(page: ReactElement) {
  return <Layout>{page}</Layout>
}

export default WebMap
Enter fullscreen mode Exit fullscreen mode

There are few things happening here. We’re using the provided router hooks from NextJS to get the query parameters. We also manage a little bit of state to display a button to navigate back to the home page. Notice there is no reference to the ArcGIS API for JavaScript in this component. We have a loadMap() method that dynamically imports a mapping module. This mapping module is how we communicate with modules from the ArcGIS API for JavaScript.

import config from '@arcgis/core/config'
import ArcGISMap from '@arcgis/core/Map'
import FeatureLayer from '@arcgis/core/layers/FeatureLayer'
import MapView from '@arcgis/core/views/MapView'
import Extent from '@arcgis/core/geometry/Extent'
import { watch } from '@arcgis/core/core/reactiveUtils'
import Expand from '@arcgis/core/widgets/Expand'
import Legend from '@arcgis/core/widgets/Legend';
import LayerList from '@arcgis/core/widgets/LayerList';

config.apiKey = process.env.NEXT_PUBLIC_API_KEY as string

interface MapApp {
    view?: MapView;
    map?: ArcGISMap;
    layer?: FeatureLayer;
    savedExtent?: any;
}

const app: MapApp = {}

let handler: IHandle

export async function initialize(container: HTMLDivElement, filter: string) {
    if (app.view) {
        app.view.destroy()
    }

    const layer = new FeatureLayer({
        portalItem: {
            id: '848d61af726f40d890219042253bedd7'
        },
        definitionExpression: `fuel1 = '${filter}'`,
    })

    const map = new ArcGISMap({
        basemap: 'arcgis-dark-gray',
        layers: [layer]
    })

    const view = new MapView({
        map,
        container
    })

    const legend = new Legend({ view });
    const list = new LayerList({ view });

    view.ui.add(legend, 'bottom-right');
    view.ui.add(list, 'top-right');

    if(app.savedExtent) {
        view.extent = Extent.fromJSON(app.savedExtent)
    } else {
        layer.when(() => {
            view.extent = layer.fullExtent
        })
    }

    handler = watch(
        () => view.stationary && view.extent,
        () => {
            app.savedExtent = view.extent.toJSON()
        }
    )

    view.when(async () => {
        await layer.when()
        const element = document.createElement('div')
        element.classList.add('esri-component', 'esri-widget', 'esri-widget--panel', 'item-description')
        element.innerHTML = layer.portalItem.description
        const expand = new Expand({
            content: element,
            expandIconClass: 'esri-icon-description'
        })
        view.ui.add(expand, 'bottom-right')
    })

    app.map = map
    app.layer = layer
    app.view = view

    return cleanup
}

function cleanup() {
    handler?.remove()
    app.view?.destroy()
}
Enter fullscreen mode Exit fullscreen mode

This  mapping module creates a thin API layer in our application to communicate with the ArcGIS API for JavaScript and our application components. The initialize method creates the map and layer. It also saves the extent as a JSON object as the user navigates the map. So when the user navigates to the home page and comes back to the map, their last viewed location will be saved and reused again. This is a useful way to provide a more seamless user experience.

This is what the finished application looks like.

ArcGIS NextJS Application

Deployment

NextJS leverages what is called serverless functions. Serverless functions are short lived methods that last only seconds, spun up for use and quickly destroyed. NextJS uses them for the API route when serving pages. You will need to keep this in mind when you deploy your application. It should be noted, NextJS is developed by Vercel, and they do offer a hosting solution that works with serverless functions. Other platforms like Heroku and Amazon do as well. It’s up to you to decide where you want to deploy your application to use these serverless functions. For demo purposes, I deployed the application to Heroku here.

Summary

NextJS is a powerful React framework you can use to build scalable production ready applications using the ArcGIS API for JavaScript. You can use tooling like Redux to help you manage your application state, and even use the ArcGIS API for JavaScript to query mapping services in serverless functions. This application also provides the benefits of fast load times by deferring loading the map until necessary.

ArcGIS JSAPI Lighthouse Scores

The combination of NextJS and the ArcGIS API for JavaScript provide a great developer experience, and one that I highly recommend you try for yourself. Have fun, and build some awesome applications!

You can view a walkthough in the video below!

Top comments (0)