reactjstypescriptuse-contextuse-reducercreatecontext

dispatch with UseReducer and useContext hooks: dispatch does not exist on type {}


I'm trying to build a small Cesium app with React that can zoom to various places on the globe by clicking a button associated with each location. I'm working on setting up the reducer using the useContext and useReducer hooks, but I'm having an issue with the dispatch that I can't figure out and haven't been able to find anything so far that points to what my issue might be. All the pages I've looked at so far tell me that what I have should work, but I'm obviously missing something.

Here are the two files I'm working with below:

location-card.tsx

export function LocationCard(props: LocationCardProps) {
  const classes = useStyles();
  const { className, name, type, location, onDetails } = props;

  const { dispatch } = useMapViewerContext();

  return (
    <Card variant="outlined" className={classes.locationCard}>
      <div className={classes.locationDetails}>
        <CardContent className={classes.locationContent}>
          <Typography component="div" variant="subtitle2">
            {name}
          </Typography>
          <Typography variant="caption" color="textSecondary">
            {type}
          </Typography>
        </CardContent>
        <div className={classes.locationCardControls}>
          <Tooltip title="Zoom to Location">
            <IconButton
              aria-label="Zoom To Location"
              onClick={() => {dispatch(zoomToLocation(location))}}
              className={classes.locationButton}
            >
.....

there is more to the above file, but this is the relevant piece. The error I get is where I try to set up dispatch. I am told

Property 'dispatch' does not exist on type '{}'

map-viewer-context.tsx

import React, { useContext, useReducer, useState } from 'react';
import Cartesian3 from 'cesium/Source/Core/Cartesian3';
import { Context } from 'node:vm';

export interface MapViewerState {
  location: Cartesian3;
  isCameraFlyTo: boolean;
  zoomToLocation: (value: Cartesian3) => void;
}

const initialState: MapViewerState = {
  location: Cartesian3.fromDegrees(
    -95.96044633079181,
    41.139682398924556,
    2100
  ),
  isCameraFlyTo: false,
  zoomToLocation: undefined,
};

export const MapViewerContext = React.createContext();

// Actions
export const ZOOM_TO_LOCATION = "ZOOM_TO_LOCATION";
export const COMPLETE_ZOOM = "FINISH";

// Action creators
export const zoomToLocation = (value: Cartesian3) => {
  return { type: ZOOM_TO_LOCATION, value };
}

// Reducer
export const mapViewerReducer = (state, action): MapViewerState => {
  switch (action.type) {
    case ZOOM_TO_LOCATION:
      return {...state, location: action.value, isCameraFlyTo: true};
    case COMPLETE_ZOOM:
      return {...state, isCameraFlyTo: false}
    default:
      return state;
  }
}

const MapViewerProvider = (props) => {
  const [state, dispatch] = useReducer(mapViewerReducer, initialState);

  const mapViewerData = { state, dispatch };

  return <MapViewerContext.Provider value={mapViewerData} {...props} />;
};

const useMapViewerContext = () => {
  return useContext(MapViewerContext);
};

export { MapViewerProvider, useMapViewerContext };

here is where I set up the actions, reducer, and context. I am also having an issue leaving React.createContext() without an argument. I can get rid of that error by providing it with the initialState I defined, but then the error in location-card.tsx simply changes to:

Property 'dispatch' does not exist on type 'MapViewerState'.

Can anyone help me figure out how to get this to work? Everything I've tried so far has been a bust.


Solution

  • The error you're seeing is caused by createContext not correctly inferring your context's type. You can explicitly specify the context's type like so:

    export interface MVContext {
      state: MapViewerState;
      dispatch: (action: MapViewerAction) => void;
    }
    
    export const MapViewerContext = createContext<MVContext>({} as MVContext);
    

    The type coercion {} as MVContext is safe as long as you're not planning on using useContext outside of a context provider. If you are, you'll need to provide default values for each key:

    export const MapViewerContext = createContext<MVContext>({
      state: MapViewerState.Default,
      dispatch: () => {},
    });