Skip to content

Commit

Permalink
useChannel hook
Browse files Browse the repository at this point in the history
  • Loading branch information
skyqrose committed Feb 27, 2020
1 parent a803b3c commit 6af1a33
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 185 deletions.
64 changes: 64 additions & 0 deletions assets/src/hooks/useChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Channel, Socket } from "phoenix"
import { useEffect, useState } from "react"
import { reload } from "../models/browser"

/** Opens a channel for the given topic
* and returns the latest data that's been pushed to it
*
* Only listens for a single event
* Assumes the data returned on join and the data from subsequent pushes are the same format
* If topic is null, does not open a channel.
* Returns offState if the channel is not open yet
* Returns loadingState after the channel has been opened but before receiving the first response
*/
export const useChannel = <T>({
socket,
topic,
event,
parser,
loadingState,
offState,
}: {
socket: Socket | undefined
topic: string | null
event: string
parser: (data: any) => T
loadingState: T
offState: T
}): T => {
const [result, setResult] = useState<T>(offState)

useEffect(() => {
let channel: Channel | undefined

if (socket !== undefined && topic !== null) {
setResult(loadingState)
channel = socket.channel(topic)
channel.on(event, ({ data: data }) => {
setResult(parser(data))
})
channel
.join()
.receive("ok", ({ data: data }) => {
setResult(parser(data))
})
.receive("error", ({ reason }) =>
// tslint:disable-next-line: no-console
console.error(`joining topic ${topic} failed`, reason)
)
.receive("timeout", () => {
reload(true)
})
} else {
setResult(offState)
}

return () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}
}, [socket, topic, event, loadingState, offState])
return result
}
36 changes: 10 additions & 26 deletions assets/src/hooks/useDataStatus.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
import { Channel, Socket } from "phoenix"
import { useState, useEffect } from "react"
import { reload } from "../models/browser"
import { Socket } from "phoenix"
import { useChannel } from "./useChannel"

export type DataStatus = "good" | "outage"

const topic = "data_status"

const useDataStatus = (socket: Socket | undefined) => {
const [state, setState] = useState<DataStatus>("good")

useEffect(() => {
if (socket !== undefined) {
const channel: Channel = socket.channel(topic)
channel.on("data_status", ({ data: status }) => {
setState(status)
})
channel
.join()
.receive("ok", ({ data: status }) => {
setState(status)
})
// tslint:disable-next-line: no-console
.receive("error", ({ reason }) => console.error("join failed", reason))
.receive("timeout", () => {
reload(true)
})
}
}, [socket])
return state
return useChannel<DataStatus>({
socket,
topic: "data_status",
event: "data_status",
parser: status => status,
loadingState: "good",
offState: "good",
})
}

export default useDataStatus
80 changes: 14 additions & 66 deletions assets/src/hooks/useSearchResults.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,29 @@
import { Channel, Socket } from "phoenix"
import { useEffect, useState } from "react"
import { reload } from "../models/browser"
import { Socket } from "phoenix"
import { SearchQuery } from "../models/searchQuery"
import {
VehicleOrGhostData,
vehicleOrGhostFromData,
} from "../models/vehicleData"
import { VehicleOrGhost } from "../realtime"
import { useChannel } from "./useChannel"

interface SearchResultsPayload {
data: VehicleOrGhostData[]
}

const subscribe = (
socket: Socket,
searchQuery: SearchQuery,
setVehicles: (vehicles: VehicleOrGhost[]) => void
): Channel => {
const handleSearchResults = (payload: SearchResultsPayload): void => {
setVehicles(
payload.data.map((data: VehicleOrGhostData) =>
vehicleOrGhostFromData(data)
)
)
}

const channel = socket.channel(
`vehicles:search:${searchQuery.property}:${searchQuery.text}`
)
channel.on("search", handleSearchResults)

channel
.join()
.receive("ok", handleSearchResults)
.receive("error", ({ reason }) =>
// tslint:disable-next-line: no-console
console.error("search channel join failed", reason)
)
.receive("timeout", () => {
reload(true)
})

return channel
}
const parser = (data: VehicleOrGhostData[]): VehicleOrGhost[] =>
data.map(vehicleOrGhostFromData)

