useEffect 多次渲染,一个解决方案有效但重构混淆了为什么它不再有效
useEffect rendering multiple times, one solution worked but refactoring has confused why it's no longer working
我有一个非常简单的组件,它通过 API 调用显示联系人列表。
allContacts.jsx
import React, { useEffect, useState } from 'react';
import PhoneLayout from './layouts/phoneLayout';
import AddContact from './addContact';
import ShowContact from './showContact';
export default function AllContacts() {
let [contacts, setContacts] = useState([]);
let [showContact, setShowContact] = useState(false);
useEffect(() => {
fetch(`http://localhost:8000/all-contacts`)
.then((response) => response.json())
.then((data) => setContacts(data))
.catch((err) => console.log(err));
}, []);
return (
<PhoneLayout>
<div className="relative">
<AddContact />
<div className="flex flex-col">
{contacts.map((contact) => (
<button onClick={() => setShowContact(true)}>
{showContact ? (
<ShowContact contactId={contact.id} open={true} />
) : (
<div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
<span className="font-bold">{contact.first_name}</span>{' '}
{contact.last_name}
</div>
)}
</button>
))}
</div>
</div>
</PhoneLayout>
);
}
我正在显示另一个组件并传递一些 props
。
<ShowContact contactId={contact.id} open={true} />
问题是 ShowContact
中的 API 调用似乎是为我的 API 数据库中的每个条目调用它。单击我在控制台中得到以下内容:
showContact.jsx:17 {contactId: 4, open: true}
showContact.jsx:17 {contactId: 5, open: true}
showContact.jsx:17 {contactId: 6, open: true}
showContact.jsx:17 {contactId: 7, open: true}
showContact.jsx:17 {contactId: 8, open: true}
showContact.jsx:17 {contactId: 9, open: true}
showContact.jsx:17 {contactId: 10, open: true}
showContact.jsx:17 {contactId: 11, open: true}
showContact.jsx:17 {contactId: 12, open: true}
showContact.jsx:17 {contactId: 13, open: true}
showContact.jsx:17 {contactId: 14, open: true}
showContact.jsx:17 {contactId: 15, open: true}
showContact.jsx:17 {contactId: 16, open: true}
showContact.jsx:17 {contactId: 17, open: true}
这是 ShowContact
个组件
// showContact.jsx
export default function ShowContact(props) {
let [isOpen, setIsOpen] = useState(false);
let [contact, setContact] = useState({});
// delete contact from api then reloads the page
function deleteContact() {
fetch(`http://localhost:8000/delete-contact/${props.contactId}`, {
method: 'DELETE',
}).catch((err) => console.log(err));
setIsOpen(false);
window.location.reload(true);
}
console.log(props);
// get contact from api by contact id
useEffect(() => {
async function fetchContact() {
await fetch(`http://localhost:8000/get-contact/${props.contactId}`)
.then((response) => response.json())
.then((data) => {
setContact(data);
console.log(data);
})
.catch((err) => console.log(err));
}
if (!props.open) {
fetchContact();
}
}, []);
return ( ...
我已经在 uesEffect
块中反转了条件,所以我可以向您展示所有发生的事情。我在 pass 中以这种方式使用了这个 useEffect
,但是组件略有不同。像这样:
export default function MyModal(props) {
let [isOpen, setIsOpen] = useState(false);
let completeButtonRef = useRef(null);
function closeModal() {
setIsOpen(false);
}
function openModal() {
setIsOpen(true);
}
// get contact from api by contact id
useEffect(() => {
async function fetchContact() {
await fetch(`http://localhost:8000/get-contact/${props.contactId}`)
.then((response) => response.json())
.then((data) => {
setContact(data);
console.log(data);
})
.catch((err) => console.log(err));
}
if (isOpen) {
fetchContact();
}
}, [isOpen]);
return (
<>
<button
type="button"
onClick={openModal}
ref={completeButtonRef}
className="rounded-md bg-black bg-opacity-20 px-4 py-2 text-sm font-medium text-white hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
Edit
</button>
正如你在这里看到的,上面的组件有一个按钮可以设置 isOpen
,而我尝试使用的组件没有这个,因此它似乎在调用 API对于每个条目。这就是为什么我想通过道具传递一个布尔值,但这似乎再次触发了 useEffect
。
我看过了:
- UseEffect being called multiple times
- 还有许多其他人解决了我原来的
above example
问题,但我正在重新设计它,我不想要编辑按钮
问题是您只有 一个 标志 (showContact
),但似乎希望每个联系人都有一个标志。但它控制着是否全部 可见:
{contacts.map((contact) => (
<button onClick={() => setShowContact(true)}>
{showContact ? ( // <===================================== one flag for all elements
<ShowContact contactId={contact.id} open={true} />
) : (
<div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
<span className="font-bold">{contact.first_name}</span>{' '}
{contact.last_name}
</div>
)}
</button>
))}
相反,如果您希望一次只显示一个联系人,请使用 contact.id
,也许以 null
开头表示“none”:
const [showContact, setShowContact] = useState(null);
然后在点击时设置联系人ID,只显示具有匹配ID的联系人:
{contacts.map((contact) => (
<button onClick={() => setShowContact(show => show === contact.id ? null : contact.id)}>
{showContact === contact.id ? (
<ShowContact contactId={contact.id} open={true} />
) : (
<div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
<span className="font-bold">{contact.first_name}</span>{' '}
{contact.last_name}
</div>
)}
</button>
))}
由于您显示的联系人可以看到该按钮,因此当您单击已显示的联系人上的按钮时,我通过设置 null
来切换它。
这是一个简化的例子:
const { useState, useEffect } = React;
const getAllContacts = (signal) => {
return new Promise((resolve, reject) => {
// Fake ajax
setTimeout(() => {
if (signal.aborted) {
reject(new Error("Operation cancelled"));
} else {
resolve([
{id: 1, name: "Joe Bloggs"},
{id: 2, name: "Valeria Hernandez"},
{id: 3, name: "Indira Patel"},
{id: 4, name: "Mohammed Abu-Yasein"},
]);
}
}, 800);
});
};
// Fake details
const contactDetails = new Map([
[1, Math.floor(Math.random() * 100)],
[2, Math.floor(Math.random() * 100)],
[3, Math.floor(Math.random() * 100)],
[4, Math.floor(Math.random() * 100)],
]);
const ShowContact = ({contact: {id, name}}) => {
const [details, setDetails] = useState(null);
useEffect(() => {
// This is very simplistic, see the more proper `useEffect` example above
console.log(`Loading details for contact #{id}...`);
setTimeout(() => {
console.log(`Loaded details for contact #{id}`);
setDetails(`high score: ${contactDetails.get(id)}`);
}, 250);
}, [id]);
return <div>{id}: {name} - {details ? details : <em>loading details...</em>}</div>;
};
const Example = () => {
const [showContact, setShowContact] = useState(null);
const [contacts, setContacts] = useState([]);
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
getAllContacts(signal)
.then(contacts => {
if (signal.aborted) {
return;
}
setContacts(contacts);
})
.catch(error => {
if (!signal.aborted) {
// ...handle/report error...
}
});
return () => {
controller.abort(); // Cancel the operation, if pending
};
}, []);
return <div>
{contacts.map(contact => <div key={contact.id} className="contact">
<input type="button" onClick={() => setShowContact(id => id === contact.id ? null : contact.id)} value="Show/Hide" />
{
showContact === contact.id
? <ShowContact contact={contact} />
: <div>Contact #{contact.id}</div>
}
</div>)}
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
.contact {
padding: 4px;
border: 1px solid gray;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
我有一个非常简单的组件,它通过 API 调用显示联系人列表。
allContacts.jsx
import React, { useEffect, useState } from 'react';
import PhoneLayout from './layouts/phoneLayout';
import AddContact from './addContact';
import ShowContact from './showContact';
export default function AllContacts() {
let [contacts, setContacts] = useState([]);
let [showContact, setShowContact] = useState(false);
useEffect(() => {
fetch(`http://localhost:8000/all-contacts`)
.then((response) => response.json())
.then((data) => setContacts(data))
.catch((err) => console.log(err));
}, []);
return (
<PhoneLayout>
<div className="relative">
<AddContact />
<div className="flex flex-col">
{contacts.map((contact) => (
<button onClick={() => setShowContact(true)}>
{showContact ? (
<ShowContact contactId={contact.id} open={true} />
) : (
<div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
<span className="font-bold">{contact.first_name}</span>{' '}
{contact.last_name}
</div>
)}
</button>
))}
</div>
</div>
</PhoneLayout>
);
}
我正在显示另一个组件并传递一些 props
。
<ShowContact contactId={contact.id} open={true} />
问题是 ShowContact
中的 API 调用似乎是为我的 API 数据库中的每个条目调用它。单击我在控制台中得到以下内容:
showContact.jsx:17 {contactId: 4, open: true}
showContact.jsx:17 {contactId: 5, open: true}
showContact.jsx:17 {contactId: 6, open: true}
showContact.jsx:17 {contactId: 7, open: true}
showContact.jsx:17 {contactId: 8, open: true}
showContact.jsx:17 {contactId: 9, open: true}
showContact.jsx:17 {contactId: 10, open: true}
showContact.jsx:17 {contactId: 11, open: true}
showContact.jsx:17 {contactId: 12, open: true}
showContact.jsx:17 {contactId: 13, open: true}
showContact.jsx:17 {contactId: 14, open: true}
showContact.jsx:17 {contactId: 15, open: true}
showContact.jsx:17 {contactId: 16, open: true}
showContact.jsx:17 {contactId: 17, open: true}
这是 ShowContact
个组件
// showContact.jsx
export default function ShowContact(props) {
let [isOpen, setIsOpen] = useState(false);
let [contact, setContact] = useState({});
// delete contact from api then reloads the page
function deleteContact() {
fetch(`http://localhost:8000/delete-contact/${props.contactId}`, {
method: 'DELETE',
}).catch((err) => console.log(err));
setIsOpen(false);
window.location.reload(true);
}
console.log(props);
// get contact from api by contact id
useEffect(() => {
async function fetchContact() {
await fetch(`http://localhost:8000/get-contact/${props.contactId}`)
.then((response) => response.json())
.then((data) => {
setContact(data);
console.log(data);
})
.catch((err) => console.log(err));
}
if (!props.open) {
fetchContact();
}
}, []);
return ( ...
我已经在 uesEffect
块中反转了条件,所以我可以向您展示所有发生的事情。我在 pass 中以这种方式使用了这个 useEffect
,但是组件略有不同。像这样:
export default function MyModal(props) {
let [isOpen, setIsOpen] = useState(false);
let completeButtonRef = useRef(null);
function closeModal() {
setIsOpen(false);
}
function openModal() {
setIsOpen(true);
}
// get contact from api by contact id
useEffect(() => {
async function fetchContact() {
await fetch(`http://localhost:8000/get-contact/${props.contactId}`)
.then((response) => response.json())
.then((data) => {
setContact(data);
console.log(data);
})
.catch((err) => console.log(err));
}
if (isOpen) {
fetchContact();
}
}, [isOpen]);
return (
<>
<button
type="button"
onClick={openModal}
ref={completeButtonRef}
className="rounded-md bg-black bg-opacity-20 px-4 py-2 text-sm font-medium text-white hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
Edit
</button>
正如你在这里看到的,上面的组件有一个按钮可以设置 isOpen
,而我尝试使用的组件没有这个,因此它似乎在调用 API对于每个条目。这就是为什么我想通过道具传递一个布尔值,但这似乎再次触发了 useEffect
。
我看过了:
- UseEffect being called multiple times
- 还有许多其他人解决了我原来的
above example
问题,但我正在重新设计它,我不想要编辑按钮
问题是您只有 一个 标志 (showContact
),但似乎希望每个联系人都有一个标志。但它控制着是否全部 可见:
{contacts.map((contact) => (
<button onClick={() => setShowContact(true)}>
{showContact ? ( // <===================================== one flag for all elements
<ShowContact contactId={contact.id} open={true} />
) : (
<div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
<span className="font-bold">{contact.first_name}</span>{' '}
{contact.last_name}
</div>
)}
</button>
))}
相反,如果您希望一次只显示一个联系人,请使用 contact.id
,也许以 null
开头表示“none”:
const [showContact, setShowContact] = useState(null);
然后在点击时设置联系人ID,只显示具有匹配ID的联系人:
{contacts.map((contact) => (
<button onClick={() => setShowContact(show => show === contact.id ? null : contact.id)}>
{showContact === contact.id ? (
<ShowContact contactId={contact.id} open={true} />
) : (
<div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
<span className="font-bold">{contact.first_name}</span>{' '}
{contact.last_name}
</div>
)}
</button>
))}
由于您显示的联系人可以看到该按钮,因此当您单击已显示的联系人上的按钮时,我通过设置 null
来切换它。
这是一个简化的例子:
const { useState, useEffect } = React;
const getAllContacts = (signal) => {
return new Promise((resolve, reject) => {
// Fake ajax
setTimeout(() => {
if (signal.aborted) {
reject(new Error("Operation cancelled"));
} else {
resolve([
{id: 1, name: "Joe Bloggs"},
{id: 2, name: "Valeria Hernandez"},
{id: 3, name: "Indira Patel"},
{id: 4, name: "Mohammed Abu-Yasein"},
]);
}
}, 800);
});
};
// Fake details
const contactDetails = new Map([
[1, Math.floor(Math.random() * 100)],
[2, Math.floor(Math.random() * 100)],
[3, Math.floor(Math.random() * 100)],
[4, Math.floor(Math.random() * 100)],
]);
const ShowContact = ({contact: {id, name}}) => {
const [details, setDetails] = useState(null);
useEffect(() => {
// This is very simplistic, see the more proper `useEffect` example above
console.log(`Loading details for contact #{id}...`);
setTimeout(() => {
console.log(`Loaded details for contact #{id}`);
setDetails(`high score: ${contactDetails.get(id)}`);
}, 250);
}, [id]);
return <div>{id}: {name} - {details ? details : <em>loading details...</em>}</div>;
};
const Example = () => {
const [showContact, setShowContact] = useState(null);
const [contacts, setContacts] = useState([]);
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
getAllContacts(signal)
.then(contacts => {
if (signal.aborted) {
return;
}
setContacts(contacts);
})
.catch(error => {
if (!signal.aborted) {
// ...handle/report error...
}
});
return () => {
controller.abort(); // Cancel the operation, if pending
};
}, []);
return <div>
{contacts.map(contact => <div key={contact.id} className="contact">
<input type="button" onClick={() => setShowContact(id => id === contact.id ? null : contact.id)} value="Show/Hide" />
{
showContact === contact.id
? <ShowContact contact={contact} />
: <div>Contact #{contact.id}</div>
}
</div>)}
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
.contact {
padding: 4px;
border: 1px solid gray;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>