Skip to content
This repository has been archived by the owner on Feb 2, 2024. It is now read-only.

Commit

Permalink
chore: switch to apollo client (#15)
Browse files Browse the repository at this point in the history
Co-authored-by: Samer Buna <[email protected]>
  • Loading branch information
samerbuna and samerbuna authored Dec 27, 2021
1 parent 7b1c850 commit cf2ade2
Show file tree
Hide file tree
Showing 22 changed files with 243 additions and 133 deletions.
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"dev:codegen": "graphql-codegen --config codegen.yml"
},
"dependencies": {
"@urql/core": "^2.3.6",
"@apollo/client": "^3.5.6",
"autoprefixer": "^10.4.0",
"body-parser": "^1.19.1",
"cookie-session": "^2.0.0",
Expand All @@ -48,14 +48,12 @@
"postcss-nested": "^5.0.6",
"react": "^18.0.0-rc.0-next-20212349a-20211223",
"react-dom": "^18.0.0-rc.0-next-20212349a-20211223",
"react-ssr-prepass": "^1.4.0",
"regenerator-runtime": "^0.13.9",
"serialize-javascript": "^6.0.0",
"source-map-loader": "^3.0.0",
"style-loader": "^3.3.1",
"ts-loader": "^9.2.6",
"typescript": "^4.5.4",
"urql": "^2.0.6",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
},
Expand Down
7 changes: 3 additions & 4 deletions src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { useAppState } from "store"

import useAuthToken from "store/use-auth-token"
import Balance from "./balance"
import Link from "./link"
import Logout from "./logout"

const Header = ({ balance }: { balance: number }) => {
const { authToken } = useAppState()
const { hasToken } = useAuthToken()

return (
<div className="header">
<Balance balance={balance} />
{authToken ? <Logout /> : <Link to="/login">Login</Link>}
{hasToken ? <Logout /> : <Link to="/login">Login</Link>}
</div>
)
}
Expand Down
13 changes: 6 additions & 7 deletions src/components/home.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { useQuery } from "urql"
import { useQuery } from "@apollo/client"

import { useAppState } from "store"
import QUERY_ME from "store/graphql/query.me"
import useAuthToken from "store/use-auth-token"

import Header from "./header"

