设置状态时重新渲染太多 - useSWR
Too many re-renders when setting state - useSWR
我正在使用 useSWR 获取数据,然后使用数据,我想通过使用 reduce 来获得总计。如果我 console.log 值输出它工作正常但是一旦我尝试用值设置状态我得到 'Too may re-renders' 消息。
import Admin from "../../../components/admin/Admin";
import { useRouter } from "next/router";
import styles from "../../../styles/Dashboard.module.css";
import { getSession, useSession } from "next-auth/client";
import { useState } from "react";
/* BOOTSTRAP */
import Col from "react-bootstrap/Col";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Spinner from "react-bootstrap/Spinner";
import Table from "react-bootstrap/Table";
import useSWR from "swr";
import axios from "axios";
const General = () => {
const [session, loading] = useSession();
const [adults, setAdults] = useState(null);
const router = useRouter();
const { id } = router.query;
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
`http://localhost:8000/api/admin/general/${id}`,
fetcher
);
if (data) {
const adults = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adults.reduce(reducer, 0);
setAdults(totalAdults);
}
return (
<Admin>
<div className={styles.admin_banner}>
<Container fluid>
<Row>
<Col>
<h2>Bookings</h2>
<h6>
{adults}
</h6>
</Col>
</Row>
</Container>
</div>
<Container fluid>
<Row>
<Col>
<div className={styles.admin_container}>
{!error && !data && (
<Spinner animation="border" role="status">
<span className="sr-only">Loading...</span>
</Spinner>
)}
{!error && data && (
<Table responsive="md">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{data &&
!error &&
data.map((d) => (
<tr key={d._id}>
<td>
{d.firstName} {d.lastName}
</td>
</tr>
))}
</tbody>
</Table>
)}
</div>
</Col>
</Row>
</Container>
</Admin>
);
};
export async function getServerSideProps(context) {
const session = await getSession({
req: context.req,
});
if (!session) {
return {
redirect: {
destination: "/admin",
permanent: false,
},
};
} else {
return { props: session };
}
}
export default General;
问题是您的数据检查中的 setAdults(totalAdults);
导致了无限循环。 React 中的钩子导致组件重新渲染,所以实际发生的是
data
为真 --> setAdults
被触发 --> 重新渲染 --> data
为真 --> ...
将您的逻辑移动到 useEffect 挂钩中:
React.useEffect(() => {
const fetcher = url =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then(res => {
const adults = data.map(a => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adults.reduce(reducer, 0);
setAdults(totalAdults);
});
useSWR(`http://localhost:8000/api/admin/general/${id}`, fetcher);
}, []);
同时更新您的加载和错误检查。也许为两者创建一个新的状态变量。
您应该使用依赖于数据值的 useEffect,这样您就可以仅在数据在渲染之间发生变化时更新它,接受的答案使用了 useEffect 中的 useSWR 挂钩,这是不正确的
示例:
const General = () => {
const [session, loading] = useSession();
const [adults, setAdults] = useState(null);
const router = useRouter();
const { id } = router.query;
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
`http://localhost:8000/api/admin/general/${id}`,
fetcher
);
useEffect(() => {
const adultsFetch = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adultsFetch.reduce(reducer, 0);
setAdults(totalAdults);
}, [data]);
}
编辑:
您还可以使用 useMemo 挂钩,这样您甚至不再需要 useState,每次数据依赖关系发生变化时,useMemo 都会重新计算成人。
更多信息:https://reactjs.org/docs/hooks-reference.html#usememo
const General = () => {
const [session, loading] = useSession();
const router = useRouter();
const { id } = router.query;
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
`http://localhost:8000/api/admin/general/${id}`,
fetcher
);
const adults = useMemo(() => {
if (!data) return null;
const adultsFetch = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adultsFetch.reduce(reducer, 0);
return totalAdults;
}, [data]);
}
我遇到了同样的问题,这是我的第一种方法(当然数据与你的不同):
const General = () => {
const [session, loading] = useSession();
const [adults, setAdults] = useState(null);
const router = useRouter();
const { id } = router.query;
const [takeData, setTakeData] = useState(false)
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
takeData ? `http://localhost:8000/api/admin/general/${id}` : null,
fetcher
);
useEffect(() => {
setTakeData(true)
}, [])
if (data) {
const adults = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adults.reduce(reducer, 0);
setAdults(totalAdults);
}
问题是一旦状态发生变化,就会发生重新渲染。
在这种情况下,useSWR 返回数据(可能)并在第一次呈现页面之前更改 setAdults
的状态。此时 adults
已更改,因此在返回页面之前再次进行渲染,因此再次触发 useSWR 并再次设置 adults
并再次调用渲染,这将继续无限循环。
为了解决这个问题,我决定仅当 takeData 为真时才发送请求,并且只有在第一次呈现页面后 takeData 才变为真(因为在这种情况下,这就是要做的 useEffect
)。但是它没有用,我不知道为什么。
所以我采用了第二种方案:
const General = () => {
const [session, loading] = useSession();
const [adults, setAdults] = useState(null);
const router = useRouter();
const { id } = router.query;
const [takeData, setTakeData] = useState(false)
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
takeData ? `http://localhost:8000/api/admin/general/${id}` : null,
fetcher
);
useEffect(() => {
if (data) {
const adults = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adults.reduce(reducer, 0);
setAdults(totalAdults);
}
}, []);
此处不是在第一次渲染后发出请求,而是立即完成请求,但在第一次渲染后检查数据。
第二种解决方案对我有用。
如果有人知道为什么第一个解决方案不起作用,请告诉我 ;)
您可以通过添加第三个参数(选项)来实现,如下所示:
const { data, error } = useSWR(
`http://localhost:8000/api/admin/general/${id}`,
fetcher,
{
revalidateOnFocus: false,
revalidateIfStale: false,
// revalidateOnReconnect: false, // personally, I didn't need this one
}
);
它基本上是缓存数据,一旦有缓存就防止重新生效。 Here is the link to the docs.
或者,根据文档,您可以使用 useSWRImmutable
,从 1.0 版开始,获得与默认设置相同的结果。
import useSWRImmutable from "swr/immutable"
const { data, error } = useSWRImmutable(`http://localhost:8000/api/admin/general/${id}`, fetcher)
我正在使用 useSWR 获取数据,然后使用数据,我想通过使用 reduce 来获得总计。如果我 console.log 值输出它工作正常但是一旦我尝试用值设置状态我得到 'Too may re-renders' 消息。
import Admin from "../../../components/admin/Admin";
import { useRouter } from "next/router";
import styles from "../../../styles/Dashboard.module.css";
import { getSession, useSession } from "next-auth/client";
import { useState } from "react";
/* BOOTSTRAP */
import Col from "react-bootstrap/Col";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Spinner from "react-bootstrap/Spinner";
import Table from "react-bootstrap/Table";
import useSWR from "swr";
import axios from "axios";
const General = () => {
const [session, loading] = useSession();
const [adults, setAdults] = useState(null);
const router = useRouter();
const { id } = router.query;
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
`http://localhost:8000/api/admin/general/${id}`,
fetcher
);
if (data) {
const adults = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adults.reduce(reducer, 0);
setAdults(totalAdults);
}
return (
<Admin>
<div className={styles.admin_banner}>
<Container fluid>
<Row>
<Col>
<h2>Bookings</h2>
<h6>
{adults}
</h6>
</Col>
</Row>
</Container>
</div>
<Container fluid>
<Row>
<Col>
<div className={styles.admin_container}>
{!error && !data && (
<Spinner animation="border" role="status">
<span className="sr-only">Loading...</span>
</Spinner>
)}
{!error && data && (
<Table responsive="md">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{data &&
!error &&
data.map((d) => (
<tr key={d._id}>
<td>
{d.firstName} {d.lastName}
</td>
</tr>
))}
</tbody>
</Table>
)}
</div>
</Col>
</Row>
</Container>
</Admin>
);
};
export async function getServerSideProps(context) {
const session = await getSession({
req: context.req,
});
if (!session) {
return {
redirect: {
destination: "/admin",
permanent: false,
},
};
} else {
return { props: session };
}
}
export default General;
问题是您的数据检查中的 setAdults(totalAdults);
导致了无限循环。 React 中的钩子导致组件重新渲染,所以实际发生的是
data
为真 --> setAdults
被触发 --> 重新渲染 --> data
为真 --> ...
将您的逻辑移动到 useEffect 挂钩中:
React.useEffect(() => {
const fetcher = url =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then(res => {
const adults = data.map(a => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adults.reduce(reducer, 0);
setAdults(totalAdults);
});
useSWR(`http://localhost:8000/api/admin/general/${id}`, fetcher);
}, []);
同时更新您的加载和错误检查。也许为两者创建一个新的状态变量。
您应该使用依赖于数据值的 useEffect,这样您就可以仅在数据在渲染之间发生变化时更新它,接受的答案使用了 useEffect 中的 useSWR 挂钩,这是不正确的
示例:
const General = () => {
const [session, loading] = useSession();
const [adults, setAdults] = useState(null);
const router = useRouter();
const { id } = router.query;
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
`http://localhost:8000/api/admin/general/${id}`,
fetcher
);
useEffect(() => {
const adultsFetch = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adultsFetch.reduce(reducer, 0);
setAdults(totalAdults);
}, [data]);
}
编辑:
您还可以使用 useMemo 挂钩,这样您甚至不再需要 useState,每次数据依赖关系发生变化时,useMemo 都会重新计算成人。
更多信息:https://reactjs.org/docs/hooks-reference.html#usememo
const General = () => {
const [session, loading] = useSession();
const router = useRouter();
const { id } = router.query;
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
`http://localhost:8000/api/admin/general/${id}`,
fetcher
);
const adults = useMemo(() => {
if (!data) return null;
const adultsFetch = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adultsFetch.reduce(reducer, 0);
return totalAdults;
}, [data]);
}
我遇到了同样的问题,这是我的第一种方法(当然数据与你的不同):
const General = () => {
const [session, loading] = useSession();
const [adults, setAdults] = useState(null);
const router = useRouter();
const { id } = router.query;
const [takeData, setTakeData] = useState(false)
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
takeData ? `http://localhost:8000/api/admin/general/${id}` : null,
fetcher
);
useEffect(() => {
setTakeData(true)
}, [])
if (data) {
const adults = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adults.reduce(reducer, 0);
setAdults(totalAdults);
}
问题是一旦状态发生变化,就会发生重新渲染。
在这种情况下,useSWR 返回数据(可能)并在第一次呈现页面之前更改 setAdults
的状态。此时 adults
已更改,因此在返回页面之前再次进行渲染,因此再次触发 useSWR 并再次设置 adults
并再次调用渲染,这将继续无限循环。
为了解决这个问题,我决定仅当 takeData 为真时才发送请求,并且只有在第一次呈现页面后 takeData 才变为真(因为在这种情况下,这就是要做的 useEffect
)。但是它没有用,我不知道为什么。
所以我采用了第二种方案:
const General = () => {
const [session, loading] = useSession();
const [adults, setAdults] = useState(null);
const router = useRouter();
const { id } = router.query;
const [takeData, setTakeData] = useState(false)
const fetcher = (url) =>
axios
.get(url, {
headers: { Authorization: "Bearer " + session.user.servertoken },
})
.then((res) => res.data);
const { data, error } = useSWR(
takeData ? `http://localhost:8000/api/admin/general/${id}` : null,
fetcher
);
useEffect(() => {
if (data) {
const adults = data.map((a) => a.adults);
const reducer = (accumlator, item) => {
return accumlator + item;
};
const totalAdults = adults.reduce(reducer, 0);
setAdults(totalAdults);
}
}, []);
此处不是在第一次渲染后发出请求,而是立即完成请求,但在第一次渲染后检查数据。 第二种解决方案对我有用。
如果有人知道为什么第一个解决方案不起作用,请告诉我 ;)
您可以通过添加第三个参数(选项)来实现,如下所示:
const { data, error } = useSWR(
`http://localhost:8000/api/admin/general/${id}`,
fetcher,
{
revalidateOnFocus: false,
revalidateIfStale: false,
// revalidateOnReconnect: false, // personally, I didn't need this one
}
);
它基本上是缓存数据,一旦有缓存就防止重新生效。 Here is the link to the docs.
或者,根据文档,您可以使用 useSWRImmutable
,从 1.0 版开始,获得与默认设置相同的结果。
import useSWRImmutable from "swr/immutable"
const { data, error } = useSWRImmutable(`http://localhost:8000/api/admin/general/${id}`, fetcher)