Skip to content

Commit

Permalink
refactor(app): pass service through dependency injection (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kohei Asai authored Mar 28, 2021
1 parent 277c892 commit 2dc4587
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 69 deletions.
32 changes: 32 additions & 0 deletions components/service.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from "react";
import { ServiceContainer } from "../core/service-container";

const ServiceContainerContext = React.createContext<ServiceContainer | null>(
null
);

export function useService(): ServiceContainer {
const service = React.useContext(ServiceContainerContext);

if (!service) {
throw new Error("useService() is called outside <ServiceProvider>.");
}

return service;
}

interface ServiceProviderProps {
serviceContainer: ServiceContainer;
}

export const ServiceProvider: React.FC<ServiceProviderProps> = ({
serviceContainer,
children,
...props
}) => {
return (
<ServiceContainerContext.Provider value={serviceContainer} {...props}>
{children}
</ServiceContainerContext.Provider>
);
};
7 changes: 7 additions & 0 deletions core/service-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { I18nDictionaryService } from "../services/i18n-dictionary";
import { UserMonitoringService } from "../services/user-monitoring";

export interface ServiceContainer {
i18nDictionary: I18nDictionaryService;
userMonitoring: UserMonitoringService;
}
29 changes: 20 additions & 9 deletions core/test-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import mitt from "next/dist/next-server/lib/mitt";
import { RouterContext } from "next/dist/next-server/lib/router-context";
import * as React from "react";
import { IntlProvider } from "react-intl";
import { ServiceProvider } from "../components/service";
import { OriginProvider } from "../global-hooks/url";
import { EmptyI18nDictionaryService } from "../services/i18n-dictionary";
import { EmptyUserMonitoringService } from "../services/user-monitoring";
import { ServiceContainer } from "./service-container";

interface TestAppProps extends React.Attributes {
origin?: string;
Expand All @@ -25,6 +29,7 @@ interface TestAppProps extends React.Attributes {
locale?: string;
defaultLocale?: string;
};
serviceContainer?: Partial<ServiceContainer>;
}

export interface TestAppImperativeHandle {
Expand Down Expand Up @@ -57,6 +62,10 @@ export const TestApp = React.forwardRef<
reload = () => {},
} = {},
intl: { messages = {}, locale = "en-US", defaultLocale = "en-US" } = {},
serviceContainer: {
i18nDictionary = new EmptyI18nDictionaryService(),
userMonitoring = new EmptyUserMonitoringService(),
} = {},
children,
},
ref
Expand Down Expand Up @@ -149,15 +158,17 @@ export const TestApp = React.forwardRef<

return (
<RouterContext.Provider value={nextRouterMock}>
<OriginProvider origin={origin}>
<IntlProvider
messages={messages}
locale={locale}
defaultLocale={defaultLocale}
>
{children}
</IntlProvider>
</OriginProvider>
<ServiceProvider serviceContainer={{ i18nDictionary, userMonitoring }}>
<OriginProvider origin={origin}>
<IntlProvider
messages={messages}
locale={locale}
defaultLocale={defaultLocale}
>
{children}
</IntlProvider>
</OriginProvider>
</ServiceProvider>
</RouterContext.Provider>
);
}
Expand Down
85 changes: 41 additions & 44 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { mix, shade, tint, transparentize } from "polished";
import * as React from "react";
import { IntlConfig, IntlProvider } from "react-intl";
import TopLoadingBar from "react-top-loading-bar";
import { FALLBACK_LOCALE } from "../constants/locale";
import { OriginProvider } from "../global-hooks/url";
import { getIntlMessages } from "../services/translation";
import { ServiceProvider } from "../components/service";
import { ServiceContainer } from "../core/service-container";
import { IsomorphicI18nDictionaryService } from "../services/i18n-dictionary";
import { BrowserUserMonitoringService } from "../services/user-monitoring";

import "normalize.css/normalize.css";
import { FALLBACK_LOCALE } from "../constants/locale";

// initialize sentry client only when the env var is set
// you can comment out the env var in .env.local when you want to debug
Expand Down Expand Up @@ -47,6 +50,14 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
IntlConfig["messages"]
>(pageProps.intlMessages);

const serviceContainer = React.useMemo<ServiceContainer>(
() => ({
i18nDictionary: new IsomorphicI18nDictionaryService(),
userMonitoring: new BrowserUserMonitoringService(),
}),
[]
);

React.useEffect(() => {
const onRouteChangeStart = () => {
topLoadingBarRef.current.staticStart();
Expand All @@ -66,54 +77,40 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
}, []);

React.useEffect(() => {
if (typeof (globalThis as any).gtag === "function") {
(globalThis as any).gtag("event", "page_view", {
page_title: globalThis.document.title,
page_location: globalThis.location.href,
page_path: `${globalThis.location.pathname}?hl=${new URLSearchParams(
globalThis.location.search
).get("hl")}`,
});

const onRouteChangeComplete = () => {
(globalThis as any).gtag("event", "page_view", {
page_title: globalThis.document.title,
page_location: globalThis.location.href,
page_path: `${globalThis.location.pathname}?hl=${new URLSearchParams(
globalThis.location.search
).get("hl")}`,
});
};

router.events.on("routeChangeComplete", onRouteChangeComplete);

return () => {
router.events.off("routeChangeComplete", onRouteChangeComplete);
};
}
serviceContainer.userMonitoring.trackPageView();

return () => {};
}, []);
const onRouteChangeComplete = () => {
serviceContainer.userMonitoring.trackPageView();
};

router.events.on("routeChangeComplete", onRouteChangeComplete);

return () => {
router.events.off("routeChangeComplete", onRouteChangeComplete);
};
}, [serviceContainer.userMonitoring]);