const Home = () => {
const { authToken } = useAppState()
const { hasToken } = useAuthToken()

const [result] = useQuery({
query: QUERY_ME,
variables: { hasToken: Boolean(authToken) },
const { data } = useQuery(QUERY_ME, {
variables: { hasToken },
})

const me = result?.data?.me
const me = data?.me
const balance = me?.defaultAccount?.wallets?.[0]?.balance ?? 0

return (
Expand Down
5 changes: 4 additions & 1 deletion src/components/login.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useApolloClient } from "@apollo/client"
import intlTelInput from "intl-tel-input"
import React, { useCallback, useRef, useState } from "react"

import config from "server/config"
import { history, useRequest } from "store"
import { setCachedAuthToken } from "store/use-auth-token"

const PhoneNumber = ({ onSuccess }: { onSuccess: (arg: string) => void }) => {
const iti = useRef<intlTelInput.Plugin | null>(null)
Expand Down Expand Up @@ -55,6 +57,7 @@ const PhoneNumber = ({ onSuccess }: { onSuccess: (arg: string) => void }) => {
}

const AuthCode = ({ phoneNumber }: { phoneNumber: string }) => {
const client = useApolloClient()
const request = useRequest()
const [errorMessage, setErrorMessage] = useState("")

Expand All @@ -73,7 +76,7 @@ const AuthCode = ({ phoneNumber }: { phoneNumber: string }) => {
setErrorMessage(data.message)
return
}

setCachedAuthToken(client)(data?.authToken)
history.push("/", { authToken: data?.authToken })
}

Expand Down
3 changes: 3 additions & 0 deletions src/components/logout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { useApolloClient } from "@apollo/client"
import { useAppDispatcher, useRequest } from "store"

const Logout = () => {
const client = useApolloClient()
const request = useRequest()
const dispatch = useAppDispatcher()

const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = async (event) => {
event.preventDefault()
await request.post("/api/logout")
client.clearStore()
dispatch({ type: "logout" })
}

Expand Down
5 changes: 0 additions & 5 deletions src/components/root-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ import { Suspense } from "react"
import appRoutes, { SupportedRoutes } from "server/routes"

const RootComponent = ({ path }: { path: RoutePath }) => {
if (!path) {
throw new Error("MISSING_ROOT_PATH")
}

const checkedRoutePath = SupportedRoutes.find(
(supportedRoute) => supportedRoute === path,
)

if (!checkedRoutePath) {
throw new Error("INVALID_ROOT_PATH")
}
Expand Down
31 changes: 0 additions & 31 deletions src/components/root-provider.tsx

This file was deleted.

13 changes: 11 additions & 2 deletions src/components/root.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import "@testing-library/jest-dom/extend-expect"

import { render } from "@testing-library/react"
import { MockedProvider } from "@apollo/client/testing"

import Root from "./root"

describe("Root", () => {
it("renders Home and matches snapshot", () => {
const { asFragment } = render(<Root initialState={{ path: "/" }} />)
const { asFragment } = render(
<MockedProvider>
<Root initialState={{ path: "/" }} />
</MockedProvider>,
)
expect(asFragment()).toMatchSnapshot()
})
it("renders Login and matches snapshot", () => {
const { asFragment } = render(<Root initialState={{ path: "/login" }} />)
const { asFragment } = render(
<MockedProvider>
<Root initialState={{ path: "/login" }} />
</MockedProvider>,
)
expect(asFragment()).toMatchSnapshot()
})
})
7 changes: 3 additions & 4 deletions src/components/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import { useEffect, useReducer } from "react"

import { GwwContext, history } from "store"
import mainReducer from "store/reducer"

import RootProvider from "./root-provider"
import RootComponent from "./root-component"

const Root = ({ initialState }: { initialState: InitialState }) => {
const [state, dispatch] = useReducer(mainReducer, initialState)

useEffect(() => {
const unlisten = history.listen(({ location }) => {
dispatch({
type: "state",
type: "navigate",
path: location.pathname,
...(location.state as Record<string, unknown> | null),
})
Expand All @@ -21,7 +20,7 @@ const Root = ({ initialState }: { initialState: InitialState }) => {

return (
<GwwContext.Provider value={{ state, dispatch }}>
<RootProvider />
<RootComponent path={state.path} />
</GwwContext.Provider>
)
}
Expand Down
16 changes: 10 additions & 6 deletions src/renderers/dom.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import * as ReactDOM from "react-dom"

import Root from "components/root"
import { ApolloProvider } from "@apollo/client"

import "../styles/index.css"
import { ssr } from "store"

import Root from "components/root"
import client from "store/client"

const container = document.getElementById("root")

if (!container) {
throw new Error("HTML_ROOT_ELEMENT_IS_MISSING")
}

ssr.restoreData(window.__G_DATA.ssrData)

ReactDOM.hydrateRoot(container, <Root initialState={window.__G_DATA.initialState} />)
ReactDOM.hydrateRoot(
container,
<ApolloProvider client={client}>
<Root initialState={window.__G_DATA.initialState} />
</ApolloProvider>,
)
36 changes: 27 additions & 9 deletions src/renderers/server.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import * as ReactDOMServer from "react-dom/server"
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink,
} from "@apollo/client"
import { Request } from "express"
import prepass from "react-ssr-prepass"

import config from "server/config"
import appRoutes from "server/routes"
import Root from "components/root"
import { ssr } from "store"
import { renderToStringWithData } from "@apollo/client/react/ssr"

export const serverRenderer =
(req: Request) =>
Expand All @@ -16,18 +21,31 @@ export const serverRenderer =
authToken,
}

const element = <Root initialState={initialState} />

await prepass(element)
const cache = new InMemoryCache()
const client = new ApolloClient({
ssrMode: true,
link: createHttpLink({
uri: config.graphqlUri,
headers: {
authorization: authToken ? `Bearer ${authToken}` : "",
},
}),
cache,
})

const initialMarkup = ReactDOMServer.renderToString(element)
const App = (
<ApolloProvider client={client}>
<Root initialState={initialState} />
</ApolloProvider>
)

const ssrData = ssr.extractData()
const initialMarkup = await renderToStringWithData(App)
const ssrData = client.extract()

return Promise.resolve({
initialState,
ssrData,
initialMarkup,
ssrData,
pageData: appRoutes[path],
})
}
13 changes: 6 additions & 7 deletions src/server/api-router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import "cross-fetch/polyfill" // The URQL client depends on fetch
import "cross-fetch/polyfill" // The Apollo client depends on fetch
import express from "express"
import MUTATION_USER_LOGIN from "store/graphql/mutation.user-login"
import client from "./graphql"
Expand All @@ -13,13 +13,12 @@ apiRouter.post("/login", async (req, res) => {
throw new Error("INVALID_LOGIN_REQUEST")
}

const { error, data } = await client(req)
.mutation(MUTATION_USER_LOGIN, {
input: { phone: phoneNumber, code: authCode },
})
.toPromise()
const { data } = await client(req).mutate({
mutation: MUTATION_USER_LOGIN,
variables: { input: { phone: phoneNumber, code: authCode } },
})

if (error || data?.userLogin?.errors?.length > 0 || !data?.userLogin?.authToken) {
if (data?.userLogin?.errors?.length > 0 || !data?.userLogin?.authToken) {
throw new Error(data?.userLogin?.errors?.[0].message || "SOMETHING_WENT_WRONG")
}

Expand Down
23 changes: 13 additions & 10 deletions src/server/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { createClient } from "@urql/core"
import { Request } from "express"
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"

import config from "./config"

const client = (req: Request) =>
createClient({
url: config.graphqlUri,
fetchOptions: () => {
const token = req.session?.authToken
return {
headers: { authorization: token ? `Bearer ${token}` : "" },
}
},
const client = (req: Request) => {
const authToken = req.session?.authToken
return new ApolloClient({
ssrMode: true,
link: createHttpLink({
uri: config.graphqlUri,
headers: {
authorization: authToken ? `Bearer ${authToken}` : "",
},
}),
cache: new InMemoryCache(),
})
}

export default client
30 changes: 30 additions & 0 deletions src/store/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ApolloClient, InMemoryCache, ApolloLink, from, HttpLink } from "@apollo/client"

import config from "server/config"
import { getCachedAuthToken, setCachedAuthToken } from "./use-auth-token"

export const cache = new InMemoryCache().restore(window.__G_DATA.ssrData)

const authLink = new ApolloLink((operation, forward) => {
const user = getCachedAuthToken(cache)
operation.setContext(({ headers }: { headers: Record<string, string> }) => ({
headers: {
authorization: user ? `Bearer ${user.authToken}` : "",
...headers,
},
}))
return forward(operation)
})

const httpLink = new HttpLink({ uri: config.graphqlUri })

const client = new ApolloClient({
cache,
link: from([authLink, httpLink]),
})

if (window.__G_DATA.initialState.authToken) {
setCachedAuthToken(client)(window.__G_DATA.initialState.authToken)
}

export default client
2 changes: 1 addition & 1 deletion src/store/graphql/mutation.user-login.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { gql } from "urql"
import { gql } from "@apollo/client"

const MUTATION_USER_LOGIN = gql`
mutation userLogin($input: UserLoginInput!) {
Expand Down
Loading

0 comments on commit cf2ade2

Please sign in to comment.