fix: cache with duplicated data

This commit is contained in:
Divlo 2022-08-30 21:30:06 +02:00
parent 3d185bf044
commit a068d31d14
No known key found for this signature in database
GPG Key ID: 8F9478F220CE65E9
22 changed files with 98 additions and 95 deletions

View File

@ -1,3 +1,4 @@
import { memo } from 'react'
import classNames from 'clsx'
import Link from 'next/link'
import { useRouter } from 'next/router'
@ -14,7 +15,7 @@ export interface ChannelProps {
selected?: boolean
}
export const Channel: React.FC<ChannelProps> = (props) => {
const ChannelMemo: React.FC<ChannelProps> = (props) => {
const { channel, path, selected = false } = props
const router = useRouter()
@ -53,3 +54,5 @@ export const Channel: React.FC<ChannelProps> = (props) => {
</Link>
)
}
export const Channel = memo(ChannelMemo)

View File

@ -1,3 +1,4 @@
import { memo } from 'react'
import Image from 'next/image'
import { GuildWithDefaultChannelId } from '../../../models/Guild'
@ -8,7 +9,7 @@ export interface GuildProps {
selected?: boolean
}
export const Guild: React.FC<GuildProps> = (props) => {
const GuildMemo: React.FC<GuildProps> = (props) => {
const { guild, selected } = props
return (
@ -34,3 +35,5 @@ export const Guild: React.FC<GuildProps> = (props) => {
</IconLink>
)
}
export const Guild = memo(GuildMemo)

View File

@ -1,3 +1,4 @@
import { memo } from 'react'
import Image from 'next/image'
import Link from 'next/link'
@ -8,7 +9,7 @@ export interface MemberProps {
member: MemberWithPublicUser
}
export const Member: React.FC<MemberProps> = (props) => {
const MemberMemo: React.FC<MemberProps> = (props) => {
const { member } = props
return (
@ -45,3 +46,5 @@ export const Member: React.FC<MemberProps> = (props) => {
</Link>
)
}
export const Member = memo(MemberMemo)

View File

@ -1,4 +1,4 @@
import { createContext, useContext, useEffect } from 'react'
import { createContext, useContext, useEffect, useMemo } from 'react'
import { useRouter } from 'next/router'
import { NextPage, usePagination } from '../hooks/usePagination'
@ -28,7 +28,9 @@ export const ChannelsProvider: React.FC<
const router = useRouter()
const { authentication } = useAuthentication()
const cacheKey: CacheKey = `${path.guildId}-${CHANNELS_CACHE_KEY}`
const cacheKey = useMemo<CacheKey>(() => {
return `${path.guildId}-${CHANNELS_CACHE_KEY}`
}, [path.guildId])
const {
items: channels,
@ -75,7 +77,7 @@ export const ChannelsProvider: React.FC<
export const useChannels = (): Channels => {
const channels = useContext(ChannelsContext)
if (channels === defaultChannelsContext) {
throw new Error('useChannels must be used within ChannelsProvider')
throw new Error('`useChannels` must be used within `ChannelsProvider`')
}
return channels
}

View File

@ -11,9 +11,7 @@ export interface GuildMember {
member: Member
}
export interface GuildMemberResult extends GuildMember {
setGuildMember: React.Dispatch<React.SetStateAction<GuildMember>>
}
export interface GuildMemberResult extends GuildMember {}
export interface GuildMemberProps {
guildMember: GuildMember
@ -79,8 +77,7 @@ export const GuildMemberProvider: React.FC<
return (
<GuildMemberContext.Provider
value={{
...guildMember,
setGuildMember
...guildMember
}}
>
{children}
@ -91,7 +88,9 @@ export const GuildMemberProvider: React.FC<
export const useGuildMember = (): GuildMemberResult => {
const guildMember = useContext(GuildMemberContext)
if (guildMember === defaultGuildMemberContext) {
throw new Error('useGuildMember must be used within GuildMemberProvider')
throw new Error(
'`useGuildMember` must be used within `GuildMemberProvider`'
)
}
return guildMember
}

View File

@ -62,7 +62,7 @@ export const GuildsProvider: React.FC<React.PropsWithChildren<{}>> = (
export const useGuilds = (): Guilds => {
const guilds = useContext(GuildsContext)
if (guilds === defaultGuildsContext) {
throw new Error('useGuilds must be used within GuildsProvider')
throw new Error('`useGuilds` must be used within `GuildsProvider`')
}
return guilds
}

View File

@ -1,4 +1,4 @@
import { createContext, useContext, useEffect } from 'react'
import { createContext, useContext, useEffect, useMemo } from 'react'
import { NextPage, usePagination } from '../hooks/usePagination'
import { useAuthentication } from '../tools/authentication'
@ -28,7 +28,9 @@ export const MembersProviders: React.FC<
const { authentication } = useAuthentication()
const cacheKey: CacheKey = `${path.guildId}-${MEMBERS_CACHE_KEY}`
const cacheKey = useMemo<CacheKey>(() => {
return `${path.guildId}-${MEMBERS_CACHE_KEY}`
}, [path.guildId])
const {
items: members,
@ -89,7 +91,7 @@ export const MembersProviders: React.FC<
export const useMembers = (): Members => {
const members = useContext(MembersContext)
if (members === defaultMembersContext) {
throw new Error('useMembers must be used within MembersProvider')
throw new Error('`useMembers` must be used within `MembersProvider`')
}
return members
}

View File

@ -1,4 +1,4 @@
import { createContext, useContext, useEffect } from 'react'
import { createContext, useContext, useEffect, useMemo } from 'react'
import { NextPage, usePagination } from '../hooks/usePagination'
import { useAuthentication } from '../tools/authentication'
@ -26,7 +26,9 @@ export const MessagesProvider: React.FC<
const { path, children } = props
const { authentication, user } = useAuthentication()
const cacheKey: CacheKey = `${path.channelId}-${MESSAGES_CACHE_KEY}`
const cacheKey = useMemo<CacheKey>(() => {
return `${path.channelId}-${MESSAGES_CACHE_KEY}`
}, [path.channelId])
const {
items: messages,
@ -88,7 +90,7 @@ export const MessagesProvider: React.FC<
export const useMessages = (): Messages => {
const messages = useContext(MessagesContext)
if (messages === defaultMessagesContext) {
throw new Error('useMessages must be used within a MessagesProvider')
throw new Error('`useMessages` must be used within a `MessagesProvider`')
}
return messages
}

View File

@ -241,8 +241,8 @@ describe('Pages > /application/[guildId]/[channelId]', () => {
cy.visit('/application/abc/abc', {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
it("should redirect the user to `/404` if `guildId` doesn't exist", () => {
@ -253,8 +253,8 @@ describe('Pages > /application/[guildId]/[channelId]', () => {
cy.visit(`/application/123/${channelExample.id}`, {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
it("should redirect the user to `/404` if `channelId` doesn't exist", () => {
@ -263,8 +263,8 @@ describe('Pages > /application/[guildId]/[channelId]', () => {
getGuildMemberWithGuildIdHandler
]).setCookie('refreshToken', 'refresh-token')
cy.visit(`/application/${guildExample.id}/123`, { failOnStatusCode: false })
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
})

View File

@ -92,8 +92,8 @@ describe('Pages > /application/[guildId]/[channelId]/settings', () => {
cy.visit(`/application/${guildExample.id}/${channelExample.id}/settings`, {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
it('should redirect the user to `/404` if `guildId` or `channelId` are not numbers', () => {
@ -104,8 +104,8 @@ describe('Pages > /application/[guildId]/[channelId]/settings', () => {
cy.visit('/application/abc/abc/settings', {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
it("should redirect the user to `/404` if `guildId` doesn't exist", () => {
@ -116,8 +116,8 @@ describe('Pages > /application/[guildId]/[channelId]/settings', () => {
cy.visit(`/application/123/${channelExample.id}/settings`, {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
it("should redirect the user to `/404` if `channelId` doesn't exist", () => {
@ -128,8 +128,8 @@ describe('Pages > /application/[guildId]/[channelId]/settings', () => {
cy.visit(`/application/${guildExample.id}/123/settings`, {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
})

View File

@ -56,8 +56,8 @@ describe('Pages > /application/[guildId]/channels/create', () => {
cy.visit('/application/abc/channels/create', {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
it("should redirect the user to `/404` if `guildId` doesn't exist", () => {
@ -68,8 +68,8 @@ describe('Pages > /application/[guildId]/channels/create', () => {
cy.visit(`/application/123/channels/create`, {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
})

View File

@ -86,8 +86,8 @@ describe('Pages > /application/[guildId]/settings', () => {
cy.visit('/application/abc/settings', {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
it("should redirect the user to `/404` if `guildId` doesn't exist", () => {
@ -98,8 +98,8 @@ describe('Pages > /application/[guildId]/settings', () => {
cy.visit(`/application/123/settings`, {
failOnStatusCode: false
})
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
})

View File

@ -29,8 +29,8 @@ describe('Pages > /application/users/[userId]', () => {
'refresh-token'
)
cy.visit(`/application/users/123`, { failOnStatusCode: false })
.location('pathname')
.should('eq', '/404')
.get('[data-cy=status-code]')
.contains('404')
})
})

View File

@ -45,15 +45,6 @@ export const usePagination = <T extends PaginationItem>(
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') {
@ -68,21 +59,33 @@ export const usePagination = <T extends PaginationItem>(
const { data: newItems } = await api.get<T[]>(
`${url}?${searchParameters.toString()}`
)
updateAfterId(newItems)
if (!inverse) {
afterId.current =
newItems.length > 0 ? newItems[newItems.length - 1].id : null
} else {
afterId.current = newItems.length > 0 ? newItems[0].id : null
}
setItems((oldItems) => {
const updatedItems = inverse
? [...newItems, ...oldItems]
: [...oldItems, ...newItems]
if (cacheKey != null) {
savePaginationCache(cacheKey, updatedItems)
const unique = updatedItems.reduce<T[]>((accumulator, item) => {
const isExisting = accumulator.some(
(itemSome) => itemSome.id === item.id
)
if (!isExisting) {
accumulator.push(item)
}
return updatedItems
return accumulator
}, [])
if (cacheKey != null) {
savePaginationCache(cacheKey, unique)
}
return unique
})
setHasMore(newItems.length > 0)
fetchState.current = 'idle'
},
// eslint-disable-next-line react-hooks/exhaustive-deps -- We don't want infinite loops with updateAfterId
[api, url, inverse, cacheKey]
)
@ -106,13 +109,18 @@ export const usePagination = <T extends PaginationItem>(
afterId.current = null
setItems([])
} else {
fetchState.current = 'loading'
const newItems = getPaginationCache<T>(cacheKey)
setItems(newItems)
updateAfterId(newItems)
if (!inverse) {
afterId.current =
newItems.length > 0 ? newItems[newItems.length - 1].id : null
} else {
afterId.current = newItems.length > 0 ? newItems[0].id : null
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- We don't want infinite loops with updateAfterId
}, [cacheKey])
fetchState.current = 'idle'
}
}, [cacheKey, inverse])
return { items, hasMore, nextPage, resetPagination, setItems }
}

View File

@ -68,10 +68,7 @@ export const getServerSideProps = authenticationFromServerSide({
const guildId = Number(context?.params?.guildId)
if (isNaN(channelId) || isNaN(guildId)) {
return {
redirect: {
destination: '/404',
permanent: false
}
notFound: true
}
}
const { data: guildMember } = await api.get(`/guilds/${guildId}`)

View File

@ -63,10 +63,7 @@ export const getServerSideProps = authenticationFromServerSide({
const guildId = Number(context?.params?.guildId)
if (isNaN(channelId) || isNaN(guildId)) {
return {
redirect: {
destination: '/404',
permanent: false
}
notFound: true
}
}
const { data: guildMember } = await api.get<GuildMember>(
@ -74,10 +71,7 @@ export const getServerSideProps = authenticationFromServerSide({
)
if (!guildMember.member.isOwner) {
return {
redirect: {
destination: '/404',
permanent: false
}
notFound: true
}
}
const { data: selectedChannelData } = await api.get(

View File

@ -49,10 +49,7 @@ export const getServerSideProps = authenticationFromServerSide({
const guildId = Number(context?.params?.guildId)
if (isNaN(guildId)) {
return {
redirect: {
destination: '/404',
permanent: false
}
notFound: true
}
}
const { data: guildMember } = await api.get(`/guilds/${guildId}`)

View File

@ -43,10 +43,7 @@ export const getServerSideProps = authenticationFromServerSide({
const guildId = Number(context?.params?.guildId)
if (isNaN(guildId)) {
return {
redirect: {
destination: '/404',
permanent: false
}
notFound: true
}
}
const { data: guildMember } = await api.get<GuildMember>(

View File

@ -38,10 +38,7 @@ export const getServerSideProps = authenticationFromServerSide({
const userId = Number(context?.params?.userId)
if (isNaN(userId)) {
return {
redirect: {
destination: '/404',
permanent: false
}
notFound: true
}
}
const { data } = await api.get(`/users/${userId}`)

View File

@ -16,7 +16,7 @@ export class Authentication {
constructor(tokens: Tokens, disableSocketIO: boolean = false) {
this.tokens = tokens
this.accessTokenAge = Date.now()
if (disableSocketIO || typeof window === 'undefined') {
if (typeof window === 'undefined' || disableSocketIO) {
this.socket = undefined
} else {
this.socket = io(API_URL, {

View File

@ -25,8 +25,7 @@ export const AuthenticationProvider: React.FC<
const [user, setUser] = useState<UserCurrent>(props.authentication.user)
const authentication = useMemo(() => {
const disableSocketIO = typeof window === 'undefined'
return new Authentication(props.authentication.tokens, disableSocketIO)
return new Authentication(props.authentication.tokens)
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this memo once
}, [])

View File

@ -54,10 +54,13 @@ export const authenticationFromServerSide = (
}
}
} else {
let data = {}
let data: any = {}
if (fetchData != null) {
data = await fetchData(context, api)
}
if (data.notFound != null) {
return data
}
return { props: data }
}
} else {
@ -71,7 +74,7 @@ export const authenticationFromServerSide = (
} else {
try {
let data: any = {}
const authentication = new Authentication(tokens, true)
const authentication = new Authentication(tokens)
const { data: currentUser } = await authentication.api.get<
unknown,
AxiosResponse<UserCurrent>
@ -79,7 +82,7 @@ export const authenticationFromServerSide = (
if (fetchData != null) {
data = await fetchData(context, authentication.api)
}
if (data.redirect != null) {
if (data.notFound != null) {
return data
}
return {
@ -87,10 +90,7 @@ export const authenticationFromServerSide = (
}
} catch {
return {
redirect: {
destination: '/404',
permanent: false
}
notFound: true
}
}
}