chore: Enable ESLint rules of hooks + fix new lint errors (#5714)

This commit is contained in:
Sébastien Lorber 2021-10-20 16:09:52 +02:00 committed by GitHub
parent 3db4fcaec7
commit 098f210890
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 110 additions and 48 deletions

View file

@ -28,6 +28,7 @@ module.exports = {
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'airbnb', 'airbnb',
'prettier', 'prettier',
'prettier/react', 'prettier/react',
@ -41,6 +42,8 @@ module.exports = {
}, },
plugins: ['react-hooks', 'header'], plugins: ['react-hooks', 'header'],
rules: { rules: {
'react-hooks/rules-of-hooks': ERROR,
'react-hooks/exhaustive-deps': ERROR,
'class-methods-use-this': OFF, // It's a way of allowing private variables. 'class-methods-use-this': OFF, // It's a way of allowing private variables.
'func-names': OFF, 'func-names': OFF,
// Ignore certain webpack alias because it can't be resolved // Ignore certain webpack alias because it can't be resolved
@ -77,7 +80,6 @@ module.exports = {
'react/destructuring-assignment': OFF, // Too many lines. 'react/destructuring-assignment': OFF, // Too many lines.
'react/prefer-stateless-function': WARNING, 'react/prefer-stateless-function': WARNING,
'react/jsx-props-no-spreading': OFF, 'react/jsx-props-no-spreading': OFF,
'react-hooks/rules-of-hooks': ERROR,
'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}], 'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}],
'@typescript-eslint/no-inferrable-types': OFF, '@typescript-eslint/no-inferrable-types': OFF,
'import/first': OFF, 'import/first': OFF,

View file

@ -51,7 +51,7 @@ function DocPageContent({
setHiddenSidebar(false); setHiddenSidebar(false);
} }
setHiddenSidebarContainer(!hiddenSidebarContainer); setHiddenSidebarContainer((value) => !value);
}, [hiddenSidebar]); }, [hiddenSidebar]);
return ( return (

View file

@ -93,7 +93,7 @@ function useAutoExpandActiveCategory({
if (justBecameActive && collapsed) { if (justBecameActive && collapsed) {
setCollapsed(false); setCollapsed(false);
} }
}, [isActive, wasActive, collapsed]); }, [isActive, wasActive, collapsed, setCollapsed]);
} }
function DocSidebarItemCategory({ function DocSidebarItemCategory({

View file

@ -141,7 +141,7 @@ function DropdownNavbarItemMobile({
if (containsActive) { if (containsActive) {
setCollapsed(!containsActive); setCollapsed(!containsActive);
} }
}, [localPathname, containsActive]); }, [localPathname, containsActive, setCollapsed]);
return ( return (
<li <li

View file

@ -68,7 +68,7 @@ const useTheme = (): useThemeReturns => {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
}, [setTheme]); }, [disableSwitch, setTheme]);
useEffect(() => { useEffect(() => {
if (disableSwitch && !respectPrefersColorScheme) { if (disableSwitch && !respectPrefersColorScheme) {
@ -80,7 +80,7 @@ const useTheme = (): useThemeReturns => {
.addListener(({matches}) => { .addListener(({matches}) => {
setTheme(matches ? themes.dark : themes.light); setTheme(matches ? themes.dark : themes.light);
}); });
}, []); }, [disableSwitch, respectPrefersColorScheme]);
return { return {
isDarkTheme: theme === themes.dark, isDarkTheme: theme === themes.dark,

View file

@ -88,3 +88,8 @@ export {
useScrollPosition, useScrollPosition,
useScrollPositionBlocker, useScrollPositionBlocker,
} from './utils/scrollUtils'; } from './utils/scrollUtils';
export {
useIsomorphicLayoutEffect,
useDynamicCallback,
} from './utils/reactUtils';

View file

@ -83,14 +83,14 @@ const useAnnouncementBarContextValue = (): AnnouncementBarAPI => {
if (isNewAnnouncement || !isDismissedInStorage()) { if (isNewAnnouncement || !isDismissedInStorage()) {
setClosed(false); setClosed(false);
} }
}, []); }, [announcementBar]);
return useMemo(() => { return useMemo(() => {
return { return {
isActive: !!announcementBar && !isClosed, isActive: !!announcementBar && !isClosed,
close: handleClose, close: handleClose,
}; };
}, [isClosed]); }, [announcementBar, isClosed, handleClose]);
}; };
const AnnouncementBarContext = createContext<AnnouncementBarAPI | null>(null); const AnnouncementBarContext = createContext<AnnouncementBarAPI | null>(null);

