diff --git a/src/.eslintrc.js b/src/.eslintrc.js index a97039a..9c715e8 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -6,12 +6,13 @@ module.exports = { plugins: [ '@typescript-eslint', 'unused-imports', + 'react', + 'react-hooks', ], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', - 'plugin:react-hooks/recommended', ], settings: { react: { @@ -159,6 +160,9 @@ module.exports = { 'react/jsx-props-no-multi-spaces': 'warn', 'react/jsx-props-no-spreading': 'error', 'react/jsx-tag-spacing': [ 'warn', { closingSlash: 'never', beforeSelfClosing: 'always', afterOpening: 'never', beforeClosing: 'never' } ], + + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', }, }; diff --git a/src/client/webapp/elements/components/button-download.tsx b/src/client/webapp/elements/components/button-download.tsx index 0e09111..455d6b8 100644 --- a/src/client/webapp/elements/components/button-download.tsx +++ b/src/client/webapp/elements/components/button-download.tsx @@ -102,7 +102,7 @@ const DownloadButton: FC = (props: DownloadButtonProps) => setButtonText('Reveal in Explorer'); setDownloading(false); - }, [ downloading, buttonShaking, downloadPath, downloadBuff ]); + }, [ guild, resourceId, resourceName, downloading, buttonShaking, downloadPath, downloadBuff ]); return ; }; diff --git a/src/client/webapp/elements/components/control-choices.tsx b/src/client/webapp/elements/components/control-choices.tsx index 9222ddb..5c23d0e 100644 --- a/src/client/webapp/elements/components/control-choices.tsx +++ b/src/client/webapp/elements/components/control-choices.tsx @@ -18,7 +18,7 @@ const ChoicesControl: FC = (props: ChoicesControlProps) => > {choice.display} - )), [ choices, selectedId ]); + )), [ choices, selectedId, setSelectedId ]); return (
diff --git a/src/client/webapp/elements/components/display.tsx b/src/client/webapp/elements/components/display.tsx index a0d2a3a..62b295a 100644 --- a/src/client/webapp/elements/components/display.tsx +++ b/src/client/webapp/elements/components/display.tsx @@ -45,11 +45,11 @@ const Display: FC = (props: DisplayProps) => { } }, [ saving, saveFailed ]); - const dismissInfoMessage = () => { - setDismissedInfoMessage(infoMessage); - }; - const popup = useMemo(() => { + const dismissInfoMessage = () => { + setDismissedInfoMessage(infoMessage); + }; + if (errorMessage) { return ( @@ -72,7 +72,7 @@ const Display: FC = (props: DisplayProps) => { } else { return null; } - }, [ errorMessage, changes, resetChanges, saveChanges ]); + }, [ errorMessage, infoMessage, dismissedInfoMessage, changes, resetChanges, saveChanges, saveButtonShaking, changesButtonText ]); return (
diff --git a/src/client/webapp/elements/components/input-dropdown.tsx b/src/client/webapp/elements/components/input-dropdown.tsx index b54cdc3..7e7f7e6 100644 --- a/src/client/webapp/elements/components/input-dropdown.tsx +++ b/src/client/webapp/elements/components/input-dropdown.tsx @@ -30,7 +30,7 @@ const DropdownInput: FC = (props: DropdownInputProps) => { useEffect(() => { // only do this expensive version once setDisplay(options.find(option => option.value === value)?.display ?? ''); - }, []); + }, [ options, value ]); const optionElements = useMemo(() => options.map(option => { const className = option.value === value ? 'option selected' : 'option'; @@ -40,7 +40,7 @@ const DropdownInput: FC = (props: DropdownInputProps) => { setOptionsOpen(false); }; return
{option.display}
; - }), [ options ]); + }), [ options, setValue, value ]); const labelElement = useMemo(() => label &&
{label}
, [ label ]); diff --git a/src/client/webapp/elements/components/input-text.tsx b/src/client/webapp/elements/components/input-text.tsx index 4b9fc82..4962bb7 100644 --- a/src/client/webapp/elements/components/input-text.tsx +++ b/src/client/webapp/elements/components/input-text.tsx @@ -40,7 +40,7 @@ const TextInput: FC = React.forwardRef(function TextInput(props: } setValid(true); setMessage(null); - }, [ value ]); + }, [ allowEmpty, label, maxLength, setMessage, setValid, value ]); const labelElement = useMemo(() => label &&
{label}
, [ label ]); diff --git a/src/client/webapp/elements/components/overlay.tsx b/src/client/webapp/elements/components/overlay.tsx index 59595bd..0db8312 100644 --- a/src/client/webapp/elements/components/overlay.tsx +++ b/src/client/webapp/elements/components/overlay.tsx @@ -18,6 +18,8 @@ const Overlay: FC = (props: OverlayProps) => { const setOverlay = useSetRecoilState(overlayState); if (childRootRef) { + // this is alright to do since childRootRef (the ref itself) should never change for each component using this element + // eslint-disable-next-line react-hooks/rules-of-hooks useActionWhenEscapeOrClickedOrContextOutsideEffect(childRootRef, () => setOverlay(null)); } diff --git a/src/client/webapp/elements/contexts/context-menu-connection-info.tsx b/src/client/webapp/elements/contexts/context-menu-connection-info.tsx index e158d89..837e381 100644 --- a/src/client/webapp/elements/contexts/context-menu-connection-info.tsx +++ b/src/client/webapp/elements/contexts/context-menu-connection-info.tsx @@ -30,12 +30,12 @@ const ConnectionInfoContextMenu: FC = (props: Co
{status}
- )), [ setSelfStatus ]); + )), [ setSelfStatus, close ]); const openPersonalize = useCallback(() => { close(); setOverlay(); - }, [ close ]); + }, [ close, guild, selfMember, setOverlay ]); const alignment = useMemo(() => ({ bottom: 'top', centerX: 'centerX' }), []); @@ -54,3 +54,4 @@ const ConnectionInfoContextMenu: FC = (props: Co }; export default ConnectionInfoContextMenu; + diff --git a/src/client/webapp/elements/contexts/context-menu-guild-title.tsx b/src/client/webapp/elements/contexts/context-menu-guild-title.tsx index 48406c0..fa8bc39 100644 --- a/src/client/webapp/elements/contexts/context-menu-guild-title.tsx +++ b/src/client/webapp/elements/contexts/context-menu-guild-title.tsx @@ -32,12 +32,12 @@ const GuildTitleContextMenu: FC = (props: GuildTitle return; } setOverlay(); - }, [ close ]); + }, [ close, guild, setOverlay ]); const openCreateChannel = useCallback(() => { close(); setOverlay(); - }, [ close ]); + }, [ close, setOverlay ]); const guildSettingsElement = useMemo(() => { if (!isLoaded(selfMember)) return null; @@ -61,10 +61,11 @@ const GuildTitleContextMenu: FC = (props: GuildTitle ); }, [ selfMember, openCreateChannel ]); - if (guildSettingsElement === null && createChannelElement === null) return null; const alignment = useMemo(() => ({ top: 'bottom', centerX: 'centerX' }), []); + if (guildSettingsElement === null && createChannelElement === null) return null; + return (
diff --git a/src/client/webapp/elements/displays/display-guild-overview.tsx b/src/client/webapp/elements/displays/display-guild-overview.tsx index 424b4ba..9fdd5ce 100644 --- a/src/client/webapp/elements/displays/display-guild-overview.tsx +++ b/src/client/webapp/elements/displays/display-guild-overview.tsx @@ -43,14 +43,14 @@ const GuildOverviewDisplay: FC = (props: GuildOvervie if (!isLoaded(guildMeta)) return; if (name === savedName) setName(guildMeta.value.name); setSavedName(guildMeta.value.name); - }, [ guildMeta ]); + }, [ guildMeta, name, savedName ]); useEffect(() => { if (isLoaded(iconResource)) { if (iconBuff === savedIconBuff) setIconBuff(iconResource.value.data); setSavedIconBuff(iconResource.value.data); } - }, [ iconResource ]); + }, [ iconBuff, iconResource, savedIconBuff ]); const changes = useMemo( () => name !== savedName || iconBuff?.toString('hex') !== savedIconBuff?.toString('hex'), @@ -110,7 +110,7 @@ const GuildOverviewDisplay: FC = (props: GuildOvervie } setSaving(false); - }, [ name, iconBuff, errorMessage, saving, guild, iconBuff, savedIconBuff ]); + }, [ name, iconBuff, errorMessage, saving, savedName, savedIconBuff, guild ]); return ( = (props: ChannelElementProps) => const launchModify = useCallback(() => { setOverlay(); - }, [ guild, channel ]); + }, [ setOverlay, channel ]); return (
diff --git a/src/client/webapp/elements/lists/message-list.tsx b/src/client/webapp/elements/lists/message-list.tsx index 2422a71..d55dcf8 100644 --- a/src/client/webapp/elements/lists/message-list.tsx +++ b/src/client/webapp/elements/lists/message-list.tsx @@ -47,7 +47,7 @@ const MessageList: FC = () => { result.push(); } return result; - }, [ messages ]); + }, [ guild, messages ]); return (
diff --git a/src/client/webapp/elements/overlays/overlay-add-guild.tsx b/src/client/webapp/elements/overlays/overlay-add-guild.tsx index a8867fd..1267c27 100644 --- a/src/client/webapp/elements/overlays/overlay-add-guild.tsx +++ b/src/client/webapp/elements/overlays/overlay-add-guild.tsx @@ -86,7 +86,7 @@ const AddGuildOverlay: FC = (props: AddGuildOverlayProps) if (exampleAvatarBuff) { if (avatarBuff === null) setAvatarBuff(exampleAvatarBuff); } - }, [ exampleAvatarBuff ]); + }, [ avatarBuff, exampleAvatarBuff ]); const validationErrorMessage = useMemo(() => { if (exampleAvatarBuffError && !avatarBuff) return 'Unable to load example avatar'; diff --git a/src/client/webapp/elements/overlays/overlay-channel.tsx b/src/client/webapp/elements/overlays/overlay-channel.tsx index 1240320..8ad767a 100644 --- a/src/client/webapp/elements/overlays/overlay-channel.tsx +++ b/src/client/webapp/elements/overlays/overlay-channel.tsx @@ -49,14 +49,14 @@ const ChannelOverlay: FC = (props: ChannelOverlayProps) => } else { setEdited(name.length > 0 && flavorText.length > 0); } - }, [ name, flavorText ]); + }, [ name, flavorText, channel ]); const validationErrorMessage = useMemo(() => { if (!edited) return null; if (!nameInputValid && nameInputMessage) return nameInputMessage; if (!flavorTextInputValid && flavorTextInputMessage) return flavorTextInputMessage; return null; - }, [ nameInputValid, nameInputMessage, flavorTextInputValid, flavorTextInputMessage ]); + }, [ edited, nameInputValid, nameInputMessage, flavorTextInputValid, flavorTextInputMessage ]); const infoMessage = useMemo(() => { if (nameInputValid && nameInputMessage) return nameInputMessage; diff --git a/src/client/webapp/elements/overlays/overlay-personalize.tsx b/src/client/webapp/elements/overlays/overlay-personalize.tsx index be3c21a..9661f6f 100644 --- a/src/client/webapp/elements/overlays/overlay-personalize.tsx +++ b/src/client/webapp/elements/overlays/overlay-personalize.tsx @@ -48,11 +48,11 @@ const PersonalizeOverlay: FC = (props: PersonalizeOverl if (avatarBuff === savedAvatarBuff) setAvatarBuff(avatarResource.value.data); setSavedAvatarBuff(avatarResource.value.data); } - }, [ avatarResource ]); + }, [ avatarBuff, avatarResource, savedAvatarBuff ]); useEffect(() => { displayNameInputRef.current?.focus(); - }, []); + }, [ displayNameInputRef ]); const validationErrorMessage = useMemo(() => { if (isFailed(avatarResource)) return 'Unable to load avatar'; diff --git a/src/client/webapp/elements/sections/send-message.tsx b/src/client/webapp/elements/sections/send-message.tsx index 80637cd..364939a 100644 --- a/src/client/webapp/elements/sections/send-message.tsx +++ b/src/client/webapp/elements/sections/send-message.tsx @@ -109,7 +109,7 @@ const SendMessage: FC = (props: SendMessageProps) => { return; } } - }, []); + }, [ isMounted ]); const removeAttachment = useCallback(() => { setAttachmentBuff(null); @@ -121,7 +121,7 @@ const SendMessage: FC = (props: SendMessageProps) => { const attachmentPreview = useMemo(() => { if (!attachmentBuff || !attachmentName) return null; return ; - }, [ attachmentBuff, attachmentName ]); + }, [ attachmentBuff, attachmentName, removeAttachment ]); // WARNING: The types on this are funky because of react's lack of explicit support for 'plaintext-only' const contentEditableType = useMemo(() => {