Wie sind Haken nützlich?
Bevor ich Ihnen sage, was und warum ich enttäuscht war, möchte ich offiziell erklären, dass ich tatsächlich ein Fan von Haken bin .
Ich höre oft, dass Hooks erstellt werden, um Klassenkomponenten zu ersetzen. Leider wirbt der Beitrag "Einführung in Hooks" auf der offiziellen React-Website für diese Innovation, ehrlich gesagt, unglücklich:
Hooks sind eine Innovation in React 16.8, mit der Sie Status- und andere React-Funktionen verwenden können, ohne Klassen zu schreiben.
Die Nachricht, die ich hier sehe, lautet ungefähr so: "Klassen sind nicht cool!". Nicht genug, um Sie zu motivieren, Haken zu verwenden. Mit Hooks können wir meiner Meinung nach Probleme mit Querschnittsfunktionen eleganter lösen als mit früheren Ansätzen: Mixins , Komponenten höherer Ordnung und Rendering-Requisiten .
Protokollierungs- und Authentifizierungsfunktionen sind allen Komponenten gemeinsam, und mit Hooks können Sie solche wiederverwendbaren Funktionen an Komponenten anhängen.
Was ist los mit Klassenkomponenten?
Es gibt eine unverständliche Schönheit in einer zustandslosen Komponente (d. H. Einer Komponente ohne internen Zustand), die Requisiten als Eingabe verwendet und ein React-Element zurückgibt. Dies ist eine reine Funktion, dh eine Funktion ohne Nebenwirkungen.
export const Heading: React.FC<HeadingProps> = ({ level, className, tabIndex, children, ...rest }) => {
const Tag = `h${level}` as Taggable;
return (
<Tag className={cs(className)} {...rest} tabIndex={tabIndex}>
{children}
</Tag>
);
};
Leider schränkt das Fehlen von Nebenwirkungen die Verwendung zustandsloser Komponenten ein. Schließlich ist eine staatliche Manipulation unerlässlich. In React bedeutet dies, dass Class Beans, die Stateful sind, Nebenwirkungen hinzugefügt werden. Sie werden auch Containerkomponenten genannt. Sie führen Nebenwirkungen aus und geben Requisiten an reine staatenlose Funktionen weiter.
Es gibt einige bekannte Probleme mit klassenbasierten Lebenszyklusereignissen. Viele Menschen sind unglücklich, dass sie die Logik in den Methoden
componentDidMountund wiederholen müssen componentDidUpdate.
async componentDidMount() {
const response = await get(`/users`);
this.setState({ users: response.data });
};
async componentDidUpdate(prevProps) {
if (prevProps.resource !== this.props.resource) {
const response = await get(`/users`);
this.setState({ users: response.data });
}
};
Früher oder später sind alle Entwickler mit diesem Problem konfrontiert.
Dieser Nebenwirkungscode kann mit einem Effekt-Hook in einer einzelnen Komponente ausgeführt werden.
const UsersContainer: React.FC = () => {
const [ users, setUsers ] = useState([]);
const [ showDetails, setShowDetails ] = useState(false);
const fetchUsers = async () => {
const response = await get('/users');
setUsers(response.data);
};
useEffect( () => {
fetchUsers(users)
}, [ users ]
);
// etc.
Hook
useEffectmacht das Leben viel einfacher, aber es beraubt sich der reinen Funktion - der zustandslosen Komponente -, die wir zuvor verwendet haben. Dies ist das erste, was mich enttäuscht hat.
Ein weiteres JavaScript-Paradigma zu wissen
Ich bin 49 Jahre alt und ein Fan von React. Nach der Entwicklung einer Glut- App mit diesem Beobachter und dem berechneten Eigenschaftswahnsinn werde ich immer ein warmes Gefühl für den unidirektionalen Datenfluss haben.
Das Problem mit Hooks
useEffectund dergleichen ist, dass sie nirgendwo anders in der JavaScript-Landschaft verwendet werden. Er ist ungewöhnlich und im Allgemeinen komisch. Ich sehe nur einen Weg, es zu zähmen - diesen Haken zu üben und zu leiden. Und keine Beispiele für Zähler werden mich dazu bringen, die ganze Nacht selbstlos zu codieren. Ich bin freiberuflich tätig und benutze nicht nur React, sondern auch andere Bibliotheken. Ich bin bereits müdeFolgen Sie all diesen Innovationen. Sobald ich denke, dass ich ein Eslint-Plugin installieren muss, das mich auf den richtigen Weg bringt, beginnt mich dieses neue Paradigma zu belasten.
Abhängigkeitsarrays sind die Hölle
Der useEffect-Hook kann ein optionales zweites Argument verwenden, das als Abhängigkeitsarray bezeichnet wird, und ermöglicht es Ihnen, den Effekt bei Bedarf zurückzurufen. Um festzustellen, ob eine Änderung aufgetreten ist , vergleicht React die Werte mithilfe der Object.is- Methode miteinander . Wenn sich seit dem letzten Renderzyklus Elemente geändert haben, wird der Effekt auf die neuen Werte angewendet.
Der Vergleich eignet sich hervorragend für den Umgang mit primitiven Datentypen. Wenn jedoch eines der Elemente ein Objekt oder ein Array ist, können Probleme auftreten. Object.is vergleicht Objekte und Arrays anhand von Referenzen, und Sie können nichts dagegen tun. Der benutzerdefinierte Vergleichsalgorithmus kann nicht angewendet werden.
Das Validieren von Objekten anhand von Referenzen ist ein bekannter Stolperstein. Schauen wir uns eine vereinfachte Version eines Problems an, auf das ich kürzlich gestoßen bin.
const useFetch = (config: ApiOptions) => {
const [data, setData] = useState(null);
useEffect(() => {
const { url, skip, take } = config;
const resource = `${url}?$skip=${skip}&take=${take}`;
axios({ url: resource }).then(response => setData(response.data));
}, [config]); // <-- will fetch on each render
return data;
};
const App: React.FC = () => {
const data = useFetch({ url: "/users", take: 10, skip: 0 });
return <div>{data.map(d => <div>{d})}</div>;
};
In Zeile 14
useFetch wird für jedes Rendering ein neues Objekt an die Funktion übergeben , es sei denn, wir machen es so, dass jedes Mal dasselbe Objekt verwendet wird. In diesem Szenario würden Sie die Felder des Objekts überprüfen, nicht den Verweis darauf.
Ich verstehe, warum React keine tiefen Objektvergleiche wie diese Lösung durchführt . Daher müssen Sie den Hook vorsichtig verwenden, da sonst ernsthafte Probleme mit der Anwendungsleistung auftreten können. Ich frage mich ständig, was ich dagegen tun kann, und habe bereits mehrere Optionen gefunden . Für dynamischere Objekte müssen Sie nach mehr Problemumgehungen suchen.
Es gibt ein Eslint-Plugin , um Fehler automatisch zu behebenwährend der Codeüberprüfung gefunden. Es ist für jeden Texteditor geeignet. Um ehrlich zu sein, ärgere ich mich über all diese neuen Funktionen, für deren Test ein externes Plugin installiert werden muss.
Die bloße Existenz von Plugins wie use-deep-object-compare und use-memo-one deutet darauf hin, dass es wirklich ein Problem gibt (oder zumindest ein Durcheinander).
Die Reaktion hängt von der Reihenfolge ab, in der die Hooks aufgerufen werden
Die frühesten benutzerdefinierten Hooks waren mehrere Implementierungen einer Funktion
useFetchzum Senden von Anforderungen an eine Remote-API. Die meisten von ihnen lösen nicht das Problem, Remote-API-Anforderungen von einem Ereignishandler zu stellen, da Hooks nur am Anfang einer Funktionskomponente verwendet werden können.
Was aber, wenn die Daten Links zu paginierten Websites enthalten und wir den Effekt erneut ausführen möchten, wenn der Benutzer auf den Link klickt? Hier ist ein einfacher Anwendungsfall
useFetch:
const useFetch = (config: ApiOptions): [User[], boolean] => {
const [data, setData] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const { skip, take } = config;
api({ skip, take }).then(response => {
setData(response);
setLoading(false);
});
}, [config]);
return [data, loading];
};
const App: React.FC = () => {
const [currentPage, setCurrentPage] = useState<ApiOptions>({
take: 10,
skip: 0
});
const [users, loading] = useFetch(currentPage);
if (loading) {
return <div>loading....</div>;
}
return (
<>
{users.map((u: User) => (
<div>{u.name}</div>
))}
<ul>
{[...Array(4).keys()].map((n: number) => (
<li>
<button onClick={() => console.log(' ?')}>{n + 1}</button>
</li>
))}
</ul>
</>
);
};
In Zeile 23 wird der Hook
useFetchbeim ersten Rendern einmal aufgerufen. In den Zeilen 35–38 rendern wir die Paginierungsschaltflächen. Aber wie würden wir den Hook useFetchvom Event-Handler für diese Buttons aufrufen ?
In den Hook-Regeln heißt es eindeutig:
Verwenden Sie keine Hooks in Schleifen, Bedingungen oder verschachtelten Funktionen, sondern verwenden Sie Hooks immer nur auf der obersten Ebene der React-Funktionen.
Hooks werden bei jedem Rendern der Komponente in derselben Reihenfolge aufgerufen. Dafür gibt es mehrere Gründe, die Sie in diesem ausgezeichneten Beitrag kennenlernen können.
Du kannst das nicht machen:
<button onClick={() => useFetch({ skip: n + 1 * 10, take: 10 })}>
{n + 1}
</button>
Das Aufrufen eines
useFetchHooks von einem Ereignishandler verstößt gegen die Regeln von Hooks, da sich die Reihenfolge ihres Aufrufs mit jedem Rendern ändert.
Rückgabe einer ausführbaren Funktion von einem Hook
Ich kenne zwei Lösungen für dieses Problem. Sie verfolgen den gleichen Ansatz und ich mag beide. Das React-Async-Hook- Plugin gibt eine Funktion vom Hook zurück
execute:
import { useAsyncCallback } from 'react-async-hook';
const AppButton = ({ onClick, children }) => {
const asyncOnClick = useAsyncCallback(onClick);
return (
<button onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}>
{asyncOnClick.loading ? '...' : children}
</button>
);
};
const CreateTodoButton = () => (
<AppButton
onClick={async () => {
await createTodoAPI('new todo text');
}}
>
Create Todo
</AppButton>
);
Durch Aufrufen des Hooks
useAsyncCallbackwird ein Objekt mit den erwarteten Eigenschaften für Laden, Fehler und Ergebnisse sowie eine Funktion zurückgegeben execute, die von einem Ereignishandler aufgerufen werden kann.
React-hooks-async ist ein Plugin mit einem ähnlichen Ansatz. Es verwendet eine Funktion
useAsyncTask.
Hier ist ein vollständiges Beispiel mit einer vereinfachten Version
useAsyncTask:
const createTask = (func, forceUpdateRef) => {
const task = {
start: async (...args) => {
task.loading = true;
task.result = null;
forceUpdateRef.current(func);
try {
task.result = await func(...args);
} catch (e) {
task.error = e;
}
task.loading = false;
forceUpdateRef.current(func);
},
loading: false,
result: null,
error: undefined
};
return task;
};
export const useAsyncTask = (func) => {
const forceUpdate = useForceUpdate();
const forceUpdateRef = useRef(forceUpdate);
const task = useMemo(() => createTask(func, forceUpdateRef), [func]);
useEffect(() => {
forceUpdateRef.current = f => {
if (f === func) {
forceUpdate({});
}
};
const cleanup = () => {
forceUpdateRef.current = () => null;
};
return cleanup;
}, [func, forceUpdate]);
return useMemo(
() => ({
start: task.start,
loading: task.loading,
error: task.error,
result: task.result
}),
[task.start, task.loading, task.error, task.result]
);
};
Die Funktion createTask gibt ein Aufgabenobjekt in der folgenden Form zurück.
interface Task {
start: (...args: any[]) => Promise<void>;
loading: boolean;
result: null;
error: undefined;
}
Der Job hat Staaten
, und , was wir erwarten. Die Funktion gibt aber auch eine Funktion zurück start, die später aufgerufen werden kann. Der mit der Funktion erstellte Job createTaskwirkt sich nicht auf die Aktualisierung aus. Das Update wird durch Funktionen forceUpdateund forceUpdateRefin ausgelöst useAsyncTask.
Wir haben jetzt eine Funktion
start, die von einem Ereignishandler oder von einem anderen Code aufgerufen werden kann, nicht unbedingt vom Beginn einer Funktionskomponente an.
Wir haben jedoch die Möglichkeit verloren, den Hook beim ersten Durchlauf der Funktionskomponente aufzurufen. Es ist gut, dass das React-Hooks-Async- Plugin eine Funktion enthält
useAsyncRun- das macht die Sache einfacher:
export const useAsyncRun = (
asyncTask: ReturnType<typeof useAsyncTask>,
...args: any[]
) => {
const { start } = asyncTask;
useEffect(() => {
start(...args);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [asyncTask.start, ...args]);
useEffect(() => {
const cleanup = () => {
//
};
return cleanup;
});
};
Die Funktion
startwird immer dann ausgeführt, wenn sich eines der Argumente ändert args. Jetzt sieht der Code mit Hooks folgendermaßen aus:
const App: React.FC = () => {
const asyncTask = useFetch(initialPage);
useAsyncRun(asyncTask);
const { start, loading, result: users } = asyncTask;
if (loading) {
return <div>loading....</div>;
}
return (
<>
{(users || []).map((u: User) => (
<div>{u.name}</div>
))}
<ul>
{[...Array(4).keys()].map((n: number) => (
<li key={n}>
<button onClick={() => start({ skip: 10 * n, take: 10 })}>
{n + 1}
</button>
</li>
))}
</ul>
</>
);
};
Gemäß den Hook-Regeln verwenden wir einen Hook
useFetcham Anfang einer Funktionskomponente. Die Funktion useAsyncRunruft die API ganz am Anfang auf, und startwir verwenden die Funktion im Handler onClickfür die Paginierungsschaltflächen.
Jetzt kann der Haken
useFetchfür den vorgesehenen Zweck verwendet werden, aber leider müssen Sie den falschen Weg gehen. Wir verwenden auch einen Verschluss, der mir, wie ich zugeben muss, ein wenig Angst macht.
Hooks in Anwendungsprogrammen steuern
In Anwendungen sollte alles wie vorgesehen funktionieren. Wenn Sie komponentenbezogene Probleme UND Benutzerinteraktionen mit bestimmten Komponenten verfolgen möchten , können Sie LogRocket verwenden .
LogRocket ist eine Art Webanwendungs-Videorecorder, der fast alles aufzeichnet, was auf der Website passiert. Mit dem LogRocket-Plugin für React können Sie Benutzersitzungen finden, in denen der Benutzer auf eine bestimmte Komponente Ihrer Anwendung geklickt hat. Sie werden verstehen, wie Benutzer mit Komponenten interagieren und warum einige Komponenten nichts rendern.
LogRocket zeichnet alle Aktionen und Status aus dem Redux-Speicher auf. Es handelt sich um eine Reihe von Tools für Ihre Anwendung, mit denen Sie Anforderungen / Antworten mit Headern und Textkörpern aufzeichnen können. Sie schreiben HTML und CSS auf die Seite und bieten Pixel für Pixel-Rendering auch für die komplexesten Anwendungen mit nur einer Seite.
LogRocket bietet einen modernen Ansatz zum Debuggen von React-Anwendungen - probieren Sie es kostenlos aus .
Fazit
Ich denke, das Beispiel c
useFetcherklärt am besten, warum ich mit Haken frustriert bin.
Das gewünschte Ergebnis zu erzielen, erwies sich als nicht so einfach wie erwartet, aber ich verstehe immer noch, warum es so wichtig ist, Haken in einer bestimmten Reihenfolge zu verwenden. Leider sind unsere Funktionen stark eingeschränkt, da Hooks nur am Anfang einer Funktionskomponente aufgerufen werden können und wir weiter nach Problemumgehungen suchen müssen. Die Lösung ist
useFetchziemlich kompliziert. Darüber hinaus können Sie bei der Verwendung von Haken nicht auf Verschlüsse verzichten. Verschlüsse sind ständige Überraschungen, die viele Narben in meiner Seele hinterlassen haben.
Verschlüsse (wie die an
useEffectund übergebenenuseCallback) kann ältere Versionen von Requisiten und Statuswerten abrufen. Dies ist beispielsweise der Fall, wenn aus irgendeinem Grund eine der erfassten Variablen im Eingabearray fehlt - es können Schwierigkeiten auftreten.
Der veraltete Zustand, der nach dem Ausführen von Code in einem Abschluss auftritt, ist eines der Probleme, die der Hook-Linter lösen soll. Bei Stack Overflow gibt es viele Fragen zu veralteten Hooks
useEffectund dergleichen. Ich habe Funktionen in useCallbackdiese und jene Abhängigkeitsarrays eingewickelt und verdreht, um das Problem des veralteten Zustands oder der unendlichen Wiederholung zu beseitigen. Es kann nicht anders sein, aber es ist ein bisschen nervig. Dies ist ein echtes Problem, das Sie lösen müssen, um Ihren Wert zu beweisen.
Am Anfang dieses Beitrags habe ich gesagt, dass ich generell Haken mag. Aber sie scheinen sehr kompliziert zu sein. In der aktuellen JavaScript-Landschaft gibt es nichts Vergleichbares. Das Aufrufen von Hooks jedes Mal, wenn eine Funktionskomponente gerendert wird, führt zu Problemen, die Mixins nicht haben. Die Notwendigkeit, dass ein Linter dieses Muster verwendet, ist nicht sehr glaubwürdig, und Verschlüsse sind ein Problem.
Ich hoffe, ich habe diesen Ansatz einfach falsch verstanden. Wenn ja, schreiben Sie darüber in den Kommentaren.
Weiterlesen: