Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui-hooks): update useSafeMemo and useUpdatedRef #2309

Merged
merged 2 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions webapp/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ export default [
"react/hook-use-state": "error",
"react/prop-types": "off",
"react/self-closing-comp": "error",
"react-hooks/exhaustive-deps": [
"warn",
{
// Includes hooks from 'react-use'
additionalHooks:
"(useSafeMemo|useUpdateEffectOnce|useDeepCompareEffect|useShallowCompareEffect|useCustomCompareEffect)",
},
],
"require-await": "warn", // TODO: switch to "error" when the quantity of warning will be low
},
},
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 { 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 @@
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 Expand Up @@ -179,7 +179,7 @@
unsubscribeWsChannels();
window.clearInterval(intervalId);
};
}, [studyId]);

Check warning on line 182 in webapp/src/components/App/Singlestudy/FreezeStudy.tsx

View workflow job for this annotation

GitHub Actions / npm-lint

React Hook useEffect has a missing dependency: 'blockingTasksRef'. Either include it or remove the dependency array

return (
<Backdrop open={blockingTasks.length > 0} sx={{ position: "absolute" }}>
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/useAutoUpdateRef";
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/useAutoUpdateRef";
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/useAutoUpdateRef";
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/useAutoUpdateRef";
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/useSafeMemo";

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
27 changes: 0 additions & 27 deletions webapp/src/hooks/useAutoUpdateRef.ts

This file was deleted.

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 "./useAutoUpdateRef";
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 "./useAutoUpdateRef";
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
32 changes: 0 additions & 32 deletions webapp/src/hooks/useMemoLocked.ts

This file was deleted.

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 "./useAutoUpdateRef";
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
38 changes: 38 additions & 0 deletions webapp/src/hooks/useSafeMemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

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

/**
* 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);

return state;
}

export default useSafeMemo;
35 changes: 35 additions & 0 deletions webapp/src/hooks/useUpdatedRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

import { useLayoutEffect, useRef } from "react";

/**
* 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);

// `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 useUpdatedRef;
Loading