问题
这是我认为正在发生的事情:
- 组件渲染
- 开始获取“发光”的定义。
- “发光”的定义已完成获取。它调用
setApiData(data)。
- 组件重新呈现
- 如果单击“luminous”,popper 会立即显示,这是因为 popper 的数据已准备好使用,而 setSelectedWord("luminous") 什么都不做。
- 如果您单击另一个词,例如“pity”,弹出器会尝试显示,但
setSelectedWord("pity") 会导致组件开始重新渲染。
- 组件重新呈现
- 开始获取“pity”的定义。
- “怜悯”的定义已完成获取。它调用
setApiData(data)。
- 组件重新呈现
- 如果单击“pity”,popper 会立即显示,这是因为 popper 的数据已准备好使用,而 setSelectedWord("pity") 什么也不做。
选择另一个单词会一遍又一遍地重复这个过程。
要解决此问题,您需要首先使用show 属性在渲染后显示弹出框(如果它与所选单词匹配)。但是如果这个词出现多次怎么办?如果您对“她”一词执行此操作,则会在多个位置显示弹出框。因此,您不必对每个单词进行比较,而是必须为每个单词分配一个唯一的 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
}
}
接下来,我们需要设置当前选中的单词。一旦定义可用,就会显示该单词的弹出框。
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。我们还需要选择被点击的词——即使有多个相同的词,我们也只想要被点击的那个。为此,我们也会检查它的 ID。如果我们点击一个特定的词,我们需要确保我们点击的那个被选中。我们还需要渲染出带有标点符号的原始单词。这一切都在这个块中完成:
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 调用的结果来改进这一点。