单击两次以显示反应 bootstrap 弹出窗口
Takes two clicks for react bootstrap popover to show up
我 运行 在尝试构建允许用户单击单词并在 bootstrap 弹出窗口中获取其定义的页面时遇到了问题。这是通过发送 API 请求并使用接收到的数据更新状态来实现的。
问题是弹出窗口仅在第二次单击单词后出现。 useEffect()
中的 console.log()
表示每次单击新词时都会发出 API 请求。要使弹出框出现,必须单击两次相同的单词。要是一键搞定就更好了
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
export default function App() {
const [text, setText] = useState(
"He looked at her and saw her eyes luminous with pity."
);
const [selectedWord, setSelectedWord] = useState("luminous");
const [apiData, setApiData] = useState([
{
word: "",
phonetics: [{ text: "" }],
meanings: [{ definitions: [{ definition: "", example: "" }] }]
}
]);
const words = text.split(/ /g);
useEffect(() => {
var url = "https://api.dictionaryapi.dev/api/v2/entries/en/" + selectedWord;
axios
.get(url)
.then(response => {
setApiData(response.data)
console.log("api call")
})
.catch(function (error) {
if (error) {
console.log("Error", error.message);
}
});
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const popover = (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</Popover.Body>
</Popover>
);
return (
<Alert>
{words.map((w) => (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={popover}
>
<span onClick={() => clickCallback(w)}> {w}</span>
</OverlayTrigger>
))}
</Alert>
);
}
更新:
更改了 apiData
初始化和 <Popover.Body>
组件。这并没有解决问题。
const [apiData, setApiData] = useState(null)
<Popover.Body>
{
apiData ?
<div>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</div> :
<div>Loading...</div>
}
</Popover.Body>
问题
这是我认为正在发生的事情:
- 组件渲染
- 开始获取“luminous”的定义。
- “luminous”的定义已完成获取。它调用
setApiData(data)
.
- 组件重新渲染
- 如果您点击“luminous”,弹出器会立即显示,这是因为弹出器的数据已准备就绪,setSelectedWord("luminous") 什么都不做。
- 如果您单击另一个词,例如“可惜”,弹出器会尝试显示,但
setSelectedWord("pity")
会导致组件开始重新呈现。
- 组件重新渲染
- 开始获取“可惜”的定义。
- “可惜”的定义已完成获取。它调用
setApiData(data)
.
- 组件重新渲染
- 如果单击“pity”,弹出器会立即显示,这是因为弹出器的数据已准备就绪,setSelectedWord(“pity”) 什么都不做。
选择另一个词将一遍又一遍地重复此过程。
要解决此问题,您需要先使用 show
属性 在渲染后显示弹出窗口(如果它与 selected 词匹配)。但是如果这个词出现多次怎么办?如果您对“她”一词执行此操作,它会在多个位置显示弹出窗口。因此,您不必为每个词分配一个唯一的 ID 并与之进行比较,而不是与每个词进行比较。
修复组件
要为单词分配一个在渲染之间不会改变的 ID,我们需要在组件顶部为它们分配 ID 并将它们存储在一个数组中。为了使这个“更简单”,我们可以将该逻辑抽象为组件外部的可重用函数:
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading punctuation
.replace(/[.,!?"]+$/, "") // remove trailing punctuation
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
// we can use this to prevent unnecessary rerenders
words.text = text;
// return the array-object
return words;
}
在组件中,我们需要设置状态变量来保存单词数组。通过将回调传递给 useState
,React 只会在第一次渲染时执行它,并跳过在重新渲染时调用它。
// set up state array of words that have their own UUIDs
// note: we don't want to call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
现在我们有 words
和 _setWords
,我们可以从中提取 text
值:
// extract text from words array for convenience
// probably not needed
const text = words.text;
接下来,我们可以创建自己的 setText
回调。这可能更简单,但我想确保我们支持 React 的变异更新语法 (setText(oldValue => newValue)
):
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
接下来,我们需要设置当前selected 的单词。定义可用后,将显示该词的弹出窗口。
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
如果您不想默认显示某个词,请使用:
const [selectedWordObj, setSelectedWordObj] = useState(); // nothing selected by default
要修复 API 调用,我们需要使用“使用异步效果”模式(有一些库可以简化这一点):
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
上面的代码块确保在不再需要时避免调用 setApiData
方法。它还使用 status
属性 来跟踪它的进度,以便您可以正确呈现结果。
现在定义一个显示加载消息的弹出窗口:
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
我们可以将加载弹出窗口与 apiData
混合以获得显示定义的弹出窗口。如果我们仍在加载定义,请使用加载定义。如果我们有错误,请显示错误。如果它正确完成,渲染出定义。为了使这更容易,我们可以将此逻辑放在组件外部的函数中,如下所示:
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
我们在组件中调用这个函数使用:
const selectedWordPopover = getPopover(apiData, loadingPopover);
最后,我们渲染出文字。因为我们要渲染一个数组,所以我们需要使用 key
属性 来设置每个单词的 ID。我们还需要 select 被点击的词 - 即使有多个相同的词,我们只想要被点击的词。为此,我们也会检查它的 Id。如果我们点击一个特定的词,我们需要确保我们点击的是 selected。我们还需要用标点符号渲染出原始单词。这一切都在这个块中完成:
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
完整代码
将所有这些放在一起得出:
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading characters
.replace(/[.,!?"]+$/, "") // remove trailing characters
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
words.text = text;
// return the array
return words;
}
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
export default function App() {
// set up state array of words that have their own UUIDs
// note: don't call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
// extract text from words array for convenience
const text = words.text;
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
const selectedWordPopover = getPopover(apiData, loadingPopover);
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
}
注意:您可以通过缓存 API 调用的结果来改进这一点。
我 运行 在尝试构建允许用户单击单词并在 bootstrap 弹出窗口中获取其定义的页面时遇到了问题。这是通过发送 API 请求并使用接收到的数据更新状态来实现的。
问题是弹出窗口仅在第二次单击单词后出现。 useEffect()
中的 console.log()
表示每次单击新词时都会发出 API 请求。要使弹出框出现,必须单击两次相同的单词。要是一键搞定就更好了
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
export default function App() {
const [text, setText] = useState(
"He looked at her and saw her eyes luminous with pity."
);
const [selectedWord, setSelectedWord] = useState("luminous");
const [apiData, setApiData] = useState([
{
word: "",
phonetics: [{ text: "" }],
meanings: [{ definitions: [{ definition: "", example: "" }] }]
}
]);
const words = text.split(/ /g);
useEffect(() => {
var url = "https://api.dictionaryapi.dev/api/v2/entries/en/" + selectedWord;
axios
.get(url)
.then(response => {
setApiData(response.data)
console.log("api call")
})
.catch(function (error) {
if (error) {
console.log("Error", error.message);
}
});
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const popover = (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</Popover.Body>
</Popover>
);
return (
<Alert>
{words.map((w) => (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={popover}
>
<span onClick={() => clickCallback(w)}> {w}</span>
</OverlayTrigger>
))}
</Alert>
);
}
更新:
更改了 apiData
初始化和 <Popover.Body>
组件。这并没有解决问题。
const [apiData, setApiData] = useState(null)
<Popover.Body>
{
apiData ?
<div>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</div> :
<div>Loading...</div>
}
</Popover.Body>
问题
这是我认为正在发生的事情:
- 组件渲染
- 开始获取“luminous”的定义。
- “luminous”的定义已完成获取。它调用
setApiData(data)
. - 组件重新渲染
- 如果您点击“luminous”,弹出器会立即显示,这是因为弹出器的数据已准备就绪,setSelectedWord("luminous") 什么都不做。
- 如果您单击另一个词,例如“可惜”,弹出器会尝试显示,但
setSelectedWord("pity")
会导致组件开始重新呈现。 - 组件重新渲染
- 开始获取“可惜”的定义。
- “可惜”的定义已完成获取。它调用
setApiData(data)
. - 组件重新渲染
- 如果单击“pity”,弹出器会立即显示,这是因为弹出器的数据已准备就绪,setSelectedWord(“pity”) 什么都不做。
选择另一个词将一遍又一遍地重复此过程。
要解决此问题,您需要先使用 show
属性 在渲染后显示弹出窗口(如果它与 selected 词匹配)。但是如果这个词出现多次怎么办?如果您对“她”一词执行此操作,它会在多个位置显示弹出窗口。因此,您不必为每个词分配一个唯一的 ID 并与之进行比较,而不是与每个词进行比较。
修复组件
要为单词分配一个在渲染之间不会改变的 ID,我们需要在组件顶部为它们分配 ID 并将它们存储在一个数组中。为了使这个“更简单”,我们可以将该逻辑抽象为组件外部的可重用函数:
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading punctuation
.replace(/[.,!?"]+$/, "") // remove trailing punctuation
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
// we can use this to prevent unnecessary rerenders
words.text = text;
// return the array-object
return words;
}
在组件中,我们需要设置状态变量来保存单词数组。通过将回调传递给 useState
,React 只会在第一次渲染时执行它,并跳过在重新渲染时调用它。
// set up state array of words that have their own UUIDs
// note: we don't want to call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
现在我们有 words
和 _setWords
,我们可以从中提取 text
值:
// extract text from words array for convenience
// probably not needed
const text = words.text;
接下来,我们可以创建自己的 setText
回调。这可能更简单,但我想确保我们支持 React 的变异更新语法 (setText(oldValue => newValue)
):
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
接下来,我们需要设置当前selected 的单词。定义可用后,将显示该词的弹出窗口。
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
如果您不想默认显示某个词,请使用:
const [selectedWordObj, setSelectedWordObj] = useState(); // nothing selected by default
要修复 API 调用,我们需要使用“使用异步效果”模式(有一些库可以简化这一点):
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
上面的代码块确保在不再需要时避免调用 setApiData
方法。它还使用 status
属性 来跟踪它的进度,以便您可以正确呈现结果。
现在定义一个显示加载消息的弹出窗口:
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
我们可以将加载弹出窗口与 apiData
混合以获得显示定义的弹出窗口。如果我们仍在加载定义,请使用加载定义。如果我们有错误,请显示错误。如果它正确完成,渲染出定义。为了使这更容易,我们可以将此逻辑放在组件外部的函数中,如下所示:
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
我们在组件中调用这个函数使用:
const selectedWordPopover = getPopover(apiData, loadingPopover);
最后,我们渲染出文字。因为我们要渲染一个数组,所以我们需要使用 key
属性 来设置每个单词的 ID。我们还需要 select 被点击的词 - 即使有多个相同的词,我们只想要被点击的词。为此,我们也会检查它的 Id。如果我们点击一个特定的词,我们需要确保我们点击的是 selected。我们还需要用标点符号渲染出原始单词。这一切都在这个块中完成:
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
完整代码
将所有这些放在一起得出:
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading characters
.replace(/[.,!?"]+$/, "") // remove trailing characters
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
words.text = text;
// return the array
return words;
}
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
export default function App() {
// set up state array of words that have their own UUIDs
// note: don't call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
// extract text from words array for convenience
const text = words.text;
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
const selectedWordPopover = getPopover(apiData, loadingPopover);
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
}
注意:您可以通过缓存 API 调用的结果来改进这一点。