Speichern der letzten Suchaktionen
Speichern der letzten Suchaktionen
Aufgabe: Merke dir die letzten fünf Suchbegriffe, und zeige Schaltflächen an, mit deren Hilfe du schnell zwischen den Suchvorgängen wechselst. Wenn du auf eine der Schaltfläche klickst, werden die Suchergebnisse für den gespeicherten Begriff erneut abgerufen.
Optionale Hinweise:
- Verwende für dieses Feature keinen neuen Status. Nutze stattdessen den Status
url
und die StatusaktualisierungsfunktionsetUrl
, um Suchergebnise von der API abzurufen. Passe diese für die Verwendung mehrererurls
an und lege mitsetUrls
mehrereurls
fest. Verwende die letzte URL vonurls
zum Abrufen der Daten, und die letzten fünf URLs vonurls
zum Anzeigen der Schaltflächen.
Zuerst werden wir an allen Stellen den Status url
in urls
und die Statusaktualisierungsfunktionen setUrl
in setUrls
umwandeln. Anstatt den Status mit einer url
als Zeichenfolge zu initialisieren, konvertierst du ihn zu einem Array mit der anfänglichen url
als einzigem Eintrag:
const App = () => {
...
# start-insert
const [urls, setUrls] = React.useState([
# end-insert
`${API_ENDPOINT}${searchTerm}`,
# start-insert
]);
# end-insert
...
};
Verwende anstelle des aktuellen url
-Status zum Abrufen von Daten den letzten url
-Eintrag aus dem url
-Array. Wenn der Liste der urls
eine andere url
hinzugefügt wird, wird diese zum Abrufen der Daten verwendet:
const App = () => {
...
const handleFetchStories = React.useCallback(async () => {
dispatchStories({ type: 'STORIES_FETCH_INIT' });
try {
# start-insert
const lastUrl = urls[urls.length - 1];
const result = await axios.get(lastUrl);
# end-insert
dispatchStories({
type: 'STORIES_FETCH_SUCCESS',
payload: result.data.hits,
});
} catch {
dispatchStories({ type: 'STORIES_FETCH_FAILURE' });
}
# start-insert
}, [urls]);
# end-insert
...
};
Anstatt die url
als Status mit der Statusaktualisierungsfunktion zu speichern, verknüpfe die neue url
mit den vorherigen urls
in einem Array. Dies ist der neue Status:
const App = () => {
...
const handleSearchSubmit = event => {
# start-insert
const url = `${API_ENDPOINT}${searchTerm}`;
setUrls(urls.concat(url));
# end-insert
event.preventDefault();
};
...
};
Bei jeder Suche wird eine andere URL in unserem Status urls
gespeichert. Rendere als Nächstes eine Schaltfläche für jede der letzten fünf URLs. Wir werden hierfür einen neuen universellen Handler hinzufügen. Jede übergibt eine url
mit einem spezifischeren Inline-Handler:
# start-insert
const getLastSearches = urls => urls.slice(-5);
# end-insert
...
const App = () => {
...
# start-insert
const handleLastSearch = url => {
// do something
};
# end-insert
# start-insert
const lastSearches = getLastSearches(urls);
# end-insert
return (
<div>
<h1>My Hacker Stories</h1>
<SearchForm ... />
# start-insert
{lastSearches.map(url => (
<button
key={url}
type="button"
onClick={() => handleLastSearch(url)}
>
{url}
</button>
))}
# end-insert
...
</div>
);
};
Anstatt die gesamte URL der letzten Suche in der Schaltfläche als Schaltflächentext anzuzeigen, wird als Nächstes nur der Suchbegriff angezeigt, indem der Endpunkt der API durch eine leere Zeichenfolge ersetzt wird:
# start-insert
const extractSearchTerm = url => url.replace(API_ENDPOINT, '');
# end-insert
# start-insert
const getLastSearches = urls =>
urls.slice(-5).map(url => extractSearchTerm(url));
# end-insert
...
const App = () => {
...
const lastSearches = getLastSearches(urls);
return (
<div>
...
{lastSearches.map(searchTerm => (
<button
# start-insert
key={searchTerm}
# end-insert
type="button"
# start-insert
onClick={() => handleLastSearch(searchTerm)}
# end-insert
>
# start-insert
{searchTerm}
# end-insert
</button>
))}
...
</div>
);
};
Die Funktion getLastSearches
gibt jetzt Suchbegriffe anstelle von URLs zurück. Das eigentliche searchTerm
wird statt der url
an den Inline-Handler übergeben. Durch Zuordnen über die Liste der urls
in getLastSearches
extrahieren wir den Suchbegriff für jede url
innerhalb der Zuordnungsmethode des Arrays. Um es prägnanter und knapper zu gestalten, integrieren wir alles wie folgt:
const getLastSearches = urls =>
# start-insert
urls.slice(-5).map(extractSearchTerm);
# end-insert
Jetzt stellen wir Funktionen für den neuen Handler bereit, der von jeder Schaltfläche verwendet wird --- das Klicken auf eine löst eine weitere Suche aus. Da wir den Status urls
zum Abrufen von Daten verwenden und wissen, dass die letzte URL immer hierzu verwendet wird, füge der Liste der urls
eine neue url
hinzu. So löst du eine weitere Suchanforderung aus:
const App = () => {
...
# start-insert
const handleLastSearch = searchTerm => {
const url = `${API_ENDPOINT}${searchTerm}`;
setUrls(urls.concat(url));
};
# end-insert
...
};
Wenn du die Implementierungslogik dieses neuen Handlers mit handleSearchSubmit
vergleichst, siehst du einige allgemeine Funktionen. Extrahiere die Funktionalität in einen neuen Handler und eine neue extrahierte Dienstprogrammfunktion:
# start-insert
const getUrl = searchTerm => `${API_ENDPOINT}${searchTerm}`;
# end-insert
...
const App = () => {
...
const handleSearchSubmit = event => {
# start-insert
handleSearch(searchTerm);
# end-insert
event.preventDefault();
};
const handleLastSearch = searchTerm => {
# start-insert
handleSearch(searchTerm);
# end-insert
};
# start-insert
const handleSearch = searchTerm => {
const url = getUrl(searchTerm);
setUrls(urls.concat(url));
};
# end-insert
...
};
Die neue Dienstprogrammfunktion ist an anderen Stellen in der App-Komponente verwendbar. Wenn du eine solche Funktion extrahierst, überprüfe immer, ob sie von Dritten verwendet wird.
const App = () => {
...
// important: still wraps the returned value in []
# start-insert
const [urls, setUrls] = React.useState([getUrl(searchTerm)]);
# end-insert
...
};
Die Funktionalität ist zwar fertig umgesetzt; sie ist aber leider fehlerhaft. Beispielsweise wenn derselbe Suchbegriff mehrmals genutzt wird, da searchTerm
für jedes Schaltflächenelement als key
-Attribut verwendet wird. Verkette den Schlüssel mit dem index
des zugeordneten Arrays, so ist er spezifischer und der zuvor beschriebene Fehler ist behoben.
const App = () => {
...
return (
<div>
...
# start-insert
{lastSearches.map((searchTerm, index) => (
# end-insert
<button
# start-insert
key={searchTerm + index}
# end-insert
type="button"
onClick={() => handleLastSearch(searchTerm)}
>
{searchTerm}
</button>
))}
...
</div>
);
};
Dies ist keine perfekte Lösung, da index
kein stabiler Schlüssel ist. Insbesondere das Hinzufügen von Elementen ist nicht sicher. Wir verbessern die Anwendung aber nicht weiter, denn für unser Szenario reicht die bisherige Implementierung. Die Funktion macht was man von ihr erwartet. Mir ist es wichtiger, dir UX-Verbesserungen anhand der folgenden Aufgaben vorzuschlagen.
Weitere Aufgaben:
- (1) Zeige für die aktuelle Suche keine Schaltfläche an, erstelle diese nur für die fünf vorhergehenden Suchaktionen. Tipp: Passe die Funktion
getLastSearches
an. - (2) Zeige eine Suchanfrage nicht doppelt an. Erstelle beispielsweise keine verschiedenen Schaltflächen, wenn du zweimal nach “React” suchst. Tipp: Ändere die Funktion
getLastSearches
. - (3) Lege den Eingabefeldwert der SearchForm-Komponente mit dem letzten Suchbegriff fest, wenn du auf eine der Schaltflächen klickst.
Die fünf Schaltflächen werden in der Funktion getLastSearches
erstellt. Dort nehmen wir das Array urls
und geben die neuesten fünf Einträge zurück. Jetzt ändern wir diese Funktion so, dass die letzten sechs --- anstelle von fünf --- zurückgeben werden, wobei wir den aktuellsten Eintrag auslassen. Danach zeigt die Anwendung nur die fünf vorherigen Suchvorgänge als Schaltflächen an.
const getLastSearches = urls =>
urls
# start-insert
.slice(-6)
.slice(0, -1)
# end-insert
.map(extractSearchTerm);
Wenn dieselbe Suche zweimal oder mehrmals hintereinander aufgerufen wird, werden doppelte Schaltflächen angezeigt. Das ist nicht das erwartete Verhalten. Es ist akzeptabel, gleiche Suchen zusammenzufassen. Gruppiere dieselben Suchvorgänge, bevor du das Array in die fünf vorherigen unterteilen:
const getLastSearches = urls =>
urls
# start-insert
.reduce((result, url, index) => {
const searchTerm = extractSearchTerm(url);
if (index === 0) {
return result.concat(searchTerm);
}
const previousSearchTerm = result[result.length - 1];
if (searchTerm === previousSearchTerm) {
return result;
} else {
return result.concat(searchTerm);
}
}, [])
# end-insert
.slice(-6)
.slice(0, -1);
Die Reduktionsfunktion wird mit einem leeren Array result
initialisiert. Die erste Iteration konzentriert sich auf das searchTerm
, das wir aus der url
berechnet haben. Jedes searchTerm
wird mit dem vorhergehenden verglichen. Wenn sich der vorherige Suchbegriff vom aktuellen unterscheidet, verknüpfe searchTerm
mit dem Ergebnis. Wenn die Begriffe identisch sind, gib das Ergebnis zurück ohne sonst etwas zu veranlassen.
Zuletzt wird das Eingabefeld des Suchformulars mit dem neuen searchTerm
verknüpft, wenn auf eine der Suchschaltflächen geklickt wird. Wir verwenden hierzu die Statusaktualisierungsfunktion.
const App = () => {
...
const handleLastSearch = searchTerm => {
# start-insert
setSearchTerm(searchTerm);
# end-insert
handleSearch(searchTerm);
};
...
};
Überarbeite zuletzt den in diesem Kapitel neu erstellten Code als eigenständige Komponente, damit die App übersichtlich bleibt:
const App = () => {
...
const lastSearches = getLastSearches(urls);
return (
<div>
...
# start-insert
<LastSearches
lastSearches={lastSearches}
onLastSearch={handleLastSearch}
/>
# end-insert
...
</div>
);
};
# start-insert
const LastSearches = ({ lastSearches, onLastSearch }) => (
<>
{lastSearches.map((searchTerm, index) => (
<button
key={searchTerm + index}
type="button"
onClick={() => onLastSearch(searchTerm)}
>
{searchTerm}
</button>
))}
</>
);
# end-insert
Die Umsetzung dieser Funktion war komplex. Es waren viele grundlegende React- und JavaScript-Kenntnisse erforderlich, um alles nachzuvollziehen, was ich hier gezeigt habe. Sorge dich nicht, wenn du nicht alles sofort verstanden hast. Gegebenenfalls hast du sogar einen anderen Weg gefunden, um die Aufgabe zu lösen, und dieser ist intuitiver als der, den ich hier gezeigt habe.
Übungen:
- Begutachte den Quellcode dieses Abschnitts.
- Reflektiere die Änderungen gegenüber dem letzten Kapitel.
- Reflektiere die Änderungen gegenüber dem letzten Kapitel.