View file

@ -120,7 +120,7 @@ function useContextValue() {
return { return {
savePreferredVersion, savePreferredVersion,
}; };
}, [setState]); }, [versionPersistence]);
return [state, api] as const; return [state, api] as const;
} }

View file

@ -30,7 +30,7 @@ export function useDocsPreferredVersion(
(versionName: string) => { (versionName: string) => {
api.savePreferredVersion(pluginId, versionName); api.savePreferredVersion(pluginId, versionName);
}, },
[api], [api, pluginId],
); );
return {preferredVersion, savePreferredVersionName} as const; return {preferredVersion, savePreferredVersionName} as const;

View file

@ -83,6 +83,7 @@ function useShallowMemoizedObject<O extends Record<string, unknown>>(obj: O) {
return useMemo( return useMemo(
() => obj, () => obj,
// Is this safe? // Is this safe?
// eslint-disable-next-line react-hooks/exhaustive-deps
[...Object.keys(obj), ...Object.values(obj)], [...Object.keys(obj), ...Object.values(obj)],
); );
} }

View file

@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
// This hook is like useLayoutEffect, but without the SSR warning
// It seems hacky but it's used in many React libs (Redux, Formik...)
// Also mentioned here: https://github.com/facebook/react/issues/16956
// It is useful when you need to update a ref as soon as possible after a React render (before useEffect)
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
// Permits to transform an unstable callback (like an arrow function provided as props)
// to a "stable" callback that is safe to use in a useEffect dependency array
// Useful to avoid React stale closure problems + avoid useless effect re-executions
//
// Workaround until the React team recommends a good solution, see https://github.com/facebook/react/issues/16956
// This generally works has some potential drawbacks, such as https://github.com/facebook/react/issues/16956#issuecomment-536636418
export function useDynamicCallback<T extends (...args: never[]) => unknown>(
callback: T,
): T {
const ref = useRef<T>(callback);
useIsomorphicLayoutEffect(() => {
ref.current = callback;
}, [callback]);
// @ts-expect-error: TODO, not sure how to fix this TS error
return useCallback<T>((...args) => ref.current(...args), []);
}

View file

