Skip to content

Commit

Permalink
fix: sideba rerender by re-implement apollo cache (#257)
Browse files Browse the repository at this point in the history
**Describe the pull request**

The sidebar is rerendered on each page switch du to not implementing
apollo initial state cache management. Re-implement the logic and start
the implementation of NextJS Layout will fix the problem.

**Checklist**

- [x] I have linked the relative issue to this pull request
- [x] I have made the modifications or added tests related to my PR
- [x] I have added/updated the documentation for my RP
- [x] I put my PR in Ready for Review only when all the checklist is
checked

**Breaking changes ?**
no
  • Loading branch information
42atomys authored Nov 20, 2022
1 parent ae23cee commit a485e8b
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 88 deletions.
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

0 comments on commit a485e8b

Please sign in to comment.