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

View File

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

View File

@ -1,3 +1,4 @@
import { memo } from 'react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
@ -8,7 +9,7 @@ export interface MemberProps {
member: MemberWithPublicUser member: MemberWithPublicUser
} }
export const Member: React.FC<MemberProps> = (props) => { const MemberMemo: React.FC<MemberProps> = (props) => {
const { member } = props const { member } = props
return ( return (
@ -45,3 +46,5 @@ export const Member: React.FC<MemberProps> = (props) => {
</Link> </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 { useRouter } from 'next/router'
import { NextPage, usePagination } from '../hooks/usePagination' import { NextPage, usePagination } from '../hooks/usePagination'
@ -28,7 +28,9 @@ export const ChannelsProvider: React.FC<
const router = useRouter() const router = useRouter()
const { authentication } = useAuthentication() const { authentication } = useAuthentication()
const cacheKey: CacheKey = `${path.guildId}-${CHANNELS_CACHE_KEY}` const cacheKey = useMemo<CacheKey>(() => {
return `${path.guildId}-${CHANNELS_CACHE_KEY}`
}, [path.guildId])
const { const {
items: channels, items: channels,
@ -75,7 +77,7 @@ export const ChannelsProvider: React.FC<
export const useChannels = (): Channels => { export const useChannels = (): Channels => {
const channels = useContext(ChannelsContext) const channels = useContext(ChannelsContext)
if (channels === defaultChannelsContext) { if (channels === defaultChannelsContext) {
throw new Error('useChannels must be used within ChannelsProvider') throw new Error('`useChannels` must be used within `ChannelsProvider`')
} }
return channels return channels
} }

View File

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

View File

@ -62,7 +62,7 @@ export const GuildsProvider: React.FC<React.PropsWithChildren<{}>> = (
export const useGuilds = (): Guilds => { export const useGuilds = (): Guilds => {
const guilds = useContext(GuildsContext) const guilds = useContext(GuildsContext)
if (guilds === defaultGuildsContext) { if (guilds === defaultGuildsContext) {
throw new Error('useGuilds must be used within GuildsProvider') throw new Error('`useGuilds` must be used within `GuildsProvider`')
} }
return guilds 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 { NextPage, usePagination } from '../hooks/usePagination'
import { useAuthentication } from '../tools/authentication' import { useAuthentication } from '../tools/authentication'
@ -28,7 +28,9 @@ export const MembersProviders: React.FC<
const { authentication } = useAuthentication() const { authentication } = useAuthentication()
const cacheKey: CacheKey = `${path.guildId}-${MEMBERS_CACHE_KEY}` const cacheKey = useMemo<CacheKey>(() => {
return `${path.guildId}-${MEMBERS_CACHE_KEY}`
}, [path.guildId])
const { const {
items: members, items: members,
@ -89,7 +91,7 @@ export const MembersProviders: React.FC<
export const useMembers = (): Members => { export const useMembers = (): Members => {
const members = useContext(MembersContext) const members = useContext(MembersContext)
if (members === defaultMembersContext) { if (members === defaultMembersContext) {
throw new Error('useMembers must be used within MembersProvider') throw new Error('`useMembers` must be used within `MembersProvider`')
} }
return members 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 { NextPage, usePagination } from '../hooks/usePagination'
import { useAuthentication } from '../tools/authentication' import { useAuthentication } from '../tools/authentication'
@ -26,7 +26,9 @@ export const MessagesProvider: React.FC<
const { path, children } = props const { path, children } = props
const { authentication, user } = useAuthentication() 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 { const {
items: messages, items: messages,
@ -88,7 +90,7 @@ export const MessagesProvider: React.FC<
export const useMessages = (): Messages => { export const useMessages = (): Messages => {
const messages = useContext(MessagesContext) const messages = useContext(MessagesContext)
if (messages === defaultMessagesContext) { 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 return messages
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ export class Authentication {
constructor(tokens: Tokens, disableSocketIO: boolean = false) { constructor(tokens: Tokens, disableSocketIO: boolean = false) {
this.tokens = tokens this.tokens = tokens
this.accessTokenAge = Date.now() this.accessTokenAge = Date.now()
if (disableSocketIO || typeof window === 'undefined') { if (typeof window === 'undefined' || disableSocketIO) {
this.socket = undefined this.socket = undefined
} else { } else {
this.socket = io(API_URL, { 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 [user, setUser] = useState<UserCurrent>(props.authentication.user)
const authentication = useMemo(() => { const authentication = useMemo(() => {
const disableSocketIO = typeof window === 'undefined' return new Authentication(props.authentication.tokens)
return new Authentication(props.authentication.tokens, disableSocketIO)
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this memo once // 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 { } else {
let data = {} let data: any = {}
if (fetchData != null) { if (fetchData != null) {
data = await fetchData(context, api) data = await fetchData(context, api)
} }
if (data.notFound != null) {
return data
}
return { props: data } return { props: data }
} }
} else { } else {
@ -71,7 +74,7 @@ export const authenticationFromServerSide = (
} else { } else {
try { try {
let data: any = {} let data: any = {}
const authentication = new Authentication(tokens, true) const authentication = new Authentication(tokens)
const { data: currentUser } = await authentication.api.get< const { data: currentUser } = await authentication.api.get<
unknown, unknown,
AxiosResponse<UserCurrent> AxiosResponse<UserCurrent>
@ -79,7 +82,7 @@ export const authenticationFromServerSide = (
if (fetchData != null) { if (fetchData != null) {
data = await fetchData(context, authentication.api) data = await fetchData(context, authentication.api)
} }
if (data.redirect != null) { if (data.notFound != null) {
return data return data
} }
return { return {
@ -87,10 +90,7 @@ export const authenticationFromServerSide = (
} }
} catch { } catch {
return { return {
redirect: { notFound: true
destination: '/404',
permanent: false
}
} }
} }
} }