├── .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 | ![An example application featuring OpenLayers and React Functional Components](https://taylor.callsen.me/wp-content/uploads/2020/09/tcallsen-react-function-openlayers-example-sept-2020.jpg "An example application featuring OpenLayers and React Functional Components") 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 | ); --------------------------------------------------------------------------------