Search code examples
javascriptreactjsnext.jsmapboxreact-map-gl

react-map-gl and using useContext and/or useState to change viewport from another component


I seem to be not understand anything lately and having a tough time grasping useContext and/or useState with React/Nextjs and react-map-gl.

Basically, I have two components. A searchbox utilizing Formik & a Map from react-map-gl. What I want to do is be able to have one of the variables from Formik be able to change the viewport on react-map-gl so it redirects the viewport of the map to the new coordinates. I tried researching and reading about what I can do to make this work but I just can't grasp it. I believe it has to be something simple and I'm just too new to understand. Every time I think I have it I get a error saying I can't actually do that.

For reference here's my current code using useState. Whenever I try to use it and hit the submit button I get " 1 of 1 unhandled error Unhandled Runtime Error

TypeError: setViewport is not a function "

which doesn't seem to make any sense :( . Other times when I can get it to work, I have to click the submit button twice to get the form to register the new setViewport. What am I doing wrong? Thank you for your help!

index.js

import { Grid } from '@material-ui/core';
import { useState } from 'react';
import SearchBox from '../components/searchbox';
import {
  getAllCampgrounds,
  getAllCities,
  getCampgroundsByCity,
} from '../lib/api';
import Map from '../components/map';

export default function CampList({
  graphCampgrounds,
  cities,
  campgroundsbycity,
}) {
  const [viewport, setViewport] = useState({
    height: '100vh',
    latitude: 44.0456,
    longitude: -71.6706,
    width: '100vw',
    zoom: 8,
  });
  return (
    <Grid container spacing={3}>
      <Grid item xs={12} sm={5} md={3} lg={2}>
        <SearchBox
          graphCampgrounds={graphCampgrounds}
          cities={cities}
          campgroundsbycity={campgroundsbycity}
          setViewport={() => setViewport}
        />
      </Grid>
      <Grid item xs={12} sm={7} md={9} lg={10}>
        <Map
          campgrounds={graphCampgrounds}
          viewport={viewport}
          setViewport={() => setViewport}
        />
        <pre style={{ fontSize: '2.5rem' }}>{}</pre>
      </Grid>
    </Grid>
  );
}

I omitted my getServerSideProps area. Here is my Searchbox.js:

import Link from 'next/link';
import { Form, Formik, Field, useFormik } from 'formik';
import {
  Paper,
  Grid,
  FormControl,
  InputLabel,
  Select,
  MenuItem,
  Button,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { useRouter } from 'next/router';

const useStyles = makeStyles(theme => ({
  paper: {
    margin: 'auto',
    maxWidth: 500,
    padding: theme.spacing(3),
  },
}));

export default function SearchBox({
  graphCampgrounds,
  cities,
  campgroundsbycity,
  viewport,
  setViewport,
}) {

  const router = useRouter();
  const { query } = router;


  const handleSubmit = async values => {
setViewport({
    ...viewport,
    latitude: campgroundsbycity
      ? parseFloat(campgroundsbycity.nodes[0].acfDetails.latitude.toFixed(4))
      : 44.43,
    longitude: campgroundsbycity
      ? Math.abs(
          parseFloat(campgroundsbycity.nodes[0].acfDetails.longitude.toFixed(4))
        ) * -1
      : -72.352,
    zoom: 11,
  });
    router.push(
      {
        pathname: '/camps',
        query: { ...values, page: 1 },
      },
      undefined,
      { shallow: true }
    );
  };

  const classes = useStyles();
  const smValue = singleColumn ? 12 : 6;

  const initialValues = {
    city: query.city || 'all'
  };

  return (
    <Formik initialValues={initialValues} onSubmit={handleSubmit}>
      {({ values, submitForm }) => (
        <Form>
          <Paper className={classes.paper} elevation={5}>
            <Grid container spacing={3}>

              <Grid item xs={12} sm={smValue}>
                <FormControl fullWidth variant="outlined">
                  <InputLabel id="search-cities">City</InputLabel>
                  <Field
                    name="city"
                    as={Select}
                    labelId="search-cities"
                    label="Cities"
                  >
                    <MenuItem value="all">
                      <em>All</em>
                    </MenuItem>
                    {cities.nodes.map(town => {
                      return (
                        <MenuItem
                          key={town.acfDetails.city}
                          value={town.acfDetails.city}
                        >
                          {town.acfDetails.city}
                        </MenuItem>
                      );
                    })}
                  </Field>
                </FormControl>
              </Grid>

              <Grid item xs={12}>
                <Button
                  color="primary"
                  variant="outlined"
                  type="button"
                  fullWidth
                  onClick={submitForm}
                >
                  Search Campgrounds
                </Button>
              </Grid>
            </Grid>
          </Paper>
        </Form>
      )}
    </Formik>
  );
}

And here's my map component:

import { useContext, useMemo } from 'react';
import ReactMapGL, { Marker, MapContext } from 'react-map-gl';
import { ViewportContext } from '../lib/state';

export default function Map({ campgrounds, viewport, setViewport }) {
  const markers = campgrounds.map(({ node }) => {
    console.log(node.acfDetails);
    return (
      <Marker
        key={node.title}
        longitude={
          Math.abs(parseFloat(node.acfDetails.longitude.toFixed(4))) * -1
        }
        latitude={parseFloat(node.acfDetails.latitude.toFixed(4))}
      >
        <img src="pin.png" alt={node.title} />
      </Marker>
    );
  });
  return (
    <ReactMapGL
      mapboxApiAccessToken={process.env.NEXT_PUBLIC_MAPBOX_KEY}
      mapStyle="mapbox://styles/mapbox/outdoors-v11"
      {...viewport}
      onViewportChange={nextViewport => setViewport(nextViewport)}
    >
      {markers}
    </ReactMapGL>
  );
}


Solution

  • Agreed with @juliomalves's answer.

    In you index.js, pass the function setViewport to your SearchBox and Map components by setViewport={setViewport}.

    That is how React hooks should be called.