@ -15,6 +15,7 @@ import React, {
useMemo, useMemo,
useRef, useRef,
} from 'react'; } from 'react';
import {useDynamicCallback} from './reactUtils';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
/** /**
@ -103,20 +104,22 @@ export function useScrollPosition(
const {scrollEventsEnabledRef} = useScrollController(); const {scrollEventsEnabledRef} = useScrollController();
const lastPositionRef = useRef<ScrollPosition | null>(getScrollPosition()); const lastPositionRef = useRef<ScrollPosition | null>(getScrollPosition());
const dynamicEffect = useDynamicCallback(effect);
useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
if (!scrollEventsEnabledRef.current) { if (!scrollEventsEnabledRef.current) {
return; return;
} }
const currentPosition = getScrollPosition()!; const currentPosition = getScrollPosition()!;
if (effect) { if (dynamicEffect) {
effect(currentPosition, lastPositionRef.current); dynamicEffect(currentPosition, lastPositionRef.current);
} }
lastPositionRef.current = currentPosition; lastPositionRef.current = currentPosition;
}; };
useEffect(() => {
const opts: AddEventListenerOptions & EventListenerOptions = { const opts: AddEventListenerOptions & EventListenerOptions = {
passive: true, passive: true,
}; };
@ -125,7 +128,12 @@ export function useScrollPosition(
window.addEventListener('scroll', handleScroll, opts); window.addEventListener('scroll', handleScroll, opts);
return () => window.removeEventListener('scroll', handleScroll, opts); return () => window.removeEventListener('scroll', handleScroll, opts);
}, deps); }, [
dynamicEffect,
scrollEventsEnabledRef,
// eslint-disable-next-line react-hooks/exhaustive-deps
...deps,
]);
} }
type UseScrollPositionSaver = { type UseScrollPositionSaver = {
@ -170,7 +178,7 @@ function useScrollPositionSaver(): UseScrollPositionSaver {
return {restored: heightDiff !== 0}; return {restored: heightDiff !== 0};
}, []); }, []);
return useMemo(() => ({save, restore}), []); return useMemo(() => ({save, restore}), [restore, save]);
} }
type UseScrollPositionBlockerReturn = { type UseScrollPositionBlockerReturn = {
@ -217,7 +225,7 @@ export function useScrollPositionBlocker(): UseScrollPositionBlockerReturn {
} }
}; };
}, },
[scrollController], [scrollController, scrollPositionSaver],
); );
useLayoutEffect(() => { useLayoutEffect(() => {

View file

@ -9,6 +9,7 @@ import {useEffect} from 'react';
import {useLocation} from '@docusaurus/router'; import {useLocation} from '@docusaurus/router';
import {Location} from '@docusaurus/history'; import {Location} from '@docusaurus/history';
import {usePrevious} from './usePrevious'; import {usePrevious} from './usePrevious';
import {useDynamicCallback} from './reactUtils';
type LocationChangeEvent = { type LocationChangeEvent = {
location: Location; location: Location;
@ -21,10 +22,12 @@ export function useLocationChange(onLocationChange: OnLocationChange): void {
const location = useLocation(); const location = useLocation();
const previousLocation = usePrevious(location); const previousLocation = usePrevious(location);
const onLocationChangeDynamic = useDynamicCallback(onLocationChange);
useEffect(() => { useEffect(() => {
onLocationChange({ onLocationChangeDynamic({
location, location,
previousLocation, previousLocation,
}); });
}, [location]); }, [onLocationChangeDynamic, location, previousLocation]);
} }

View file

@ -5,12 +5,13 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {useRef, useEffect} from 'react'; import {useRef} from 'react';
import {useIsomorphicLayoutEffect} from './reactUtils';
export function usePrevious<T>(value: T): T | undefined { export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>(); const ref = useRef<T>();
useEffect(() => { useIsomorphicLayoutEffect(() => {
ref.current = value; ref.current = value;
}); });

View file

@ -16,7 +16,11 @@ import clsx from 'clsx';
import Head from '@docusaurus/Head'; import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {useTitleFormatter, usePluralForm} from '@docusaurus/theme-common'; import {
useTitleFormatter,
usePluralForm,
useDynamicCallback,
} from '@docusaurus/theme-common';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useAllDocsData} from '@theme/hooks/useDocs'; import {useAllDocsData} from '@theme/hooks/useDocs';
import useSearchQuery from '@theme/hooks/useSearchQuery'; import useSearchQuery from '@theme/hooks/useSearchQuery';
@ -173,6 +177,7 @@ function SearchPage() {
}, },
initialSearchResultState, initialSearchResultState,
); );
const algoliaClient = algoliaSearch(appId, apiKey); const algoliaClient = algoliaSearch(appId, apiKey);
const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, { const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, {
hitsPerPage: 15, hitsPerPage: 15,
@ -271,7 +276,7 @@ function SearchPage() {
description: 'The search page title for empty query', description: 'The search page title for empty query',
}); });
const makeSearch = (page = 0) => { const makeSearch = useDynamicCallback((page = 0) => {
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default'); algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale); algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale);
@ -285,18 +290,16 @@ function SearchPage() {
); );
algoliaHelper.setQuery(searchQuery).setPage(page).search(); algoliaHelper.setQuery(searchQuery).setPage(page).search();
}; });
useEffect(() => { useEffect(() => {
if (!loaderRef) { if (!loaderRef) {
return undefined; return undefined;
} }
const currentObserver = observer.current;
observer.current.observe(loaderRef); currentObserver.observe(loaderRef);
return () => currentObserver.unobserve(loaderRef);
return () => {
observer.current.unobserve(loaderRef);
};
}, [loaderRef]); }, [loaderRef]);
useEffect(() => { useEffect(() => {
@ -311,7 +314,12 @@ function SearchPage() {
makeSearch(); makeSearch();
}, 300); }, 300);
} }
}, [searchQuery, docsSearchVersionsHelpers.searchVersions]); }, [
searchQuery,
docsSearchVersionsHelpers.searchVersions,
updateSearchPath,
makeSearch,
]);
useEffect(() => { useEffect(() => {
if (!searchResultState.lastPage || searchResultState.lastPage === 0) { if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
@ -319,13 +327,13 @@ function SearchPage() {
} }
makeSearch(searchResultState.lastPage); makeSearch(searchResultState.lastPage);
}, [searchResultState.lastPage]); }, [makeSearch, searchResultState.lastPage]);
useEffect(() => { useEffect(() => {
if (searchValue && searchValue !== searchQuery) { if (searchValue && searchValue !== searchQuery) {
setSearchQuery(searchValue); setSearchQuery(searchValue);
} }
}, [searchValue]); }, [searchQuery, searchValue]);
return ( return (
<Layout wrapperClassName="search-page-wrapper"> <Layout wrapperClassName="search-page-wrapper">

View file

@ -90,16 +90,16 @@ function Link({
const IOSupported = ExecutionEnvironment.canUseIntersectionObserver; const IOSupported = ExecutionEnvironment.canUseIntersectionObserver;
let io: IntersectionObserver; const ioRef = useRef<IntersectionObserver>();
const handleIntersection = (el: HTMLAnchorElement, cb: () => void) => { const handleIntersection = (el: HTMLAnchorElement, cb: () => void) => {
io = new window.IntersectionObserver((entries) => { ioRef.current = new window.IntersectionObserver((entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (el === entry.target) { if (el === entry.target) {
// If element is in viewport, stop listening/observing and run callback. // If element is in viewport, stop listening/observing and run callback.
// https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
if (entry.isIntersecting || entry.intersectionRatio > 0) { if (entry.isIntersecting || entry.intersectionRatio > 0) {
io.unobserve(el); ioRef.current!.unobserve(el);
io.disconnect(); ioRef.current!.disconnect();
cb(); cb();
} }
} }
@ -107,7 +107,7 @@ function Link({
}); });
// Add element to the observer. // Add element to the observer.
io.observe(el); ioRef.current!.observe(el);
}; };
const handleRef = (ref: HTMLAnchorElement | null) => { const handleRef = (ref: HTMLAnchorElement | null) => {
@ -138,11 +138,11 @@ function Link({
// When unmounting, stop intersection observer from watching. // When unmounting, stop intersection observer from watching.
return () => { return () => {
if (IOSupported && io) { if (IOSupported && ioRef.current) {
io.disconnect(); ioRef.current.disconnect();
} }
}; };
}, [targetLink, IOSupported, isInternal]); }, [ioRef, targetLink, IOSupported, isInternal]);
const isAnchorLink = targetLink?.startsWith('#') ?? false; const isAnchorLink = targetLink?.startsWith('#') ?? false;
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink; const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;