React.useEffect(() => {
getIntlMessages({
locale: pageProps.locale ?? FALLBACK_LOCALE,
}).then((intlMessages) => setIntlMessages(intlMessages));
}, [pageProps.locale]);
serviceContainer.i18nDictionary
.fetch(pageProps.locale ?? FALLBACK_LOCALE)
.then((dictionary) => setIntlMessages(dictionary));
}, [serviceContainer.i18nDictionary, pageProps.locale]);

return (
<>
<OriginProvider origin={pageProps.origin}>
<IntlProvider
messages={intlMessages}
locale={pageProps.locale!}
defaultLocale={FALLBACK_LOCALE}
>
<TopLoadingBar color="#ff6b6b" ref={topLoadingBarRef} />

<Component {...pageProps} />
</IntlProvider>
</OriginProvider>
<ServiceProvider serviceContainer={serviceContainer}>
<OriginProvider origin={pageProps.origin}>
<IntlProvider
messages={intlMessages}
locale={pageProps.locale!}
defaultLocale={FALLBACK_LOCALE}
>
<TopLoadingBar color="#ff6b6b" ref={topLoadingBarRef} />

<Component {...pageProps} />
</IntlProvider>
</OriginProvider>
</ServiceProvider>
</>
);
};
Expand Down
4 changes: 2 additions & 2 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from "../helpers/i18n";
import { getOriginFromRequest } from "../helpers/next";
import { getIndexPageJson, getPostEntryListJson } from "../services/cms";
import { getIntlMessages } from "../services/translation";
import { IsomorphicI18nDictionaryService } from "../services/i18n-dictionary";

interface ServerSideProps extends CommonServerSideProps {
indexPage: NonNullable<PromiseValue<ReturnType<typeof getIndexPageJson>>>;
Expand Down Expand Up @@ -125,7 +125,7 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async ({

const origin = getOriginFromRequest(req);
const [intlMessages, posts, indexPage] = await Promise.all([
getIntlMessages({ locale }),
new IsomorphicI18nDictionaryService().fetch(locale),
getPostEntryListJson({ locale, previewToken }),
getIndexPageJson({ locale, previewToken }),
]);
Expand Down
4 changes: 2 additions & 2 deletions pages/posts/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from "../../helpers/i18n";
import { getOriginFromRequest } from "../../helpers/next";
import { getPostEntryListJson, getPostJson } from "../../services/cms";
import { getIntlMessages } from "../../services/translation";
import { IsomorphicI18nDictionaryService } from "../../services/i18n-dictionary";

interface ServerSideProps extends CommonServerSideProps {
post: NonNullable<PromiseValue<ReturnType<typeof getPostJson>>>;
Expand Down Expand Up @@ -163,7 +163,7 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async ({

const origin = getOriginFromRequest(req);
const [intlMessages, posts, post] = await Promise.all([
getIntlMessages({ locale }),
new IsomorphicI18nDictionaryService().fetch(locale),
getPostEntryListJson({ locale, previewToken }),
getPostJson({ slug, locale, previewToken }),
]);
Expand Down
19 changes: 19 additions & 0 deletions services/i18n-dictionary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export abstract class I18nDictionaryService {
abstract fetch(locale: string): Promise<Record<string, any>>;
}

export class EmptyI18nDictionaryService implements I18nDictionaryService {
async fetch(_locale: string): Promise<Record<string, any>> {
return {};
}
}

export class IsomorphicI18nDictionaryService implements I18nDictionaryService {
async fetch(locale: string): Promise<Record<string, any>> {
const response = await fetch(
`https://cdn.simplelocalize.io/${process.env.NEXT_PUBLIC_SIMPLE_LOCALIZE_TOKEN}/_latest/${locale}`
);

return await response.json();
}
}
12 changes: 0 additions & 12 deletions services/translation.ts

This file was deleted.

49 changes: 49 additions & 0 deletions services/user-monitoring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export abstract class UserMonitoringService {
abstract trackPageView(): void;

abstract trackUiEvent(actionName: string, value?: number): void;
}

export class EmptyUserMonitoringService implements UserMonitoringService {
trackPageView(): void {}

trackUiEvent(_actionName: string, _value?: number): void {}
}

export class BrowserUserMonitoringService implements UserMonitoringService {
constructor() {
this.gtag = (globalThis as any).gtag ? (globalThis as any).gtag : () => {};
}

private gtag: (...args: any) => {};

trackPageView(): void {
if (!globalThis.document) {
console.warn(
`gtag("event", "page_view") has been called in a non-browser context. It does nothing.`
);

return;
}

const url = new URL(globalThis.location.href);
const title = globalThis.document.title;
const href = url.href;
const search = url.searchParams.has("hl")
? `?hl=${url.searchParams.get("hl")}`
: "";

this.gtag("event", "page_view", {
page_title: title,
page_location: href,
page_path: url.pathname + search,
});
}

trackUiEvent(actionName: string, value?: number): void {
this.gtag("event", actionName, {
event_category: "ui_event",
value: value,
});
}
}

1 comment on commit 2dc4587

@vercel
Copy link

@vercel vercel bot commented on 2dc4587 Mar 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.