这是一个演示:https ://stackblitz.com/edit/react-ts-rttbjn
这个演示按我的意愿工作。但是在 中PagingList.tsx
,会抛出 ESLint 警告:
ESLint: React Hook useEffect has a missing dependency: 'loadAfter'. Either include it or remove the dependency array.(react-hooks/exhaustive-deps)
但是使用[loadAfter]
不起作用。
所以我很困惑,为什么loadAfter
重新渲染时会改变?最佳做法是什么?
PS:非工作演示,无限重新渲染:https ://stackblitz.com/edit/react-ts-kl67zc
主要代码片段:
PS: .eslintrc.js
:
// eslint-disable-next-line no-undef
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "jest"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jest/recommended",
],
settings: {
react: {
version: "16.13",
},
},
};
index.tsx
:
import React, { FunctionComponent, useCallback } from "react";
import { PagingList } from "./PagingList";
import ReactDOM from "react-dom";
type Piece = {
id: number;
};
const MAX_COUNT = 20;
async function getAfter(
size: number,
id?: number
): Promise<[Piece[], boolean]> {
const from = Math.max(0, id || 0);
const toExclusive = Math.min(from + size, MAX_COUNT);
const items = Array.from({
length: toExclusive - from,
}).map((_, offset) => ({ id: from + offset + 1 }));
return new Promise((resolve) =>
setTimeout(() => resolve([items, toExclusive < MAX_COUNT]), 1000)
);
}
async function getBefore(
size: number,
id?: number
): Promise<[Piece[], boolean]> {
const toExclusive = Math.max(0, (id || 0) - 1);
const from = Math.max(toExclusive - size, 0);
const items = Array.from({
length: toExclusive - from,
}).map((_, offset) => ({ id: from + offset + 1 }));
return new Promise((resolve) =>
setTimeout(() => resolve([items, from > 0]), 1000)
);
}
const App: FunctionComponent = () => {
const Child = (piece: Piece) => (
<div key={piece.id}>
<span>Id: </span> {piece.id}
</div>
);
const getAfterCallback = useCallback(
(size: number, id?: number) => getAfter(size, id),
[]
);
const getBeforeCallback = useCallback(
(size: number, id?: number) => getBefore(size, id),
[]
);
return (
<PagingList
childFunction={Child}
getAfter={getAfterCallback}
getBefore={getBeforeCallback}
/>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
PagingList.tsx [Workable but with error]
:
import React, {
FunctionComponent,
useCallback,
useEffect,
useState,
} from "react";
const QUERY_SIZE = 10;
type Data = { id: number };
interface Props {
childFunction: (data: Data) => JSX.Element;
getAfter: (next: number, after?: number) => Promise<[Data[], boolean]>;
getBefore: (last: number, before?: number) => Promise<[Data[], boolean]>;
}
export const PagingList: FunctionComponent<Props> = ({
childFunction,
getAfter,
getBefore,
}) => {
const [after, setAfter] = useState<number | undefined>(undefined);
const [before, setBefore] = useState<number | undefined>(undefined);
const [data, setData] = useState<Data[]>([]);
const [hasNext, setHasNext] = useState(false);
const [hasLast, setHasLast] = useState(false);
const [isLoading, setLoading] = useState(false);
const loadAfter = useCallback(async () => {
if (isLoading) {
console.log("is loading");
return;
}
setLoading(true);
const [d, b] = await getAfter(QUERY_SIZE, after);
setHasNext(b);
setData(d);
setAfter(d[d.length - 1].id);
setBefore(d[0].id);
setHasLast(true);
setLoading(false);
}, [after, getAfter, isLoading]);
const loadBefore = useCallback(async () => {
if (isLoading) {
console.log("is loading");
return;
}
setLoading(true);
const [d, b] = await getBefore(QUERY_SIZE, before);
setHasLast(b);
setData(d);
setAfter(d[d.length - 1].id);
setBefore(d[0].id);
setHasLast(true);
setLoading(false);
}, [before, getBefore, isLoading]);
useEffect(() => {
loadAfter();
// ESLint: React Hook useEffect has a missing dependency: 'loadAfter'. Either include it or remove the dependency array.(react-hooks/exhaustive-deps)
// But `[loadAfter]` as dep array is not working
}, [getAfter]);
const body = () => {
if (isLoading) {
return <div>Loading...</div>;
} else {
return (
<div>
<div>{data.map((d) => childFunction(d))}</div>
<button disabled={!hasLast} onClick={loadBefore}>
Last Page
</button>
<button disabled={!hasNext} onClick={loadAfter}>
Next Page
</button>
</div>
);
}
};
return body();
};
PagingList.tsx [Not Working but no warning]
:
import React, {
FunctionComponent,
useCallback,
useEffect,
useState,
} from "react";
const QUERY_SIZE = 10;
type Data = { id: number };
interface Props {
childFunction: (data: Data) => JSX.Element;
getAfter: (next: number, after?: number) => Promise<[Data[], boolean]>;
getBefore: (last: number, before?: number) => Promise<[Data[], boolean]>;
}
export const PagingList: FunctionComponent<Props> = ({
childFunction,
getAfter,
getBefore,
}) => {
const [after, setAfter] = useState<number | undefined>(undefined);
const [before, setBefore] = useState<number | undefined>(undefined);
const [data, setData] = useState<Data[]>([]);
const [hasNext, setHasNext] = useState(false);
const [hasLast, setHasLast] = useState(false);
const [isLoading, setLoading] = useState(false);
const loadAfter = useCallback(async () => {
if (isLoading) {
console.log("is loading");
return;
}
setLoading(true);
const [d, b] = await getAfter(QUERY_SIZE, after);
setHasNext(b);
setData(d);
setAfter(d[d.length - 1].id);
setBefore(d[0].id);
setHasLast(true);
setLoading(false);
}, [after, getAfter, isLoading]);
const loadBefore = useCallback(async () => {
if (isLoading) {
console.log("is loading");
return;
}
setLoading(true);
const [d, b] = await getBefore(QUERY_SIZE, before);
setHasLast(b);
setData(d);
setAfter(d[d.length - 1].id);
setBefore(d[0].id);
setHasLast(true);
setLoading(false);
}, [before, getBefore, isLoading]);
useEffect(() => {
loadAfter();
// Now re-render infinitely, but now eslint warning
}, [loadAfter]);
const body = () => {
if (isLoading) {
return <div>Loading...</div>;
} else {
return (
<div>
<div>{data.map((d) => childFunction(d))}</div>
<button disabled={!hasLast} onClick={loadBefore}>
Last Page
</button>
<button disabled={!hasNext} onClick={loadAfter}>
Next Page
</button>
</div>
);
}
};
return body();
};