Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: sideba rerender by re-implement apollo cache #257

Merged
merged 1 commit into from
Nov 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@types/uuid": "^8.3.4",
"axios": "^0.27.2",
"classnames": "^2.3.1",
"deepmerge": "^4.2.2",
"google-protobuf": "^3.20.1-rc.1",
"graphql": "^16.6.0",
"graphql-request": "^5.0.0",
Expand Down
29 changes: 8 additions & 21 deletions web/ui/src/containers/clusters/ClusterSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { CampusClusterMapData, CampusNames } from '@components/ClusterMap';
import Search from '@components/Search';
import useSidebar, { Menu, MenuCategory, MenuItem } from '@components/Sidebar';
import { useClusterSidebarDataQuery } from '@graphql.d';
import { LocalStorageKeys } from '@lib/localStorageKeys';
import { isFirstLoading } from '@lib/apollo';
import '@lib/prototypes/string';
import { clusterURL } from '@lib/searchEngine';
import useLocalStorage from '@lib/useLocalStorage';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
import React from 'react';

/**
* ClusterSidebar is the sidebar for the cluster page. It contains the cluster
Expand All @@ -28,7 +27,7 @@ export const ClusterSidebar = ({
const router = useRouter();
const campusKeys = Object.keys(CampusClusterMapData) as Array<CampusNames>;

const { data: { me, locationsStatsByPrefixes = [] } = {}, loading } =
const { data: { me, locationsStatsByPrefixes = [] } = {}, networkStatus } =
useClusterSidebarDataQuery({
variables: {
campusName: campus,
Expand All @@ -37,7 +36,7 @@ export const ClusterSidebar = ({
).filter((e) => e !== '_data'),
},
});
const myCampusNameLowerFromAPI = me?.currentCampus?.name?.toLowerCase() || '';
const myCampusName = me?.currentCampus?.name?.toLowerCase() || '';
const freePlacesPerCluster: { [key: string]: number } =
locationsStatsByPrefixes
.map((l) => {
Expand All @@ -51,18 +50,7 @@ export const ClusterSidebar = ({
{}
);

const [myCampusNameCached, setMyCampusName] = useLocalStorage(
LocalStorageKeys.MyCurrentCampusName,
''
);

useEffect(() => {
if (myCampusNameCached !== myCampusNameLowerFromAPI) {
setMyCampusName(myCampusNameLowerFromAPI);
}
}, [myCampusNameLowerFromAPI]); // eslint-disable-line react-hooks/exhaustive-deps

if (loading && !myCampusNameCached) {
if (isFirstLoading(networkStatus)) {
return (
<Sidebar>
<div className="animate-pulse flex w-full flex-col">
Expand Down Expand Up @@ -106,16 +94,15 @@ export const ClusterSidebar = ({
.sort((a, b) => {
// Sort the campus list in alphabetical order and put the current
// campus at the top.
return a?.equalsIgnoreCase(myCampusNameCached)
return a?.equalsIgnoreCase(myCampusName)
? -1
: b?.equalsIgnoreCase(myCampusNameCached)
: b?.equalsIgnoreCase(myCampusName)
? 1
: a.localeCompare(b);
})
.map((campusName) => {
const campusData = CampusClusterMapData[campusName]._data;
const isMyCampus =
myCampusNameCached?.equalsIgnoreCase(campusName);
const isMyCampus = campusName?.equalsIgnoreCase(myCampusName);
const activeCampus = campus == campusName;

return (
Expand Down
152 changes: 94 additions & 58 deletions web/ui/src/lib/apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,69 +9,105 @@ import {
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import merge from 'deepmerge';
import Cookies from 'js-cookie';
import { GetServerSidePropsContext } from 'next';
import { NextRequest } from 'next/server'; // eslint-disable-line

export type ServerSideRequest = NextRequest | GetServerSidePropsContext['req'];
import { useMemo } from 'react';
import { ServerSideRequest } from 'types/next';

/**
* the token cookie name is used to store the token in the cookie and to read it
* from the cookie
*/
const tokenCookieName = '__s42.auth-token';

const errorLink = onError(({ graphQLErrors }) => {
if (graphQLErrors)
graphQLErrors.forEach(
({ message, locations, path }) =>
new Error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
});

const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_API,
credentials: 'include',
});

const authLink = setContext((_, context) => {
let authToken = Cookies.get(tokenCookieName);

if (!authToken) {
authToken = context.authToken;
}
// Reuse client on the client-side.
let apolloClient: ApolloClient<any>;

return {
headers: {
...context.headers,
Authorization: authToken ? `Bearer ${authToken}` : '',
},
};
});
/**
* Creates and configures the ApolloClient instance.
* @returns {ApolloClient} The ApolloClient instance.
*/
const createApolloClient = () => {
const errorLink = onError(({ graphQLErrors }) => {
if (graphQLErrors)
graphQLErrors.forEach(
({ message, locations, path }) =>
new Error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
});

const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_API,
credentials: 'include',
});

const authLink = setContext((_, context) => {
let authToken = Cookies.get(tokenCookieName);

if (!authToken) {
authToken = context.authToken;
}

return {
headers: {
...context.headers,
Authorization: authToken ? `Bearer ${authToken}` : '',
},
};
});

