Skip to content

Commit

Permalink
feat(ui-hooks): update useSafeMemo and useUpdatedRef
Browse files Browse the repository at this point in the history
also update 'react-hooks/exhaustive-deps' ESLint rule
  • Loading branch information
skamril committed Jan 22, 2025
1 parent c5d3157 commit fee70c7
Show file tree
Hide file tree
Showing 12 changed files with 63 additions and 39 deletions.
8 changes: 8 additions & 0 deletions webapp/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export default [
},
rules: {
...reactHookPlugin.configs.recommended.rules,
"react-hooks/exhaustive-deps": [
"warn",
{
// Includes hooks from 'react-use'
additionalHooks:
"(useSafeMemo|useUpdateEffectOnce|useDeepCompareEffect|useShallowCompareEffect|useCustomCompareEffect)",
},
],
"@typescript-eslint/array-type": ["error", { default: "array-simple" }],
"@typescript-eslint/no-restricted-imports": [
"error",
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/App/Singlestudy/FreezeStudy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Backdrop, Button, List, ListItem, ListItemText, Paper, Typography } fro
import { useEffect, useState } from "react";
import LinearProgressWithLabel from "@/components/common/LinearProgressWithLabel";
import { useTranslation } from "react-i18next";
import useAutoUpdateRef from "@/hooks/useAutoUpdateRef";
import useUpdatedRef from "@/hooks/useUpdatedRef";

interface BlockingTask {
id: TaskDTO["id"];
Expand Down Expand Up @@ -60,7 +60,7 @@ function FreezeStudy({ studyId }: FreezeStudyProps) {
const [blockingTasks, setBlockingTasks] = useState<BlockingTask[]>([]);
const { t } = useTranslation();
const hasLoadingTask = !!blockingTasks.find(isLoadingTask);
const blockingTasksRef = useAutoUpdateRef(blockingTasks);
const blockingTasksRef = useUpdatedRef(blockingTasks);

// Fetch blocking tasks and subscribe to their WebSocket channels
useEffect(() => {
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/common/Form/useFormApiPlus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {
} from "react-hook-form";
import * as RA from "ramda-adjunct";
import { useEffect, useMemo, useRef } from "react";
import useAutoUpdateRef from "../../../hooks/useUpdatedRef";
import useUpdatedRef from "../../../hooks/useUpdatedRef";
import type {
UseFormRegisterPlus,
UseFormReturnPlus,
Expand Down Expand Up @@ -57,7 +57,7 @@ function useFormApiPlus<TFieldValues extends FieldValues, TContext>(
const initialDefaultValues = useRef(isLoading ? undefined : getDefaultValues());

// Prevent to add the values in `useMemo`'s deps
const dataRef = useAutoUpdateRef({
const dataRef = useUpdatedRef({
...data,
// Don't read `formState` in `useMemo`. See `useEffect`'s comment below.
isSubmitting,
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/common/Form/useFormUndoRedo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { FieldValues } from "react-hook-form";
import { useCallback, useEffect, useRef } from "react";
import * as R from "ramda";
import type { UseFormReturnPlus } from "./types";
import useAutoUpdateRef from "../../../hooks/useUpdatedRef";
import useUpdatedRef from "../../../hooks/useUpdatedRef";

enum ActionType {
Undo = "UNDO",
Expand All @@ -34,7 +34,7 @@ function useFormUndoRedo<TFieldValues extends FieldValues, TContext>(
} = api;
const [state, { undo, redo, set, ...rest }] = useUndo(initialDefaultValues);
const lastAction = useRef<ActionType | "">("");
const dataRef = useAutoUpdateRef({ state, initialDefaultValues });
const dataRef = useUpdatedRef({ state, initialDefaultValues });

useEffect(
() => {
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/common/GroupedDataTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import ConfirmationDialog from "../dialogs/ConfirmationDialog";
import { generateUniqueValue, getTableOptionsForAlign } from "./utils";
import DuplicateDialog from "./DuplicateDialog";
import { translateWithColon } from "../../../utils/i18nUtils";
import useAutoUpdateRef from "../../../hooks/useUpdatedRef";
import useUpdatedRef from "../../../hooks/useUpdatedRef";
import * as R from "ramda";
import * as RA from "ramda-adjunct";
import type { PromiseAny } from "../../../utils/tsUtils";
Expand Down Expand Up @@ -85,7 +85,7 @@ function GroupedDataTable<TGroups extends string[], TData extends TRow<TGroups[n
const [rowSelection, setRowSelection] = useState<MRT_RowSelectionState>({});
const enqueueErrorSnackbar = useEnqueueErrorSnackbar();
// Allow to use the last version of `onNameClick` in `tableColumns`
const callbacksRef = useAutoUpdateRef({ onNameClick });
const callbacksRef = useUpdatedRef({ onNameClick });
const pendingRows = useRef<Array<TRow<TGroups[number]>>>([]);
const { createOps, deleteOps, totalOps } = useOperationInProgressCount();

Expand Down
6 changes: 3 additions & 3 deletions webapp/src/components/common/JSONEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useDeepCompareEffect, useMount } from "react-use";
import "jsoneditor/dist/jsoneditor.min.css";
import "./dark-theme.css";
import type { PromiseAny } from "../../../utils/tsUtils";
import useAutoUpdateRef from "../../../hooks/useUpdatedRef";
import useUpdatedRef from "../../../hooks/useUpdatedRef";
import { createSaveButton } from "./utils";
import * as R from "ramda";
import * as RA from "ramda-adjunct";
Expand All @@ -37,8 +37,8 @@ function JSONEditor(props: JSONEditorProps) {
const { json, onSave, onSaveSuccessful, ...options } = props;
const ref = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<JSONEditorClass>();
const onSaveRef = useAutoUpdateRef(onSave);
const callbackOptionsRef = useAutoUpdateRef<Partial<JSONEditorOptions>>(
const onSaveRef = useUpdatedRef(onSave);
const callbackOptionsRef = useUpdatedRef<Partial<JSONEditorOptions>>(
R.pickBy(RA.isFunction, options),
);
const saveBtn = useMemo(() => createSaveButton(handleSaveClick), []);
Expand Down
14 changes: 8 additions & 6 deletions webapp/src/components/common/TableForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Form, { type FormProps } from "../Form";
import Table, { type TableProps } from "./Table";
import { getCellType } from "./utils";
import { mergeSxProp } from "../../../utils/muiUtils";
import useMemoLocked from "../../../hooks/useMemoLocked";
import useSafeMemo from "../../../hooks/useMemoLocked";

type TableFieldValuesByRow = Record<IdType, Record<string, string | boolean | number>>;

Expand Down Expand Up @@ -61,11 +61,13 @@ function TableForm<TFieldValues extends TableFieldValuesByRow>(
const { columns, type, colHeaders, ...restTableProps } = tableProps;

// useForm's defaultValues are cached on the first render within the custom hook.
const defaultData = useMemoLocked(() =>
R.keys(defaultValues).map((id) => ({
...defaultValues[id],
id: id as IdType,
})),
const defaultData = useSafeMemo(
() =>
R.keys(defaultValues).map((id) => ({
...defaultValues[id],
id: id as IdType,
})),
[],
);

const formattedColumns = useMemo(() => {
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/hooks/useBlocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import type { History, Transition } from "history";
import { useContext, useEffect } from "react";
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";
import useAutoUpdateRef from "./useUpdatedRef";
import useUpdatedRef from "./useUpdatedRef";

// * Workaround until it will be supported by react-router v6.
// * Based on https://ui.dev/react-router-preventing-transitions
Expand All @@ -24,7 +24,7 @@ type Blocker = (tx: Transition) => void;

function useBlocker(blocker: Blocker, when = true): void {
const { navigator } = useContext(NavigationContext);
const blockerRef = useAutoUpdateRef(blocker);
const blockerRef = useUpdatedRef(blocker);

useEffect(
() => {
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/hooks/useConfirm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

import { useCallback, useRef, useState } from "react";
import useAutoUpdateRef from "./useUpdatedRef";
import useUpdatedRef from "./useUpdatedRef";

function errorFunction() {
throw new Error("Promise is not pending.");
Expand Down Expand Up @@ -61,7 +61,7 @@ function errorFunction() {
*/
function useConfirm() {
const [isPending, setIsPending] = useState(false);
const isPendingRef = useAutoUpdateRef(isPending);
const isPendingRef = useUpdatedRef(isPending);
const yesRef = useRef<VoidFunction>(errorFunction);
const noRef = useRef<VoidFunction>(errorFunction);
const cancelRef = useRef<VoidFunction>(errorFunction);
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/hooks/usePromiseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { type SnackbarKey, useSnackbar } from "notistack";
import useEnqueueErrorSnackbar from "./useEnqueueErrorSnackbar";
import { toError } from "../utils/fnUtils";
import { useCallback } from "react";
import useAutoUpdateRef from "./useUpdatedRef";
import useUpdatedRef from "./useUpdatedRef";

interface UsePromiseHandlerParams<T extends unknown[], U> {
fn: (...args: T) => Promise<U>;
Expand All @@ -34,7 +34,7 @@ interface UsePromiseHandlerParams<T extends unknown[], U> {
function usePromiseHandler<T extends unknown[], U>(params: UsePromiseHandlerParams<T, U>) {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const enqueueErrorSnackbar = useEnqueueErrorSnackbar();
const paramsRef = useAutoUpdateRef(params);
const paramsRef = useUpdatedRef(params);

const handlePromise = useCallback(
async (...args: T) => {
Expand Down
30 changes: 18 additions & 12 deletions webapp/src/hooks/useSafeMemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,26 @@
*/

import { useState } from "react";
import { useUpdateEffect } from "react-use";

/*
"You may rely on useMemo as a performance optimization, not as a semantic
guarantee. In the future, React may choose to “forget” some previously
memoized values and recalculate them on next render, e.g. to free memory for
offscreen components. Write your code so that it still works without useMemo —
and then add it to optimize performance."
Source: https://reactjs.org/docs/hooks-reference.html#usememo
*/
/**
* Hook that returns a memoized value with semantic guarantee.
*
* Semantic guarantee is not provided by `useMemo`, which is solely used
* for performance optimization (cf. https://react.dev/reference/react/useMemo#caveats).
*
* @param factory - A function that returns the value to memoize.
* @param deps - Dependencies that trigger the memoization.
* @returns The memoized value.
*/
function useSafeMemo<T>(factory: () => T, deps: React.DependencyList): T {
const [state, setState] = useState(factory);

useUpdateEffect(() => {
setState(factory);
}, deps);

function useMemoLocked<T>(factory: () => T): T {
// eslint-disable-next-line react/hook-use-state
const [state] = useState(factory);
return state;
}

export default useMemoLocked;
export default useSafeMemo;
16 changes: 12 additions & 4 deletions webapp/src/hooks/useUpdatedRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,24 @@
* This file is part of the Antares project.
*/

import { useEffect, useRef } from "react";
import { useLayoutEffect, useRef } from "react";

function useAutoUpdateRef<T>(value: T): React.MutableRefObject<T> {
/**
* Hook that returns a mutable ref that automatically updates its value.
*
* @param value - The value to store in the ref.
* @returns The mutable ref.
*/
function useUpdatedRef<T>(value: T): React.MutableRefObject<T> {
const ref = useRef(value);

useEffect(() => {
// `useLayoutEffect` runs before `useEffect`. So `useLayoutEffect` is used to make sure
// the value is up-to-date before any other code runs.
useLayoutEffect(() => {
ref.current = value;
});

return ref;
}

export default useAutoUpdateRef;
export default useUpdatedRef;

0 comments on commit fee70c7

Please sign in to comment.