perf: optimize load of pagination items with caching
This commit is contained in:
parent
19fc29ad47
commit
ad64f1c571
@ -22,12 +22,15 @@ export const JoinGuildsPublic: React.FC = () => {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
authentication.socket.on('guilds', (data: SocketData<GuildPublicType>) => {
|
||||
authentication?.socket?.on(
|
||||
'guilds',
|
||||
(data: SocketData<GuildPublicType>) => {
|
||||
handleSocketData({ data, setItems })
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return () => {
|
||||
authentication.socket.off('guilds')
|
||||
authentication?.socket?.off('guilds')
|
||||
}
|
||||
}, [authentication.socket, setItems])
|
||||
|
||||
|
@ -72,7 +72,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
|
||||
} else {
|
||||
try {
|
||||
const { data } = await api.post<Tokens>('/users/signin', formData)
|
||||
const authentication = new AuthenticationClass(data)
|
||||
const authentication = new AuthenticationClass(data, true)
|
||||
authentication.signin()
|
||||
await router.push('/application')
|
||||
return null
|
||||
|
@ -24,7 +24,7 @@ export const AuthenticationSocialMedia: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const data = router.query
|
||||
if (isTokens(data)) {
|
||||
const authentication = new Authentication(data)
|
||||
const authentication = new Authentication(data, true)
|
||||
authentication.signin()
|
||||
router.push('/application').catch(() => {})
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { useAuthentication } from '../tools/authentication'
|
||||
import { Channel, ChannelWithDefaultChannelId } from '../models/Channel'
|
||||
import { GuildsChannelsPath } from '../components/Application'
|
||||
import { handleSocketData, SocketData } from '../tools/handleSocketData'
|
||||
import { CacheKey, CHANNELS_CACHE_KEY } from '../tools/cache'
|
||||
|
||||
export interface Channels {
|
||||
channels: Channel[]
|
||||
@ -27,6 +28,8 @@ export const ChannelsProvider: React.FC<
|
||||
const router = useRouter()
|
||||
const { authentication } = useAuthentication()
|
||||
|
||||
const cacheKey: CacheKey = `${path.guildId}-${CHANNELS_CACHE_KEY}`
|
||||
|
||||
const {
|
||||
items: channels,
|
||||
hasMore,
|
||||
@ -35,14 +38,15 @@ export const ChannelsProvider: React.FC<
|
||||
setItems
|
||||
} = usePagination<Channel>({
|
||||
api: authentication.api,
|
||||
url: `/guilds/${path.guildId}/channels`
|
||||
url: `/guilds/${path.guildId}/channels`,
|
||||
cacheKey
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
authentication.socket.on(
|
||||
authentication?.socket?.on(
|
||||
'channels',
|
||||
async (data: SocketData<ChannelWithDefaultChannelId>) => {
|
||||
handleSocketData({ data, setItems })
|
||||
handleSocketData({ data, setItems, cacheKey })
|
||||
if (data.action === 'delete') {
|
||||
await router.push(
|
||||
`/application/${path.guildId}/${data.item.defaultChannelId}`
|
||||
@ -52,9 +56,9 @@ export const ChannelsProvider: React.FC<
|
||||
)
|
||||
|
||||
return () => {
|
||||
authentication.socket.off('channels')
|
||||
authentication?.socket?.off('channels')
|
||||
}
|
||||
}, [authentication.socket, path.guildId, router, setItems])
|
||||
}, [authentication.socket, path.guildId, router, setItems, cacheKey])
|
||||
|
||||
useEffect(() => {
|
||||
resetPagination()
|
||||
|
@ -47,7 +47,7 @@ export const GuildMemberProvider: React.FC<
|
||||
}, [path, authentication.api])
|
||||
|
||||
useEffect(() => {
|
||||
authentication.socket.on(
|
||||
authentication?.socket?.on(
|
||||
'guilds',
|
||||
async (data: SocketData<GuildWithDefaultChannelId>) => {
|
||||
if (data.item.id === path.guildId) {
|
||||
@ -72,7 +72,7 @@ export const GuildMemberProvider: React.FC<
|
||||
)
|
||||
|
||||
return () => {
|
||||
authentication.socket.off('guilds')
|
||||
authentication?.socket?.off('guilds')
|
||||
}
|
||||
}, [authentication.socket, path.guildId, router])
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { NextPage, usePagination } from '../hooks/usePagination'
|
||||
import { useAuthentication } from '../tools/authentication'
|
||||
import { GuildWithDefaultChannelId } from '../models/Guild'
|
||||
import { handleSocketData, SocketData } from '../tools/handleSocketData'
|
||||
import { GUILDS_CACHE_KEY } from '../tools/cache'
|
||||
|
||||
export interface Guilds {
|
||||
guilds: GuildWithDefaultChannelId[]
|
||||
@ -29,19 +30,20 @@ export const GuildsProvider: React.FC<React.PropsWithChildren<{}>> = (
|
||||
setItems
|
||||
} = usePagination<GuildWithDefaultChannelId>({
|
||||
api: authentication.api,
|
||||
url: '/guilds'
|
||||
url: '/guilds',
|
||||
cacheKey: GUILDS_CACHE_KEY
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
authentication.socket.on(
|
||||
authentication?.socket?.on(
|
||||
'guilds',
|
||||
(data: SocketData<GuildWithDefaultChannelId>) => {
|
||||
handleSocketData({ data, setItems })
|
||||
handleSocketData({ data, setItems, cacheKey: GUILDS_CACHE_KEY })
|
||||
}
|
||||
)
|
||||
|
||||
return () => {
|
||||
authentication.socket.off('guilds')
|
||||
authentication?.socket?.off('guilds')
|
||||
}
|
||||
}, [authentication.socket, setItems])
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { MemberWithPublicUser } from '../models/Member'
|
||||
import { GuildsChannelsPath } from '../components/Application'
|
||||
import { handleSocketData, SocketData } from '../tools/handleSocketData'
|
||||
import { User } from '../models/User'
|
||||
import { CacheKey, MEMBERS_CACHE_KEY } from '../tools/cache'
|
||||
|
||||
export interface Members {
|
||||
members: MemberWithPublicUser[]
|
||||
@ -27,6 +28,8 @@ export const MembersProviders: React.FC<
|
||||
|
||||
const { authentication } = useAuthentication()
|
||||
|
||||
const cacheKey: CacheKey = `${path.guildId}-${MEMBERS_CACHE_KEY}`
|
||||
|
||||
const {
|
||||
items: members,
|
||||
hasMore,
|
||||
@ -35,18 +38,19 @@ export const MembersProviders: React.FC<
|
||||
setItems
|
||||
} = usePagination<MemberWithPublicUser>({
|
||||
api: authentication.api,
|
||||
url: `/guilds/${path.guildId}/members`
|
||||
url: `/guilds/${path.guildId}/members`,
|
||||
cacheKey
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
authentication.socket.on(
|
||||
authentication?.socket?.on(
|
||||
'members',
|
||||
(data: SocketData<MemberWithPublicUser>) => {
|
||||
handleSocketData({ data, setItems })
|
||||
handleSocketData({ data, setItems, cacheKey })
|
||||
}
|
||||
)
|
||||
|
||||
authentication.socket.on('users', (data: SocketData<User>) => {
|
||||
authentication?.socket?.on('users', (data: SocketData<User>) => {
|
||||
setItems((oldItems) => {
|
||||
const newItems = [...oldItems]
|
||||
switch (data.action) {
|
||||
@ -65,10 +69,10 @@ export const MembersProviders: React.FC<
|
||||
})
|
||||
|
||||
return () => {
|
||||
authentication.socket.off('members')
|
||||
authentication.socket.off('users')
|
||||
authentication?.socket?.off('members')
|
||||
authentication?.socket?.off('users')
|
||||
}
|
||||
}, [authentication.socket, setItems])
|
||||
}, [authentication.socket, setItems, cacheKey])
|
||||
|
||||
useEffect(() => {
|
||||
resetPagination()
|
||||
|
@ -5,6 +5,7 @@ import { useAuthentication } from '../tools/authentication'
|
||||
import { MessageWithMember } from '../models/Message'
|
||||
import { GuildsChannelsPath } from '../components/Application'
|
||||
import { handleSocketData, SocketData } from '../tools/handleSocketData'
|
||||
import { CacheKey, MESSAGES_CACHE_KEY } from '../tools/cache'
|
||||
|
||||
export interface Messages {
|
||||
messages: MessageWithMember[]
|
||||
@ -25,6 +26,8 @@ export const MessagesProvider: React.FC<
|
||||
const { path, children } = props
|
||||
const { authentication, user } = useAuthentication()
|
||||
|
||||
const cacheKey: CacheKey = `${path.channelId}-${MESSAGES_CACHE_KEY}`
|
||||
|
||||
const {
|
||||
items: messages,
|
||||
hasMore,
|
||||
@ -34,11 +37,12 @@ export const MessagesProvider: React.FC<
|
||||
} = usePagination<MessageWithMember>({
|
||||
api: authentication.api,
|
||||
url: `/channels/${path.channelId}/messages`,
|
||||
inverse: true
|
||||
inverse: true,
|
||||
cacheKey
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
authentication.socket.on(
|
||||
authentication?.socket?.on(
|
||||
'messages',
|
||||
(data: SocketData<MessageWithMember>) => {
|
||||
if (data.item.channelId === path.channelId) {
|
||||
@ -48,7 +52,7 @@ export const MessagesProvider: React.FC<
|
||||
const isAtBottom =
|
||||
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
|
||||
messagesDiv.clientHeight
|
||||
handleSocketData({ data, setItems })
|
||||
handleSocketData({ data, setItems, cacheKey })
|
||||
if (
|
||||
data.action === 'create' &&
|
||||
(isAtBottom || data.item.member.userId === user.id)
|
||||
@ -60,9 +64,9 @@ export const MessagesProvider: React.FC<
|
||||
)
|
||||
|
||||
return () => {
|
||||
authentication.socket.off('messages')
|
||||
authentication?.socket?.off('messages')
|
||||
}
|
||||
}, [authentication.socket, setItems, path, user.id])
|
||||
}, [authentication.socket, setItems, path, user.id, cacheKey])
|
||||
|
||||
useEffect(() => {
|
||||
resetPagination()
|
||||
|
@ -2,6 +2,11 @@ import { useState, useRef, useCallback } from 'react'
|
||||
import { AxiosInstance } from 'axios'
|
||||
|
||||
import { FetchState } from './useFetchState'
|
||||
import {
|
||||
CacheKey,
|
||||
getPaginationCache,
|
||||
savePaginationCache
|
||||
} from '../tools/cache'
|
||||
|
||||
export interface Query {
|
||||
[key: string]: string
|
||||
@ -13,6 +18,11 @@ export interface UsePaginationOptions {
|
||||
api: AxiosInstance
|
||||
url: string
|
||||
inverse?: boolean
|
||||
cacheKey?: CacheKey
|
||||
}
|
||||
|
||||
export interface PaginationItem {
|
||||
id: number
|
||||
}
|
||||
|
||||
export type SetItems<T> = React.Dispatch<React.SetStateAction<T[]>>
|
||||
@ -25,16 +35,25 @@ export interface UsePaginationResult<T> {
|
||||
setItems: SetItems<T>
|
||||
}
|
||||
|
||||
export const usePagination = <T extends { id: number }>(
|
||||
export const usePagination = <T extends PaginationItem>(
|
||||
options: UsePaginationOptions
|
||||
): UsePaginationResult<T> => {
|
||||
const { api, url, inverse = false } = options
|
||||
const { api, url, inverse = false, cacheKey } = options
|
||||
|
||||
const [items, setItems] = useState<T[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const fetchState = useRef<FetchState>('idle')
|
||||
const afterId = useRef<number | null>(null)
|
||||
|
||||
const updateAfterId = (newItems: T[]): void => {
|
||||
if (!inverse) {
|
||||
afterId.current =
|
||||
newItems.length > 0 ? newItems[newItems.length - 1].id : null
|
||||
} else {
|
||||
afterId.current = newItems.length > 0 ? newItems[0].id : null
|
||||
}
|
||||
}
|
||||
|
||||
const nextPageAsync: NextPageAsync = useCallback(
|
||||
async (query) => {
|
||||
if (fetchState.current !== 'idle') {
|
||||
@ -49,19 +68,22 @@ export const usePagination = <T extends { id: number }>(
|
||||
const { data: newItems } = await api.get<T[]>(
|
||||
`${url}?${searchParameters.toString()}`
|
||||
)
|
||||
if (!inverse) {
|
||||
afterId.current =
|
||||
newItems.length > 0 ? newItems[newItems.length - 1].id : null
|
||||
} else {
|
||||
afterId.current = newItems.length > 0 ? newItems[0].id : null
|
||||
}
|
||||
updateAfterId(newItems)
|
||||
setItems((oldItems) => {
|
||||
return inverse ? [...newItems, ...oldItems] : [...oldItems, ...newItems]
|
||||
const updatedItems = inverse
|
||||
? [...newItems, ...oldItems]
|
||||
: [...oldItems, ...newItems]
|
||||
if (cacheKey != null) {
|
||||
savePaginationCache(cacheKey, updatedItems)
|
||||
}
|
||||
return updatedItems
|
||||
})
|
||||
setHasMore(newItems.length > 0)
|
||||
fetchState.current = 'idle'
|
||||
},
|
||||
[api, url, inverse]
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- We don't want infinite loops with updateAfterId
|
||||
[api, url, inverse, cacheKey]
|
||||
)
|
||||
|
||||
const nextPage: NextPage = useCallback(
|
||||
@ -80,9 +102,17 @@ export const usePagination = <T extends { id: number }>(
|
||||
)
|
||||
|
||||
const resetPagination = useCallback((): void => {
|
||||
if (cacheKey == null) {
|
||||
afterId.current = null
|
||||
setItems([])
|
||||
}, [])
|
||||
} else {
|
||||
const newItems = getPaginationCache<T>(cacheKey)
|
||||
setItems(newItems)
|
||||
updateAfterId(newItems)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- We don't want infinite loops with updateAfterId
|
||||
}, [cacheKey])
|
||||
|
||||
return { items, hasMore, nextPage, resetPagination, setItems }
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ const nextTranslate = require('next-translate')
|
||||
/** @type {import("next").NextConfig} */
|
||||
module.exports = nextTranslate(
|
||||
nextPWA({
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
images: {
|
||||
domains: [
|
||||
'api.thream.divlo.fr',
|
||||
|
@ -28,6 +28,9 @@ const Application = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||
}
|
||||
window.addEventListener('resize', appHeight)
|
||||
appHeight()
|
||||
return () => {
|
||||
window.removeEventListener('resize', appHeight)
|
||||
}
|
||||
}, [lang])
|
||||
|
||||
return (
|
||||
|
@ -9,15 +9,23 @@ import { fetchRefreshToken } from './authenticationFromServerSide'
|
||||
export class Authentication {
|
||||
public tokens: Tokens
|
||||
public accessTokenAge: number
|
||||
public socket: Socket
|
||||
public socket?: Socket
|
||||
public api: AxiosInstance
|
||||
|
||||
constructor(tokens: Tokens) {
|
||||
constructor(tokens: Tokens, disableSocketIO: boolean = false) {
|
||||
this.tokens = tokens
|
||||
this.accessTokenAge = Date.now()
|
||||
if (disableSocketIO || typeof window === 'undefined') {
|
||||
this.socket = undefined
|
||||
} else {
|
||||
this.socket = io(API_URL, {
|
||||
auth: { token: `Bearer ${tokens.accessToken}` }
|
||||
})
|
||||
this.socket.on('connect', () => {
|
||||
console.log(
|
||||
`Connected to socket with clientId: ${this.socket?.id ?? 'undefined'}`
|
||||
)
|
||||
})
|
||||
this.socket.on('connect_error', (error) => {
|
||||
if (error.message.startsWith('Unauthorized')) {
|
||||
fetchRefreshToken(this.tokens.refreshToken)
|
||||
@ -30,6 +38,7 @@ export class Authentication {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.api = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
@ -83,13 +92,14 @@ export class Authentication {
|
||||
this.tokens.accessToken = accessToken
|
||||
this.accessTokenAge = Date.now()
|
||||
const token = `${this.tokens.type} ${this.tokens.accessToken}`
|
||||
if (typeof this.socket.auth !== 'function') {
|
||||
if (typeof this?.socket?.auth !== 'function' && this.socket != null) {
|
||||
this.socket.auth.token = token
|
||||
}
|
||||
}
|
||||
|
||||
public signout(): void {
|
||||
cookies.remove('refreshToken')
|
||||
window.localStorage.clear()
|
||||
window.location.href = '/authentication/signin'
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,8 @@ export const AuthenticationProvider: React.FC<
|
||||
const [user, setUser] = useState<UserCurrent>(props.authentication.user)
|
||||
|
||||
const authentication = useMemo(() => {
|
||||
return new Authentication(props.authentication.tokens)
|
||||
const disableSocketIO = typeof window === 'undefined'
|
||||
return new Authentication(props.authentication.tokens, disableSocketIO)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this memo once
|
||||
}, [])
|
||||
@ -33,7 +34,9 @@ export const AuthenticationProvider: React.FC<
|
||||
useEffect(() => {
|
||||
setLanguage(props.authentication.user.settings.language).catch(() => {})
|
||||
setTheme(props.authentication.user.settings.theme)
|
||||
|
||||
return () => {
|
||||
authentication?.socket?.disconnect()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect once
|
||||
}, [])
|
||||
|
||||
|
@ -71,7 +71,7 @@ export const authenticationFromServerSide = (
|
||||
} else {
|
||||
try {
|
||||
let data: any = {}
|
||||
const authentication = new Authentication(tokens)
|
||||
const authentication = new Authentication(tokens, true)
|
||||
const { data: currentUser } = await authentication.api.get<
|
||||
unknown,
|
||||
AxiosResponse<UserCurrent>
|
||||
|
34
tools/cache.ts
Normal file
34
tools/cache.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { PaginationItem } from '../hooks/usePagination'
|
||||
|
||||
export const GUILDS_CACHE_KEY = 'guilds' as const
|
||||
export const CHANNELS_CACHE_KEY = 'channels' as const
|
||||
export const MEMBERS_CACHE_KEY = 'members' as const
|
||||
export const MESSAGES_CACHE_KEY = 'messages' as const
|
||||
|
||||
export type CacheKey =
|
||||
| typeof GUILDS_CACHE_KEY
|
||||
| `${number}-${typeof CHANNELS_CACHE_KEY}`
|
||||
| `${number}-${typeof MEMBERS_CACHE_KEY}`
|
||||
| `${number}-${typeof MESSAGES_CACHE_KEY}`
|
||||
|
||||
export const getPaginationCache = <T extends PaginationItem>(
|
||||
key: CacheKey
|
||||
): T[] => {
|
||||
const cache = localStorage.getItem(key)
|
||||
if (cache != null) {
|
||||
try {
|
||||
const data = JSON.parse(cache)
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export const savePaginationCache = <T extends PaginationItem>(
|
||||
key: CacheKey,
|
||||
data: T[]
|
||||
): void => {
|
||||
localStorage.setItem(key, JSON.stringify(data))
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { SetItems } from '../hooks/usePagination'
|
||||
import { CacheKey, savePaginationCache } from './cache'
|
||||
|
||||
export interface Item {
|
||||
id: number
|
||||
@ -13,6 +14,7 @@ export interface SocketData<T extends Item = Item> {
|
||||
export interface HandleSocketDataOptions<T extends Item = Item> {
|
||||
setItems: SetItems<T>
|
||||
data: SocketData<T>
|
||||
cacheKey?: CacheKey
|
||||
}
|
||||
|
||||
export type SocketListener = (data: SocketData) => void
|
||||
@ -20,7 +22,8 @@ export type SocketListener = (data: SocketData) => void
|
||||
export const handleSocketData = <T extends Item = Item>(
|
||||
options: HandleSocketDataOptions<T>
|
||||
): void => {
|
||||
const { data, setItems } = options
|
||||
const { data, setItems, cacheKey } = options
|
||||
console.log('socket.io data received: ', data)
|
||||
|
||||
setItems((oldItems) => {
|
||||
const newItems = [...oldItems]
|
||||
@ -47,6 +50,9 @@ export const handleSocketData = <T extends Item = Item>(
|
||||
break
|
||||
}
|
||||
}
|
||||
if (cacheKey != null) {
|
||||
savePaginationCache(cacheKey, newItems)
|
||||
}
|
||||
return newItems
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user