const useSearchResults = (
socket: Socket | undefined,
searchQuery: SearchQuery | null
): VehicleOrGhost[] | null | undefined => {
const [vehicles, setVehicles] = useState<VehicleOrGhost[] | null | undefined>(
undefined
)

useEffect(() => {
let channel: Channel | undefined

const leaveChannel = () => {
if (channel !== undefined) {
channel.leave()
channel = undefined
}
}

if (searchQuery === null) {
leaveChannel()
setVehicles(undefined)
}

if (socket && searchQuery !== null) {
setVehicles(null)
channel = subscribe(socket, searchQuery, setVehicles)
}

return leaveChannel
}, [socket, searchQuery])

return vehicles
const topic: string | null =
searchQuery && `vehicles:search:${searchQuery.property}:${searchQuery.text}`
return useChannel<VehicleOrGhost[] | null | undefined>({
socket,
topic,
event: "search",
parser,
loadingState: null,
offState: undefined,
})
}

export default useSearchResults
95 changes: 11 additions & 84 deletions assets/src/hooks/useShuttleVehicles.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,19 @@
import { Channel, Socket } from "phoenix"
import { Dispatch, useEffect, useReducer } from "react"
import { reload } from "../models/browser"
import { Socket } from "phoenix"
import { VehicleData, vehicleFromData } from "../models/vehicleData"
import { Vehicle } from "../realtime"
import { useChannel } from "./useChannel"

interface State {
shuttles: Vehicle[] | null
channel?: Channel
}

const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_CHANNEL":
return { ...state, channel: action.payload.channel }

case "SET_SHUTTLES":
return { ...state, shuttles: action.payload.shuttles }
}
}

const initialState: State = {
shuttles: null,
}

interface SetShuttlesAction {
type: "SET_SHUTTLES"
payload: {
shuttles: Vehicle[]
}
}

const setShuttles = (shuttles: Vehicle[]): SetShuttlesAction => ({
type: "SET_SHUTTLES",
payload: { shuttles },
})

interface SetChannelAction {
type: "SET_CHANNEL"
payload: {
channel: Channel
}
}

const setChannel = (channel: Channel): SetChannelAction => ({
type: "SET_CHANNEL",
payload: { channel },
})

type Action = SetShuttlesAction | SetChannelAction

interface ChannelPayload {
data: VehicleData[]
}

const subscribe = (socket: Socket, dispatch: Dispatch<Action>): Channel => {
const handleShuttles = (payload: ChannelPayload): void => {
dispatch(setShuttles(payload.data.map(data => vehicleFromData(data))))
}

const channel = socket.channel("vehicles:shuttle:all")

channel.on("shuttles", handleShuttles)

channel
.join()
.receive("ok", handleShuttles)
.receive("error", ({ reason }) =>
// tslint:disable-next-line: no-console
console.error("shuttle vehicles join failed", reason)
)
.receive("timeout", () => {
reload(true)
})

return channel
}
const parser = (data: VehicleData[]): Vehicle[] => data.map(vehicleFromData)

const useShuttleVehicles = (socket: Socket | undefined): Vehicle[] | null => {
const [state, dispatch] = useReducer(reducer, initialState)
const { channel, shuttles } = state
useEffect(() => {
if (socket && !channel) {
const newChannel = subscribe(socket, dispatch)
dispatch(setChannel(newChannel))
}
}, [socket])

return shuttles
return useChannel<Vehicle[] | null>({
socket,
topic: "vehicles:shuttle:all",
event: "shuttles",
parser,
loadingState: null,
offState: null,
})
}

export default useShuttleVehicles
2 changes: 1 addition & 1 deletion assets/tests/hooks/useDataStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe("useDataStatus", () => {

renderHook(() => useDataStatus(mockSocket))

expect(spyConsoleError).toHaveBeenCalledWith("join failed", "ERROR_REASON")
expect(spyConsoleError).toHaveBeenCalled()
spyConsoleError.mockRestore()
})

Expand Down
5 changes: 1 addition & 4 deletions assets/tests/hooks/useSearchResults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,7 @@ describe("useSearchResults", () => {

renderHook(() => useSearchResults(mockSocket, searchQuery))

expect(spyConsoleError).toHaveBeenCalledWith(
"search channel join failed",
"ERROR_REASON"
)
expect(spyConsoleError).toHaveBeenCalled()
spyConsoleError.mockRestore()
})

Expand Down
5 changes: 1 addition & 4 deletions assets/tests/hooks/useShuttleVehicles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,7 @@ describe("useShuttleVehicles", () => {

renderHook(() => useShuttleVehicles(mockSocket))

expect(spyConsoleError).toHaveBeenCalledWith(
"shuttle vehicles join failed",
"ERROR_REASON"
)
expect(spyConsoleError).toHaveBeenCalled()
spyConsoleError.mockRestore()
})

Expand Down

0 comments on commit 6af1a33

Please sign in to comment.