# Geolocation Attachment & Live Location Sharing

In this comprehensive example, we demonstrate how to build a live location sharing feature. Chat users will have the ability to send their location to a channel and display it through a custom `Attachment` component that displays coordinates using the [Google Maps API](https://developers.google.com/maps/documentation/javascript/overview).

The feature flow has three distinct steps:

- Click `Share Location` button with confirmation dialog
- Confirm, send and register a message for live location updates
- Render coordinates on a Google Maps overlay sent as a message attachment

## Prerequisities

We've prepared [an example geolocation context/controller](#geoloccontext) that we'll be referencing throughout the guide. This controller takes care of registering and updating messages with the current location. Note that this is an _example_ controller that should be expanded on based on your needs and requirements. This controller is missing logic such as end-of-life of the messages with live location updates, manually stopping location updates, limiting the amount of messages with live location updates or proper error handling. Treat it as a baseline to get you started.

<admonition type="note">

If you wish to use only one-time location sharing functionality, you don't need [`GeoLocContext`](#geoloccontext) controller/context and can safely omit it.

</admonition>

## Custom Message Input With Location Sharing Button

The first step in our location sharing flow is to add a custom button beside message input that on click allows a chat user to begin the process of sending their coordinates to the channel.

In this example, our custom handler function will utilise `window.confirm` dialog to get the user confirmation before requesting location data.

<admonition type="note">

We'll be using `registerMessageIds` function from our pre-defined [`GeoLocContext`](#geoloccontext) to save messages to a `localStorage` to be later loaded in case of a page reload so that we can keep sharing our location.

</admonition>

<Tabs>

</Tabs>

![](@chat-sdk/react/v14-latest/_assets/geolocation-attachment-0.png)

![](@chat-sdk/react/v14-latest/_assets/geolocation-attachment-1.png)

## Custom Map Attachment

Now that we're able to send a message with an attachment of type `location`, we need to build a custom `Attachment` component that can this new type. If a message attachment is not of type `location`, meaning it's a standard library type, we return the default `Attachment` component.

When our custom component receives an attachment of type `location`, we pass the geolocation coordinates to the `Map` and `Marker` components from [`@vis.gl/react-google-maps`](https://www.npmjs.com/package/@vis.gl/react-google-maps). This library is a React-based wrapper around the Google Maps API and displays a map and geolocation as a React component. See the [`@vis.gl/react-google-maps`](https://www.npmjs.com/package/@vis.gl/react-google-maps) documentation for more information.

<admonition type="tip">

In order to interact with the Google Maps API, you must [set up an account](https://developers.google.com/maps/documentation/javascript/cloud-setup) and generate an API key.

</admonition>

<admonition type="note">

Since we've added new fields to our `location` type message attachment, we must extend the default attachment type to support `latitude` and `longitude` through `StreamChatGenerics` which we can then pass to `AttachmentProps` type.

</admonition>

```tsx title="custom-components.tsx"
import { Attachment } from "stream-chat-react";
import { APIProvider, Map, Marker } from "@vis.gl/react-google-maps";

import type { AttachmentProps } from "stream-chat-react";

export type StreamChatGenerics = {
  attachmentType: { latitude: number; longitude: number };
  // ▽ defaults
  commandType: string;
} & Record<
  | "channelType"
  | "eventType"
  | "messageType"
  | "pollOptionType"
  | "pollType"
  | "reactionType"
  | "userType",
  Record<string, unknown>
>;

const GOOGLE_MAPS_API_KEY = "key";

export const AttachmentWithMap = (
  props: AttachmentProps<StreamChatGenerics>,
) => {
  const [locationAttachment] = props.attachments;

  if (locationAttachment.type === "location") {
    const { latitude, longitude } = locationAttachment;

    return (
      <APIProvider apiKey={GOOGLE_MAPS_API_KEY}>
        <Map
          style={{ width: "300px", height: "300px" }}
          defaultCenter={{ lat: latitude, lng: longitude }}
          defaultZoom={16}
          disableDefaultUI={true}
          gestureHandling="greedy"
        >
          <Marker position={{ lat: latitude, lng: longitude }} />
        </Map>
      </APIProvider>
    );
  }

  return <Attachment {...props} />;
};
```

## Implementation

Now that each individual piece has been constructed, we can assemble all of the snippets into the final code example.

```tsx title="app.tsx"
import { Chat, ChannelList, Channel, Window, Thread } from "stream-chat-react";

import {
  AttachmentWithMap,
  MessageInputWithLocationButton,
} from "./custom-components";
import { GeoLocContextProvider } from "./geo-loc-context";

import type { StreamChatGenerics } from "./custom-components";

const App = () => {
  const chatClient = useCreateChatClient<StreamChatGenerics>(/*...*/);

  if (!chatClient) return <div>Loading...</div>;

  return (
    <Chat client={chatClient}>
      <ChannelList />
      <Channel Attachment={AttachmentWithMap}>
        <Window>
          <ChannelHeader />
          <MessageList />
          <MessageInputWithLocationButton />
        </Window>
        <Thread />
      </Channel>
    </Chat>
  );
};
```

![](@chat-sdk/react/v14-latest/_assets/geolocation-attachment-2.png)

## GeoLocContext

```tsx title="geo-loc-context.tsx"
import React, {
  useState,
  useEffect,
  useCallback,
  useRef,
  useContext,
} from "react";
import { useChatContext } from "stream-chat-react";

import type { PropsWithChildren } from "react";

type GeoLocContextValue = {
  registeredMessageIds: string[];
  registerMessageIds: (messageIds: string[]) => void;
  unregisterMessageIds: (messageIds: string[]) => void;
};

const GeoLocContext = React.createContext<GeoLocContextValue>({
  registeredMessageIds: [],
  registerMessageIds: () => {},
  unregisterMessageIds: () => {},
});

export const useGeoLocContext = () => useContext(GeoLocContext);

const constructStorageKey = (userId: string) => {
  return `$geoloc.registeredMessageIds-${userId}`;
};

export const GeoLocContextProvider = ({ children }: PropsWithChildren) => {
  const { client } = useChatContext();

  const [registeredMessageIds, setRegisteredMessageIds] = useState(() => {
    const registeredMessageIdsString = window.localStorage.getItem(
      constructStorageKey(client.userID!),
    );

    if (!registeredMessageIdsString) return [];

    try {
      const messageIds = JSON.parse(atob(registeredMessageIdsString));

      return messageIds as unknown as string[];
    } catch (error) {
      console.log(error);
      return [];
    }
  });

  const registerMessageIds = useCallback((messageIds: string[]) => {
    setRegisteredMessageIds((currentMessageIds) =>
      currentMessageIds.concat(messageIds),
    );
  }, []);

  const unregisterMessageIds = useCallback((messageIds: string[]) => {
    setRegisteredMessageIds((currentMessageIds) =>
      currentMessageIds.filter((messageId) => !messageIds.includes(messageId)),
    );
  }, []);

  const handlePositionChangeRef =
    useRef<(position: GeolocationPosition) => Promise<void>>();

  handlePositionChangeRef.current = async (position) => {
    const settled = await Promise.allSettled(
      registeredMessageIds.map((messageId) =>
        client.partialUpdateMessage(messageId, {
          set: {
            attachments: [
              {
                type: "location",
                latitude: position.coords.latitude,
                longitude: position.coords.longitude,
              },
            ],
          },
        }),
      ),
    );

    const idsToDelete = [];
    for (let index = 0; index < settled.length; index++) {
      if (settled[index].status !== "rejected") continue;
      idsToDelete.push(registeredMessageIds[index]);
    }

    unregisterMessageIds(idsToDelete);
  };

  useEffect(() => {
    // sync storage
    const oldValue = window.localStorage.getItem(
      constructStorageKey(client.userID!),
    );
    const newValue = btoa(JSON.stringify(registeredMessageIds));

    if (newValue === oldValue) return;

    window.localStorage.setItem(constructStorageKey(client.userID!), newValue);
  }, [client, registeredMessageIds]);

  const shouldWatch = registeredMessageIds.length !== 0;

  useEffect(() => {
    let promise: Promise<void> | null = null;

    if (!shouldWatch) return;

    const position = navigator.geolocation.watchPosition(
      (newPosition) => {
        if (promise) return;

        promise = handlePositionChangeRef.current!(newPosition).finally(() => {
          promise = null;
        });
      },
      console.log,
      { enableHighAccuracy: true },
    );

    return () => {
      navigator.geolocation.clearWatch(position);
    };
  }, [shouldWatch]);

  return (
    <GeoLocContext.Provider
      value={{ registeredMessageIds, registerMessageIds, unregisterMessageIds }}
    >
      {children}
    </GeoLocContext.Provider>
  );
};
```


---

This page was last updated at 2026-05-22T16:32:11.995Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v12/guides/theming/actions/geolocation-attachment/](https://getstream.io/chat/docs/sdk/react/v12/guides/theming/actions/geolocation-attachment/).