Apollo Client Websocket (Reed Barger)
Created
October 2, 2020 13:09
-
-
Save GGrassiant/777d47c58750c52cff73481a6bbe9035 to your computer and use it in GitHub Desktop.
Apollo Client Websocket (custom hook)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // for subscriptions | |
| import { ApolloProvider } from "react-apollo"; | |
| import { ApolloClient } from 'apollo-client'; | |
| import { WebSocketLink } from "apollo-link-ws"; | |
| import { InMemoryCache } from "apollo-cache-inmemory"; | |
| const wsLink = new WebSocketLink({ | |
| uri: process.env.NODE_ENV === 'production' ? 'wss://geopins-mtl.herokuapp.com/graphql' : 'ws://localhost:4000/graphql', | |
| options: { reconnect: true }}); | |
| // for the Apollo Provider around the React app | |
| const client = new ApolloClient({ | |
| link: wsLink, | |
| cache: new InMemoryCache() | |
| }); | |
| const Root = () => { | |
| const initialState = useContext(Context); | |
| const [state, dispatch] = useReducer(reducer, initialState); | |
| return ( | |
| <Router> | |
| <ApolloProvider client={client}> | |
| <Context.Provider value={ { state, dispatch } }> // see Gist on custom Global state hook | |
| <Switch> | |
| <ProtectedRoute exact path="/" component={App} /> | |
| <Route path="/login" component={Splash} /> | |
| </Switch> | |
| </Context.Provider> | |
| </ApolloProvider> | |
| </Router> | |
| ); | |
| }; | |
| ReactDOM.render(<Root />, document.getElementById("root")); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useEffect, useContext } from "react"; | |
| import ReactMapGL, { NavigationControl, Marker, Popup } from 'react-map-gl'; | |
| import { withStyles } from "@material-ui/core/styles"; | |
| import Button from "@material-ui/core/Button"; | |
| import Typography from "@material-ui/core/Typography"; | |
| import DeleteIcon from "@material-ui/icons/DeleteTwoTone"; | |
| // for media-query | |
| import { unstable_useMediaQuery as useMediaQuery } from "@material-ui/core/useMediaQuery"; | |
| // for subscriptions | |
| import { Subscription } from 'react-apollo'; | |
| import { PIN_ADDED_SUBSCRIPTION, PIN_UPDATED_SUBSCRIPTION, PIN_DELETED_SUBSCRIPTION } from "../graphql/subscriptions"; | |
| // helper for time | |
| import differenceInMinutes from "date-fns/difference_in_minutes"; | |
| // Get the custom client hook the query to populate the map with pins | |
| import { useClient } from "../client"; | |
| import { GET_PINS_QUERY } from "../graphql/queries"; | |
| import { DELETE_PIN_MUTATION } from "../graphql/mutations"; | |
| import PinIcon from "./PinIcon"; | |
| import Blog from "./Blog"; | |
| // store and global state | |
| import Context from "../context"; | |
| import { | |
| CREATE_DRAFT, | |
| UPDATE_DRAFT_LOCATION, | |
| GET_PINS, | |
| SET_PIN, | |
| DELETE_PIN, | |
| CREATE_PIN, | |
| CREATE_COMMENT | |
| } from '../actions/types'; | |
| // default value for the map positioning | |
| const INITIAL_VIEWPORT = { | |
| latitude: 45.5017, | |
| longitude: -73.5673, | |
| zoom: 13 | |
| }; | |
| const Map = ({ classes }) => { | |
| // useMediaQuery for responsive | |
| const mobileSize = useMediaQuery('(max-width: 650px )'); | |
| // custom hook to create the Apollo Server | |
| const client = useClient(); | |
| // use global state draft for pins | |
| const { state, dispatch } = useContext(Context); | |
| // on Mount, get all the pins | |
| useEffect(() => { | |
| getPins(); | |
| }, []); | |
| // track local state of map to move around by dragging it in the viewport | |
| // no need for global state with useContext or useReducer for this feature | |
| const[viewport, setViewport] = useState(INITIAL_VIEWPORT); | |
| // same thing for the pins | |
| const [userPosition, setUserPosition] = useState(null); | |
| useEffect(() => { | |
| getUserPosition() | |
| }, []); | |
| const getUserPosition = () => { | |
| if ("geolocation" in navigator) { // function from the window object | |
| navigator.geolocation.getCurrentPosition(position => { | |
| const { latitude, longitude } = position.coords; | |
| // spread the viewport object to keep the zoom value | |
| // set the viewport on the current user location if they accepts the location detection | |
| // we have the fallback INITIAL_VIEWPORT if they refuse | |
| setViewport({ ...viewport, latitude, longitude }); | |
| // setting the value for user position as well, which initially is null | |
| setUserPosition({ latitude, longitude }); | |
| }); | |
| } | |
| }; | |
| const getPins = async() => { | |
| // destructuring getPins from data | |
| const { getPins } = await client.request(GET_PINS_QUERY); | |
| dispatch({ type: GET_PINS, payload: getPins }) | |
| }; | |
| const [popup, setPopup] = useState(null); | |
| // remove pop-up for all users if the pin is removed by its author | |
| useEffect(() => { | |
| const pinExists = popup && state.pins.findIndex(pin => | |
| pin._id === popup._id | |
| ) > -1; | |
| // loop through the pin and check if the current one has an index in the array of pins | |
| // if so compare to -1 to turn it into a boolean | |
| if (!pinExists) { | |
| setPopup(null); | |
| } | |
| }, [state.pins.length]); // run when the number of pins has changed | |
| const handleMapClick = ({ lngLat, leftButton }) => { // props from the event object. left button is left click of the mouse | |
| // if no click, return | |
| if (!leftButton) return; | |
| if (popup) return; | |
| // if no draft yet i.e. if initial state of draft, create one with 0/0 as coordinates | |
| if(!state.draft) { | |
| dispatch({ type: CREATE_DRAFT }) | |
| } | |
| const [longitude, latitude] = lngLat; | |
| // update the draft location in the global state | |
| dispatch({ | |
| type: UPDATE_DRAFT_LOCATION, | |
| payload: { longitude, latitude } | |
| }) | |
| }; | |
| const highlightNewPin = pin => { | |
| const isNewPin = differenceInMinutes(Date.now(), Number(pin.createdAt)) <= 30; | |
| return isNewPin ? "limegreen" : "darkblue"; | |
| }; | |
| const isAuthUser = () => state.currentUser._id === popup.author._id; | |
| const handleDeletePin = async pin => { | |
| const variables = { pinId: pin._id }; | |
| const { deletePin } = await client.request(DELETE_PIN_MUTATION, variables); | |
| console.log('Pin deleted!', { deletePin }); | |
| // dispatch is moved to the subscriptions | |
| // dispatch({ type: DELETE_PIN, payload: deletePin }); | |
| setPopup(null); | |
| }; | |
| const handleSelectPin = pin => { | |
| setPopup(pin); | |
| dispatch({ type: SET_PIN, payload: pin }) | |
| }; | |
| return( | |
| <div className={mobileSize ? classes.rootMobile : classes.root}> | |
| <ReactMapGL | |
| width={'100vw'} | |
| height={'calc(100vh - 64px)'} | |
| mapStyle={'mapbox://styles/mapbox/streets-v9'} | |
| mapboxApiAccessToken={'MAPBOXTOKEN'} | |
| scrollZoom={!mobileSize} | |
| onViewportChange={newViewport => setViewport(newViewport)} | |
| onClick={handleMapClick} | |
| {...viewport} | |
| > | |
| {/*zoom controls*/} | |
| <div className={classes.navigationControl}> | |
| <NavigationControl | |
| onViewportChange={newViewport => setViewport(newViewport)} | |
| /> | |
| </div> | |
| {/*Current location*/} | |
| {userPosition && ( | |
| <Marker | |
| latitude={userPosition.latitude} | |
| longitude={userPosition.longitude} | |
| offsetLeft={-19} | |
| offsetRight={-37} | |
| > | |
| <PinIcon | |
| size={40} | |
| color={'red'} | |
| /> | |
| </Marker> | |
| )} | |
| {/*draft pin*/} | |
| {state.draft && ( | |
| <Marker | |
| latitude={state.draft.latitude} | |
| longitude={state.draft.longitude} | |
| offsetLeft={-19} | |
| offsetRight={-37} | |
| > | |
| <PinIcon | |
| size={40} | |
| color={'hotpink'} | |
| /> | |
| </Marker> | |
| )} | |
| {/*Display all pins*/} | |
| {state.pins.map(pin => | |
| (<Marker | |
| key={pin._id} | |
| latitude={pin.latitude} | |
| longitude={pin.longitude} | |
| offsetLeft={-19} | |
| offsetRight={-37} | |
| > | |
| <PinIcon | |
| onClick={() => handleSelectPin(pin)} | |
| size={40} | |
| color={highlightNewPin(pin)} | |
| /> | |
| </Marker>) | |
| )} | |
| {/*Pop up info*/} | |
| {popup && ( | |
| <Popup | |
| anchor={'top'} | |
| latitude={popup.latitude} | |
| longitude={popup.longitude} | |
| closeOnClick={false} | |
| onClose={() => setPopup(null)} | |
| > | |
| <img | |
| className={classes.popupImage} | |
| src={popup.image} | |
| alt={popup.title} | |
| /> | |
| <div className={classes.popupTab}> | |
| <Typography> | |
| {popup.latitude.toFixed(6)}, {popup.longitude.toFixed(6)} | |
| </Typography> | |
| {isAuthUser() && ( | |
| <Button onClick={() => handleDeletePin(popup)}> | |
| <DeleteIcon className={classes.deleteIcon} /> | |
| </Button> | |
| )} | |
| </div> | |
| </Popup> | |
| )} | |
| </ReactMapGL> | |
| {/*Subscription for creating/updating/deleting pins*/} | |
| <Subscription | |
| subscription={PIN_ADDED_SUBSCRIPTION} | |
| onSubscriptionData={({ subscriptionData }) => { | |
| const { pinAdded } = subscriptionData.data; | |
| dispatch({ type: CREATE_PIN, payload: pinAdded }) | |
| }} | |
| /> | |
| <Subscription | |
| subscription={PIN_UPDATED_SUBSCRIPTION} | |
| onSubscriptionData={({ subscriptionData }) => { | |
| const { pinUpdated } = subscriptionData.data; | |
| dispatch({ type: CREATE_COMMENT, payload: pinUpdated }) | |
| }} | |
| /> | |
| <Subscription | |
| subscription={PIN_DELETED_SUBSCRIPTION} | |
| onSubscriptionData={({ subscriptionData }) => { | |
| const { pinDeleted } = subscriptionData.data; | |
| dispatch({ type: DELETE_PIN, payload: pinDeleted }) | |
| }} | |
| /> | |
| {/*Blog*/} | |
| <Blog popup={popup}/> | |
| </div> | |
| ); | |
| }; | |
| const styles = { | |
| root: { | |
| display: "flex" | |
| }, | |
| rootMobile: { | |
| display: "flex", | |
| flexDirection: "column-reverse" | |
| }, | |
| navigationControl: { | |
| position: "absolute", | |
| top: 0, | |
| left: 0, | |
| margin: "1em" | |
| }, | |
| deleteIcon: { | |
| color: "red" | |
| }, | |
| popupImage: { | |
| padding: "0.4em", | |
| height: 200, | |
| width: 200, | |
| objectFit: "cover" | |
| }, | |
| popupTab: { | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| flexDirection: "column" | |
| } | |
| }; | |
| export default withStyles(styles)(Map); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Libs | |
| import { useState, useEffect } from "react"; | |
| import { GraphQLClient } from "graphql-request"; | |
| export const BASE_URL = | |
| process.env.NODE_ENV === "production" | |
| ? "https://geopins-mtl.herokuapp.com/graphql" | |
| : "http://localhost:4000/graphql"; | |
| export const useClient = () => { | |
| const [idToken, setIdToken] = useState(""); | |
| useEffect(() => { | |
| const token = window.gapi.auth2 | |
| .getAuthInstance() | |
| .currentUser.get() | |
| .getAuthResponse().id_token; | |
| setIdToken(token); | |
| }, []); | |
| return new GraphQLClient(BASE_URL, { | |
| headers: { authorization: idToken } | |
| }); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment