├── .gitignore
├── .nvmrc
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── mock-geojson-api.json
└── robots.txt
└── src
├── App.css
├── App.js
├── components
└── MapWrapper.js
├── index.css
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v22.11.0
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Using OpenLayers with React Functional Components and Hooks
2 |
3 | Here is a sample application that demonstrates how to integrate OpenLayers with React Functional Components including:
4 |
5 | * creating a wrapper component ([MapWrapper.js](./src/components/MapWrapper.js))
6 | * using hooks to initialize the map and add features from props
7 | * get/set component state from within an OpenLayers event handler using refs
8 | * Fetch and parse GeoJSON features from a mock API (done inside [App.js](./src/App.js))
9 |
10 | 
11 |
12 | ## Install
13 |
14 | Install the dependencies with the following command:
15 |
16 | ```
17 | nvm use
18 | npm install
19 | ```
20 |
21 | ## Launch with Development Server
22 |
23 | To run a development build and launch the development server, execute:
24 |
25 | `npm start`
26 |
27 | Once completed, the app should be avialable from http://localhost:3000/
28 |
29 | ## Development Environment
30 |
31 | This application was developed using create-react-app, with Node version `v22.11.0`
32 |
33 | ## Supplemental Blog Post
34 |
35 | Here is a blog post I created that explains the integration in further detail: [https://taylor.callsen.me/using-openlayers-with-react-functional-components/](https://taylor.callsen.me/using-openlayers-with-react-functional-components/)
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-func-openlayers",
3 | "version": "0.4.0",
4 | "dependencies": {
5 | "ol": "^10.2.1",
6 | "react": "^18.3.1",
7 | "react-dom": "^18.3.1",
8 | "react-scripts": "^5.0.1"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "test": "react-scripts test",
14 | "eject": "react-scripts eject"
15 | },
16 | "eslintConfig": {
17 | "extends": "react-app"
18 | },
19 | "browserslist": {
20 | "production": [
21 | ">0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcallsen/react-func-openlayers/6bba3f7857596a3e373b12bc154fef25df9d6e2c/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
21 | OpenLayers React Functional Components Sample
22 |
23 |
24 |
25 |
26 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | height: 100%;
4 | width: 100%;
5 | }
6 |
7 | .app-label{
8 | width: 50%;
9 | overflow: auto;
10 | margin: auto;
11 | position: absolute;
12 | top: 0;
13 | left: 0;
14 | right: 0;
15 | z-index: 2;
16 | background: white;
17 | pointer-events: none;
18 | }
19 |
20 | .map-container{
21 | height: 100vh;
22 | width: 100%;
23 | }
24 |
25 | .clicked-coord-label{
26 | position: absolute;
27 | right: 0;
28 | bottom: 0;
29 | background: white;
30 | border-radius: 5px;
31 | }
32 |
33 | .clicked-coord-label p {
34 | margin: 10px;
35 | }
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 |
3 | // react
4 | import React, { useState, useEffect } from 'react';
5 |
6 | // openlayers
7 | import GeoJSON from 'ol/format/GeoJSON'
8 |
9 | // components
10 | import MapWrapper from './components/MapWrapper'
11 |
12 | function App() {
13 |
14 | // set intial state
15 | const [ features, setFeatures ] = useState([])
16 |
17 | // initialization - retrieve GeoJSON features from Mock JSON API get features from mock
18 | // GeoJson API (read from flat .json file in public directory)
19 | useEffect( () => {
20 |
21 | fetch('/mock-geojson-api.json')
22 | .then(response => response.json())
23 | .then( (fetchedFeatures) => {
24 |
25 | // parse fetched geojson into OpenLayers features
26 | // use options to convert feature from EPSG:4326 to EPSG:3857
27 | const wktOptions = {
28 | dataProjection: 'EPSG:4326',
29 | featureProjection: 'EPSG:3857'
30 | }
31 | const parsedFeatures = new GeoJSON().readFeatures(fetchedFeatures, wktOptions)
32 |
33 | // set features into state (which will be passed into OpenLayers
34 | // map component as props)
35 | setFeatures(parsedFeatures)
36 |
37 | })
38 |
39 | },[])
40 |
41 | return (
42 |
43 |
44 |
45 |
React Functional Components with OpenLayers Example
46 |
Click the map to reveal location coordinate via React State
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 |
55 | export default App
56 |
--------------------------------------------------------------------------------
/src/components/MapWrapper.js:
--------------------------------------------------------------------------------
1 | // react
2 | import React, { useState, useEffect, useRef } from 'react';
3 |
4 | // openlayers
5 | import Map from 'ol/Map'
6 | import View from 'ol/View'
7 | import TileLayer from 'ol/layer/Tile'
8 | import VectorLayer from 'ol/layer/Vector'
9 | import VectorSource from 'ol/source/Vector'
10 | import XYZ from 'ol/source/XYZ'
11 | import {transform} from 'ol/proj'
12 | import {toStringXY} from 'ol/coordinate';
13 |
14 | function MapWrapper(props) {
15 |
16 | // set intial state
17 | const [ map, setMap ] = useState()
18 | const [ featuresLayer, setFeaturesLayer ] = useState()
19 | const [ selectedCoord , setSelectedCoord ] = useState()
20 |
21 | // pull refs
22 | const mapElement = useRef()
23 |
24 | // create state ref that can be accessed in OpenLayers onclick callback function
25 | // https://stackoverflow.com/a/60643670
26 | const mapRef = useRef()
27 | mapRef.current = map
28 |
29 | // initialize map on first render - logic formerly put into componentDidMount()
30 | useEffect( () => {
31 |
32 | // create and add vector source layer
33 | const initalFeaturesLayer = new VectorLayer({
34 | source: new VectorSource()
35 | })
36 |
37 | // create map
38 | const initialMap = new Map({
39 | target: mapElement.current,
40 | layers: [
41 |
42 | // USGS Topo
43 | new TileLayer({
44 | source: new XYZ({
45 | url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
46 | })
47 | }),
48 |
49 | // Google Maps Terrain
50 | /* new TileLayer({
51 | source: new XYZ({
52 | url: 'http://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}',
53 | })
54 | }), */
55 |
56 | initalFeaturesLayer
57 |
58 | ],
59 | view: new View({
60 | projection: 'EPSG:3857',
61 | center: [0, 0],
62 | zoom: 2
63 | }),
64 | controls: []
65 | })
66 |
67 | // set map onclick handler
68 | initialMap.on('click', handleMapClick)
69 |
70 | // save map and vector layer references to state
71 | setMap(initialMap)
72 | setFeaturesLayer(initalFeaturesLayer)
73 |
74 | // optional - safely reset map on unmount to prevent flickering issues - logic formerly put into componentWillUnmount()
75 | return () => {
76 | initialMap.setTarget(null)
77 | setMap(null)
78 | }
79 |
80 | },[])
81 |
82 | // update map if features prop changes - logic formerly put into componentDidUpdate()
83 | useEffect( () => {
84 |
85 | if (props.features.length) { // may be null on first render
86 |
87 | // set features to map
88 | featuresLayer.setSource(
89 | new VectorSource({
90 | features: props.features // make sure features is an array
91 | })
92 | )
93 |
94 | // fit map to feature extent (with 100px of padding)
95 | map.getView().fit(featuresLayer.getSource().getExtent(), {
96 | padding: [100,100,100,100]
97 | })
98 |
99 | }
100 |
101 | },[props.features])
102 |
103 | // map click handler
104 | const handleMapClick = (event) => {
105 |
106 | // get clicked coordinate using mapRef to access current React state inside OpenLayers callback
107 | // https://stackoverflow.com/a/60643670
108 | const clickedCoord = mapRef.current.getCoordinateFromPixel(event.pixel);
109 |
110 | // transform coord to EPSG 4326 standard Lat Long
111 | const transormedCoord = transform(clickedCoord, 'EPSG:3857', 'EPSG:4326')
112 |
113 | // set React state
114 | setSelectedCoord( transormedCoord )
115 |
116 | }
117 |
118 | // render component
119 | return (
120 |
121 |
122 |
123 |
124 |
125 |
{ (selectedCoord) ? toStringXY(selectedCoord, 5) : '' }
126 |
127 |
128 |
129 | )
130 |
131 | }
132 |
133 | export default MapWrapper
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as ReactDOMClient from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 |
6 | // Initial render: Render an element to the root
7 | const container = document.getElementById('root');
8 | const root = ReactDOMClient.createRoot(container);
9 | root.render(
10 |
11 | );
--------------------------------------------------------------------------------