return new ApolloClient({
link: from([authLink, errorLink, httpLink]),
version: process.env.NEXT_PUBLIC_VERSION,
ssrMode: typeof window === 'undefined',
connectToDevTools: process.env.NODE_ENV === 'development',
cache: new InMemoryCache(),
credentials: 'include',
defaultOptions: {},
});
};

/**
* Definition of the Apollo client used in the application.
* Creates and initialize a new Apollo Client instance with an optional initial
* state.
* @param initialState The initial state to hydrate the client with.
* @returns The new Apollo Client instance.
*/
export const apolloClient = new ApolloClient({
link: from([authLink, errorLink, httpLink]),
version: process.env.NEXT_PUBLIC_VERSION,
ssrMode: typeof window === 'undefined',
connectToDevTools: process.env.NODE_ENV === 'development',
cache: new InMemoryCache(),
credentials: 'include',
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: typeof window === 'undefined' ? 'no-cache' : 'network-only',
errorPolicy: 'all',
},
mutate: {
fetchPolicy: typeof window === 'undefined' ? 'no-cache' : 'network-only',
errorPolicy: 'all',
},
},
});
export const initializeApollo = (initialState: any = null) => {
const client = apolloClient ?? createApolloClient();

// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// get hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = client.extract();

// Merge the existing cache into data passed from getStaticProps/getServerSideProps
const data = merge(initialState, existingCache);

// Restore the cache with the merged data
client.cache.restore(data);
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return client;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = client;

return client;
};

/** This function is used to get the initialized Apollo Client or initialize a
* new one if it doesn't exist. The client is memorized to prevent it from
* being reinitialized on every request on the client side with Memo.
*
* This is the default function to use when you want to use Apollo Client.
*/
export const useApollo = (initialState: any) => {
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
};

/**
* This function is used to authenticate a server-side request with the
Expand All @@ -82,14 +118,15 @@ export const queryAuthenticatedSSR = async <T = any>(
opts: QueryOptions<T>
): Promise<ApolloQueryResult<T>> => {
const { query, context, ...rest } = opts;
const client = createApolloClient();

const token =
typeof req.cookies?.get === 'function'
? req.cookies.get(tokenCookieName)
: // @ts-ignore
req.cookies[tokenCookieName];

return apolloClient.query<T>({
return client.query<T, any>({
query,
context: {
authToken: token,
Expand All @@ -98,7 +135,6 @@ export const queryAuthenticatedSSR = async <T = any>(
...rest,
});
};

/**
* Retrieve if the current network status is a refetch event or not
* @param networkStatus - the network status of apollo query
Expand All @@ -121,4 +157,4 @@ export const isFirstLoading = (networkStatus: NetworkStatus): boolean => {
);
};

export default apolloClient;
export default useApollo;
17 changes: 10 additions & 7 deletions web/ui/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { ApolloProvider } from '@apollo/client';
import useNotification from '@components/Notification';
import { Theme } from '@graphql.d';
import apolloClient from '@lib/apollo';
import { useApollo } from '@lib/apollo';
import useSettings, { useTheme } from '@lib/useSettings';
import { NextComponentType, NextPageContext } from 'next';
import { SessionProvider, SessionProviderProps } from 'next-auth/react';
import { AppProps } from 'next/app';
import Script from 'next/script';
Expand All @@ -13,16 +12,20 @@ import '../styles/globals.css';
const Interface = ({
Component,
session,
pageProps,
}: AppProps & {
Component: NextComponentType<NextPageContext, any, {}>;
} & SessionProviderProps) => {
pageProps: props,
}: AppProps & SessionProviderProps) => {
const { initialApolloState, ...pageProps } = props;
const apolloClient = useApollo(initialApolloState);

const { NotificationProvider, NotificationContainer } = useNotification();
// eslint-disable-next-line react-hooks/exhaustive-deps
const MemorizedComponent = useMemo(() => Component, [pageProps]);
const [settings] = useSettings({ apolloClient });
useTheme(settings.theme || Theme.AUTO);

// Use the layout defined at the page level, if available
const getLayout = MemorizedComponent.getLayout || ((page) => page);

return (
<SessionProvider
session={session}
Expand All @@ -31,7 +34,7 @@ const Interface = ({
>
<ApolloProvider client={apolloClient}>
<NotificationProvider>
<MemorizedComponent {...pageProps} />
{getLayout(<MemorizedComponent {...pageProps} />)}
<NotificationContainer />
</NotificationProvider>
</ApolloProvider>
Expand Down
19 changes: 17 additions & 2 deletions web/ui/src/types/next.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import type { NextComponentType, NextPageContext } from 'next';
import type {
GetServerSidePropsContext,
NextComponentType,
NextPageContext,
} from 'next';
import type { Session } from 'next-auth';
import type { Router } from 'next/router';
import { NextRequest } from 'next/server';

export type ServerSideRequest = NextRequest | GetServerSidePropsContext['req'];

declare module 'next' {
type NextPage<P = {}, IP = P> = React.ComponentType<P> & {
getInitialProps?(context: NextPageContext): IP | Promise<IP>;
getLayout?: (page: ReactElement) => ReactNode;
};
}
declare module 'next/app' {
type AppProps<P = Record<string, unknown>> = {
Component: NextComponentType<NextPageContext, any, P>;
Component: NextComponentType<NextPageContext, any, P> & {
getLayout?: (page: ReactElement) => ReactNode;
};
router: Router;
__N_SSG?: boolean;
__N_SSP?: boolean;
Expand Down