diff --git a/components/Application/Channels/Channel.tsx b/components/Application/Channels/Channel.tsx index ab78a13..f9eea35 100644 --- a/components/Application/Channels/Channel.tsx +++ b/components/Application/Channels/Channel.tsx @@ -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 = (props) => { +const ChannelMemo: React.FC = (props) => { const { channel, path, selected = false } = props const router = useRouter() @@ -53,3 +54,5 @@ export const Channel: React.FC = (props) => { ) } + +export const Channel = memo(ChannelMemo) diff --git a/components/Application/Guilds/Guild.tsx b/components/Application/Guilds/Guild.tsx index c7e33b4..f30765f 100644 --- a/components/Application/Guilds/Guild.tsx +++ b/components/Application/Guilds/Guild.tsx @@ -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 = (props) => { +const GuildMemo: React.FC = (props) => { const { guild, selected } = props return ( @@ -34,3 +35,5 @@ export const Guild: React.FC = (props) => { ) } + +export const Guild = memo(GuildMemo) diff --git a/components/Application/Members/Member.tsx b/components/Application/Members/Member.tsx index 93c3147..0ca4bf4 100644 --- a/components/Application/Members/Member.tsx +++ b/components/Application/Members/Member.tsx @@ -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 = (props) => { +const MemberMemo: React.FC = (props) => { const { member } = props return ( @@ -45,3 +46,5 @@ export const Member: React.FC = (props) => { ) } + +export const Member = memo(MemberMemo) diff --git a/contexts/Channels.tsx b/contexts/Channels.tsx index e154cff..e7d946f 100644 --- a/contexts/Channels.tsx +++ b/contexts/Channels.tsx @@ -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(() => { + 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 } diff --git a/contexts/GuildMember.tsx b/contexts/GuildMember.tsx index 40f7b19..d15b1e6 100644 --- a/contexts/GuildMember.tsx +++ b/contexts/GuildMember.tsx @@ -11,9 +11,7 @@ export interface GuildMember { member: Member } -export interface GuildMemberResult extends GuildMember { - setGuildMember: React.Dispatch> -} +export interface GuildMemberResult extends GuildMember {} export interface GuildMemberProps { guildMember: GuildMember @@ -79,8 +77,7 @@ export const GuildMemberProvider: React.FC< return ( {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 } diff --git a/contexts/Guilds.tsx b/contexts/Guilds.tsx index 6c7b792..02eac89 100644 --- a/contexts/Guilds.tsx +++ b/contexts/Guilds.tsx @@ -62,7 +62,7 @@ export const GuildsProvider: React.FC> = ( 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 } diff --git a/contexts/Members.tsx b/contexts/Members.tsx index ac8bc15..ade7e7b 100644 --- a/contexts/Members.tsx +++ b/contexts/Members.tsx @@ -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(() => { + 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 } diff --git a/contexts/Messages.tsx b/contexts/Messages.tsx index dca08bf..f3ee5fc 100644 --- a/contexts/Messages.tsx +++ b/contexts/Messages.tsx @@ -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(() => { + 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 } diff --git a/cypress/e2e/pages/application/[guildId]/[channelId]/index.cy.ts b/cypress/e2e/pages/application/[guildId]/[channelId]/index.cy.ts index 43753ce..f414973 100644 --- a/cypress/e2e/pages/application/[guildId]/[channelId]/index.cy.ts +++ b/cypress/e2e/pages/application/[guildId]/[channelId]/index.cy.ts @@ -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') }) }) diff --git a/cypress/e2e/pages/application/[guildId]/[channelId]/settings.cy.ts b/cypress/e2e/pages/application/[guildId]/[channelId]/settings.cy.ts index d78f79e..20e6abd 100644 --- a/cypress/e2e/pages/application/[guildId]/[channelId]/settings.cy.ts +++ b/cypress/e2e/pages/application/[guildId]/[channelId]/settings.cy.ts @@ -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') }) }) diff --git a/cypress/e2e/pages/application/[guildId]/channels/create.cy.ts b/cypress/e2e/pages/application/[guildId]/channels/create.cy.ts index 58d97a7..b9fe32b 100644 --- a/cypress/e2e/pages/application/[guildId]/channels/create.cy.ts +++ b/cypress/e2e/pages/application/[guildId]/channels/create.cy.ts @@ -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') }) }) diff --git a/cypress/e2e/pages/application/[guildId]/settings.cy.ts b/cypress/e2e/pages/application/[guildId]/settings.cy.ts index 78e4c71..dce023e 100644 --- a/cypress/e2e/pages/application/[guildId]/settings.cy.ts +++ b/cypress/e2e/pages/application/[guildId]/settings.cy.ts @@ -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') }) }) diff --git a/cypress/e2e/pages/application/users/[userId]/index.cy.ts b/cypress/e2e/pages/application/users/[userId]/index.cy.ts index cd2c09a..1700017 100644 --- a/cypress/e2e/pages/application/users/[userId]/index.cy.ts +++ b/cypress/e2e/pages/application/users/[userId]/index.cy.ts @@ -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') }) }) diff --git a/hooks/usePagination.ts b/hooks/usePagination.ts index f82b0e6..027e7e7 100644 --- a/hooks/usePagination.ts +++ b/hooks/usePagination.ts @@ -45,15 +45,6 @@ export const usePagination = ( const fetchState = useRef('idle') const afterId = useRef(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 = ( const { data: newItems } = await api.get( `${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] + const unique = updatedItems.reduce((accumulator, item) => { + const isExisting = accumulator.some( + (itemSome) => itemSome.id === item.id + ) + if (!isExisting) { + accumulator.push(item) + } + return accumulator + }, []) if (cacheKey != null) { - savePaginationCache(cacheKey, updatedItems) + savePaginationCache(cacheKey, unique) } - return updatedItems + 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 = ( afterId.current = null setItems([]) } else { + fetchState.current = 'loading' const newItems = getPaginationCache(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 + } + fetchState.current = 'idle' } - - // eslint-disable-next-line react-hooks/exhaustive-deps -- We don't want infinite loops with updateAfterId - }, [cacheKey]) + }, [cacheKey, inverse]) return { items, hasMore, nextPage, resetPagination, setItems } } diff --git a/pages/application/[guildId]/[channelId]/index.tsx b/pages/application/[guildId]/[channelId]/index.tsx index 8aa7ab6..04baffc 100644 --- a/pages/application/[guildId]/[channelId]/index.tsx +++ b/pages/application/[guildId]/[channelId]/index.tsx @@ -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}`) diff --git a/pages/application/[guildId]/[channelId]/settings.tsx b/pages/application/[guildId]/[channelId]/settings.tsx index 9a6e009..9e7b55f 100644 --- a/pages/application/[guildId]/[channelId]/settings.tsx +++ b/pages/application/[guildId]/[channelId]/settings.tsx @@ -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( @@ -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( diff --git a/pages/application/[guildId]/channels/create.tsx b/pages/application/[guildId]/channels/create.tsx index 90ae802..fcf4154 100644 --- a/pages/application/[guildId]/channels/create.tsx +++ b/pages/application/[guildId]/channels/create.tsx @@ -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}`) diff --git a/pages/application/[guildId]/settings.tsx b/pages/application/[guildId]/settings.tsx index 01422f2..aaf32e5 100644 --- a/pages/application/[guildId]/settings.tsx +++ b/pages/application/[guildId]/settings.tsx @@ -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( diff --git a/pages/application/users/[userId]/index.tsx b/pages/application/users/[userId]/index.tsx index 910ecf0..31b0eed 100644 --- a/pages/application/users/[userId]/index.tsx +++ b/pages/application/users/[userId]/index.tsx @@ -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}`) diff --git a/tools/authentication/Authentication.ts b/tools/authentication/Authentication.ts index 5222eca..ccf0580 100644 --- a/tools/authentication/Authentication.ts +++ b/tools/authentication/Authentication.ts @@ -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, { diff --git a/tools/authentication/AuthenticationContext.tsx b/tools/authentication/AuthenticationContext.tsx index 208f2af..f7f32c8 100644 --- a/tools/authentication/AuthenticationContext.tsx +++ b/tools/authentication/AuthenticationContext.tsx @@ -25,8 +25,7 @@ export const AuthenticationProvider: React.FC< const [user, setUser] = useState(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 }, []) diff --git a/tools/authentication/authenticationFromServerSide.ts b/tools/authentication/authenticationFromServerSide.ts index 29e2e85..808c5be 100644 --- a/tools/authentication/authenticationFromServerSide.ts +++ b/tools/authentication/authenticationFromServerSide.ts @@ -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 @@ -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 } } }