Reagieren + IndexDb + Auto-Update = fast AsyncRedux

In diesem Artikel werde ich Ihnen Schritt für Schritt erklären, wie Sie IndexDB (eine Datenbank, die in jeden modernen Browser integriert ist) für die Verwendung in in ReactJS geschriebenen Projekten vorbereiten. Infolgedessen können Sie die Daten aus IndexDB so bequem verwenden, als wären sie im Redux Store Ihrer Anwendung.

IndexDB ist ein dokumentenorientiertes DBMS, ein praktisches Tool zum temporären Speichern einer relativ kleinen Menge (Einheiten und Dutzende Megabyte) strukturierter Daten auf der Browserseite. Die Standardaufgabe, für die ich IndexDB verwenden muss, umfasst das Zwischenspeichern von Daten von Unternehmensverzeichnissen auf der Clientseite (Namen von Ländern, Städten, Währungen nach Code usw.). Nachdem Sie sie auf die Clientseite kopiert haben, können Sie nur gelegentlich Updates aus diesen Verzeichnissen vom Server herunterladen (oder das Ganze - sie sind klein) und dies nicht jedes Mal, wenn Sie das Browserfenster öffnen.

Es gibt nicht standardmäßige, sehr kontroverse, aber funktionierende Methoden zur Verwendung von IndexDB:

  • Zwischenspeichern von Daten zu allen Geschäftsobjekten, damit auf der Browserseite die umfangreichen Sortier- und Filterfunktionen genutzt werden können
  • Speichern des Anwendungsstatus in IndexDB anstelle von Redux Store

Drei wesentliche Unterschiede zwischen IndexDB und Redux Store sind uns wichtig:

  1. IndexDB ist ein externer Speicher, der beim Verlassen der Seite nicht gelöscht wird. Darüber hinaus gilt dies auch für mehrere geöffnete Registerkarten (was manchmal zu etwas unerwartetem Verhalten führt).
  2. IndexDB ist ein vollständig asynchrones DBMS. Alle Operationen - Öffnen, Lesen, Schreiben, Suchen - asynchron.
  3. IndexDB kann nicht (auf triviale Weise) in JSON gespeichert werden und verwendet Brain-Tricking-Techniken von Redux, um Snapshots zu erstellen, das Debuggen zu vereinfachen und eine Reise in die Vergangenheit zu unternehmen.

Schritt 0: Aufgabenliste


Bereits ein klassisches Beispiel mit einer Liste von Aufgaben. Variante mit Zustandsspeicher im Zustand der aktuellen und einzigen Komponente

Implementierung einer Aufgabenlistenkomponente, die die Liste in einem Komponentenstatus speichert
import React, { PureComponent } from 'react'; import Button from 'react-bootstrap/Button'; import counter from 'common/counter'; import Form from 'react-bootstrap/Form'; import Table from 'react-bootstrap/Table'; export default class Step0 extends PureComponent { constructor() { super( ...arguments ); this.state = { newTaskText: '', tasks: [ { id: counter(), text: 'Sample task' }, ], }; this.handleAdd = () => { this.setState( state => ( { tasks: [ ...state.tasks, { id: counter(), text: state.newTaskText } ], newTaskText: '', } ) ); }; this.handleDeleteF = idToDelete => () => this.setState( state => ( { tasks: state.tasks.filter( ( { id } ) => id !== idToDelete ), } ) ); this.handleNewTaskTextChange = ( { target: { value } } ) => this.setState( { newTaskText: value || '', } ); } render() { return <Table bordered hover striped> <thead><tr> <th>#</th><th>Text</th><th /> </tr></thead> <tbody> { this.state.tasks.map( task => <tr key={task.id}> <td>{task.id}</td> <td>{task.text}</td> <td><Button onClick={this.handleDeleteF( task.id )} type="button" variant="danger"></Button></td> </tr> ) } <tr key="+1"> <td /> <td><Form.Control onChange={this.handleNewTaskTextChange} placeholder="  " type="text" value={this.state.newTaskText || ''} /></td> <td><Button onClick={this.handleAdd} type="button" variant="primary"></Button></td> </tr> </tbody> </Table>; } } 
( Github-Quellcode )

Bisher sind alle Operationen mit Aufgaben synchron. Wenn das Hinzufügen einer Aufgabe 3 Sekunden dauert, friert der Browser nur 3 Sekunden lang ein. Während wir alles in unserer Erinnerung behalten, können wir natürlich nicht darüber nachdenken. Wenn wir die Verarbeitung mit einem Server oder mit einer lokalen Datenbank einschließen, müssen wir uns auch um die schöne Verarbeitung der Asynchronität kümmern. Blockieren Sie beispielsweise die Arbeit mit einer Tabelle (oder einzelnen Elementen), während Sie Elemente hinzufügen oder entfernen.

Um die Beschreibung der Benutzeroberfläche in Zukunft nicht zu wiederholen, platzieren wir sie in einer separaten TaskList-Komponente, deren einzige Aufgabe den HTML-Code der Aufgabenliste generiert. Gleichzeitig ersetzen wir die üblichen Schaltflächen durch einen speziellen Wrapper um die Bootstrap-Schaltfläche, der die Schaltfläche blockiert, bis der Schaltflächenhandler seine Ausführung abgeschlossen hat, auch wenn dieser Handler eine asynchrone Funktion ist.

Implementieren einer Komponente, die eine Aufgabenliste im Reaktionszustand speichert
 import React, { PureComponent } from 'react'; import counter from 'common/counter'; import TaskList from '../common/TaskList'; export default class Step01 extends PureComponent { constructor() { super( ...arguments ); this.state = { tasks: [ { id: counter(), text: 'Sample task' }, ] }; this.handleAdd = newTaskText => { this.setState( state => ( { tasks: [ ...state.tasks, { id: counter(), text: newTaskText } ], } ) ); }; this.handleDelete = idToDelete => this.setState( state => ( { tasks: state.tasks.filter( ( { id } ) => id !== idToDelete ), } ) ); } render() { return <> <h1>      </h1> <h2>       </h2> <TaskList onAdd={this.handleAdd} onDelete={this.handleDelete} tasks={this.state.tasks} /> </>; } } 
( Github-Quellcode )

Implementierung einer Komponente, die eine Liste von Aufgaben anzeigt und ein Formular zum Hinzufügen einer neuen enthält
 import React, { PureComponent } from 'react'; import Button from './AutoDisableButtonWithSpinner'; import Form from 'react-bootstrap/Form'; import Table from 'react-bootstrap/Table'; export default class TaskList extends PureComponent { constructor() { super( ...arguments ); this.state = { newTaskAdding: false, newTaskText: '', }; this.handleAdd = async() => { this.setState( { newTaskAdding: true } ); try { //   ,     await this.props.onAdd( this.state.newTaskText ); this.setState( { newTaskText: '' } ); } finally { this.setState( { newTaskAdding: false } ); } }; this.handleDeleteF = idToDelete => async() => await this.props.onDelete( idToDelete ); this.handleNewTaskTextChange = ( { target: { value } } ) => this.setState( { newTaskText: value || '', } ); } render() { return <Table bordered hover striped> <thead><tr> <th>#</th><th>Text</th><th /> </tr></thead> <tbody> { this.props.tasks.map( task => <tr key={task.id}> <td>{task.id}</td> <td>{task.text}</td> <td><Button onClick={this.handleDeleteF( task.id )} type="button" variant="danger"></Button></td> </tr> ) } <tr key="+1"> <td /> <td><Form.Control disabled={this.state.newTaskAdding} onChange={this.handleNewTaskTextChange} placeholder="  " type="text" value={this.state.newTaskText || ''} /></td> <td><Button onClick={this.handleAdd} type="button" variant="primary"></Button></td> </tr> </tbody> </Table>; } } 
( Github-Quellcode )

Bereits im Beispielcode sehen Sie die Schlüsselwörter async / await. Async / await-Konstrukte können die Menge an Code, die mit Promises funktioniert, erheblich reduzieren. Mit dem Schlüsselwort await können Sie auf eine Antwort einer Funktion warten, die Promise zurückgibt, als wäre es eine reguläre Funktion (anstatt auf ein Ergebnis in then () zu warten). Natürlich wird eine asynchrone Funktion nicht auf magische Weise zu einer synchronen Funktion, und beispielsweise wird der Ausführungsthread unterbrochen, wenn das Warten verwendet wird. Aber dann wird der Code prägnanter und verständlicher und das Warten kann sowohl in Schleifen als auch in try / catch / finally-Konstrukten verwendet werden.

Beispielsweise ruft TaskList nicht nur den Handler this.props.onAdd , sondern verwendet auch das Schlüsselwort await . In diesem Fall setzt die TaskList Komponente die handleAdd Methode einfach auf die übliche Weise fort, wenn der Handler eine normale Funktion ist, die nichts TaskList oder einen anderen Wert als Promise TaskList . Wenn der Handler jedoch Promise zurückgibt (auch wenn der Handler als asynchrone Funktion deklariert ist), TaskList die TaskList bis der Handler die Ausführung beendet hat, und setzt erst dann die Werte der newTaskText newTaskAdding und newTaskText .

Schritt 1: Fügen Sie IndexDB zur Reaktionskomponente hinzu


Um unsere Arbeit zu vereinfachen, werden wir zunächst eine einfache Komponente schreiben, die Promise-Methoden implementiert für:

  • Öffnen einer Datenbank zusammen mit trivialer Fehlerbehandlung
  • Suche nach Elementen in der Datenbank
  • Hinzufügen von Elementen zur Datenbank

Der erste ist der „nicht trivialste“ - bis zu 5 Event-Handler. Allerdings keine Raketenwissenschaft:

openDatabasePromise () - öffnet eine Datenbank
 function openDatabasePromise( keyPath ) { return new Promise( ( resolve, reject ) => { const dbOpenRequest = window.indexedDB.open( DB_NAME, '1.0.0' ); dbOpenRequest.onblocked = () => { reject( '    ,    , ' + '      .' ); }; dbOpenRequest.onerror = err => { console.log( 'Unable to open indexedDB ' + DB_NAME ); console.log( err ); reject( '   ,       .' + ( err.message ? ' : ' + err.message : '' ) ); }; dbOpenRequest.onupgradeneeded = event => { const db = event.target.result; try { db.deleteObjectStore( OBJECT_STORE_NAME ); } catch ( err ) { console.log( err ); } db.createObjectStore( OBJECT_STORE_NAME, { keyPath } ); }; dbOpenRequest.onsuccess = () => { console.info( 'Successfully open indexedDB connection to ' + DB_NAME ); resolve( dbOpenRequest.result ); }; dbOpenRequest.onerror = reject; } ); } 

getAllPromise / getPromise / putPromise - Wrapper IndexDb ruft in Promise auf
 //    ObjectStore,   IDBRequest //     Promise function wrap( methodName ) { return function() { const [ objectStore, ...etc ] = arguments; return new Promise( ( resolve, reject ) => { const request = objectStore[ methodName ]( ...etc ); request.onsuccess = () => resolve( request.result ); request.onerror = reject; } ); }; } const deletePromise = wrap( 'delete' ); const getAllPromise = wrap( 'getAll' ); const getPromise = wrap( 'get' ); const putPromise = wrap( 'put' ); } 

Alles zu einer IndexedDbRepository-Klasse zusammenfassen

IndexedDbRepository - Wrapper um IDBDatabase
 const DB_NAME = 'objectStore'; const OBJECT_STORE_NAME = 'objectStore'; /* ... */ export default class IndexedDbRepository { /* ... */ constructor( keyPath ) { this.error = null; this.keyPath = keyPath; //     async //      this.openDatabasePromise = this._openDatabase(); } async _openDatabase( keyPath ) { try { this.dbConnection = await openDatabasePromise( keyPath ); } catch ( error ) { this.error = error; throw error; } } async _tx( txMode, callback ) { await this.openDatabasePromise; // await db connection const transaction = this.dbConnection.transaction( [ OBJECT_STORE_NAME ], txMode ); const objectStore = transaction.objectStore( OBJECT_STORE_NAME ); return await callback( objectStore ); } async findAll() { return this._tx( 'readonly', objectStore => getAllPromise( objectStore ) ); } async findById( key ) { return this._tx( 'readonly', objectStore => getPromise( objectStore, key ) ); } async deleteById( key ) { return this._tx( 'readwrite', objectStore => deletePromise( objectStore, key ) ); } async save( item ) { return this._tx( 'readwrite', objectStore => putPromise( objectStore, item ) ); } } 
( Github-Quellcode )

Jetzt können Sie über den Code auf IndexDB zugreifen:

  const db = new IndexedDbRepository( 'id' ); // ,     await db.save( { id: 42, text: 'Task text' } ); const item = await db.findById( 42 ); const items = await db.findAll(); 

Verbinden Sie dieses „Repository“ mit unserer Komponente. Gemäß den Reaktionsregeln sollte ein Aufruf des Servers in der componentDidMount () -Methode erfolgen:

 import IndexedDbRepository from '../common/IndexedDbRepository'; /*...*/ componentDidMount() { this.repository = new IndexedDbRepository( 'id' ); //     this.repository.findAll().then( tasks => this.setState( { tasks } ) ); } 

Theoretisch kann die Funktion componentDidMount() als async deklariert werden, dann können anstelle von then () async / await-Konstrukte verwendet werden. Dennoch ist componentDidMount() nicht "unsere" Funktion, sondern wird von React aufgerufen. Wer weiß, wie sich die React 17.x-Bibliothek als Reaktion auf den Versuch verhält, Promise anstelle von undefined ?

Anstatt es im Konstruktor mit einem leeren Array (oder einem Array mit Testdaten) zu füllen, füllen wir es jetzt mit null. Beim Rendern wird diese Null verarbeitet, da auf die Datenverarbeitung gewartet werden muss. Diejenigen, die dies im Prinzip wünschen, können dies in separate Flaggen setzen, aber warum sollten sie Entitäten produzieren?

  constructor() { super( ...arguments ); this.state = { tasks: null }; /* ... */ } /* ... */ render() { if ( this.state.tasks === null ) return <><Spinner animation="border" aria-hidden="true" as="span" role="status" /><span>  ...</span></>; /* ... */ } 

Die handleAdd / handleDelete handleAdd handleDelete :

  constructor() { /* ... */ this.handleAdd = async( newTaskText ) => { await this.repository.save( { id: counter(), text: newTaskText } ); this.setState( { tasks: null } ); this.setState( { tasks: await this.repository.findAll() } ); }; this.handleDelete = async( idToDelete ) => { await this.repository.deleteById( idToDelete ); this.setState( { tasks: null } ); this.setState( { tasks: await this.repository.findAll() } ); }; } 

In beiden Handlern wenden wir uns zuerst an das Repository, um ein Element hinzuzufügen oder zu entfernen. Anschließend löschen wir den Status der aktuellen Komponente und fordern erneut eine neue Liste vom Repository an. Es scheint, dass die Aufrufe von setState () nacheinander erfolgen. Das Schlüsselwort await in den letzten Zeilen der Handler führt jedoch dazu, dass der zweite Aufruf von setState () erst ausgeführt wird, nachdem das von der findAll () -Methode erhaltene Promise () aufgelöst wurde.

Schritt 2. Hören Sie sich die Änderungen an


Ein großer Fehler im obigen Code besteht darin, dass zunächst das Repository in jeder Komponente verbunden ist. Zweitens, wenn eine Komponente den Inhalt des Repositorys ändert, weiß die andere Komponente nichts davon, bis sie den Status als Ergebnis von Benutzeraktionen erneut liest. Dies ist unpraktisch.

Um dem entgegenzuwirken, werden wir die neue RepositoryListener-Komponente einführen und zwei Dinge tun lassen. Diese Komponente kann zum einen Änderungen im Repository abonnieren. Zweitens benachrichtigt der RepositoryListener die Komponente, die ihn erstellt hat, über diese Änderungen.

Fügen Sie zunächst die Möglichkeit hinzu, Handler in IndexedDbRepository zu registrieren:

 export default class IndexedDbRepository { /*...*/ constructor( keyPath ) { /*...*/ this.listeners = new Set(); this.stamp = 0; /*...*/ } /*...*/ addListener( listener ) { this.listeners.add( listener ); } onChange() { this.stamp++; this.listeners.forEach( listener => listener( this.stamp ) ); } removeListener( listener ) { this.listeners.delete( listener ); } } 

( Github-Quellcode )

Wir werden den Handlern einen Stempel übergeben, der sich bei jedem Aufruf von onChange () ändert. Und wir ändern die _tx-Methode so, dass onChange() für jeden Aufruf in einer Transaktion mit readwrite Modus aufgerufen wird:

  async _tx( txMode, callback ) { await this.openDatabasePromise; // await db connection try { const transaction = this.dbConnection.transaction( [ OBJECT_STORE_NAME ], txMode ); const objectStore = transaction.objectStore( OBJECT_STORE_NAME ); return await callback( objectStore ); } finally { if ( txMode === 'readwrite' ) this.onChange(); // notify listeners } } 

( Github-Quellcode )

Wenn wir then() / catch() , um mit Promise zu arbeiten, müssten wir entweder den Aufruf von onChange() duplizieren oder spezielle Polyfills für Promise () verwenden, die final() . Glücklicherweise können Sie mit async / await dies einfach und ohne unnötigen Code tun.

Die RepositoryListener-Komponente selbst verbindet einen Ereignis-Listener mit den Methoden componentDidMount und componentWillUnmount:

RepositoryListener-Code
 import IndexedDbRepository from './IndexedDbRepository'; import { PureComponent } from 'react'; export default class RepositoryListener extends PureComponent { constructor() { super( ...arguments ); this.prevRepository = null; this.repositoryListener = repositoryStamp => this.props.onChange( repositoryStamp ); } componentDidMount() { this.subscribe(); } componentDidUpdate() { this.subscribe(); } componentWillUnmount() { this.unsubscribe(); } subscribe() { const { repository } = this.props; if ( repository instanceof IndexedDbRepository && this.prevRepository !== repository ) { if ( this.prevRepository !== null ) { this.prevRepository.removeListener( this.repositoryListener ); } this.prevRepository = repository; repository.addListener( this.repositoryListener ); } } unsubscribe( ) { if ( this.prevRepository !== null ) { this.prevRepository.removeListener( this.repositoryListener ); this.prevRepository = null; } } render() { return this.props.children || null; } } 
( Github-Quellcode )

Jetzt werden wir die Verarbeitung von Repository-Änderungen in unsere Hauptkomponente aufnehmen und handleAdd dem DRY-Prinzip den entsprechenden Code aus dem handleDelete handleAdd / handleDelete entfernen:

  constructor() { super( ...arguments ); this.state = { tasks: null }; this.handleAdd = async( newTaskText ) => { await this.repository.save( { id: counter(), text: newTaskText } ); }; this.handleDelete = async( idToDelete ) => { await this.repository.deleteById( idToDelete ); }; this.handleRepositoryChanged = async() => { this.setState( { tasks: null } ); this.setState( { tasks: await this.repository.findAll() } ); }; } componentDidMount() { this.repository = new IndexedDbRepository( 'id' ); this.handleRepositoryChanged(); // initial load } 

( Github-Quellcode )

Und wir fügen einen Aufruf von handleRepositoryChanged aus dem verbundenen RepositoryListener hinzu:

  render() { /* ... */ return <RepositoryListener onChange={this.handleRepoChanged} repository={this.repository}> <TaskList onAdd={this.handleAdd} onDelete={this.handleDelete} tasks={this.state.tasks} /> </RepositoryListener>; } 

( Github-Quellcode )

Schritt 3. Nehmen Sie das Laden der Daten und deren Aktualisierung in einer separaten Komponente heraus


Wir haben eine Komponente geschrieben, die Daten aus dem Repository empfangen und Daten im Repository ändern kann. Wenn Sie sich jedoch ein großes Projekt mit mehr als 100 Komponenten vorstellen, muss jede Komponente, die Daten aus dem Repository anzeigt, Folgendes tun:

  • Stellen Sie sicher, dass das Repository von einem einzelnen Punkt aus korrekt verbunden ist
  • Stellen Sie das anfängliche Laden der Daten in der componentDidMount() -Methode bereit
  • Verbinden Sie die RepositoryListener Komponente, die einen Handleraufruf zum erneuten Laden von Änderungen bereitstellt

Gibt es zu viele doppelte Aktionen? Es scheint nicht so. Und wenn etwas vergessen wird? Mit Copy Paste verloren gehen?

Es wäre großartig, irgendwie sicherzustellen, dass wir einmal die Regel schreiben, die Liste der Aufgaben aus dem Repository abzurufen, und etwas Magisches diese Methoden ausführt, uns Daten liefert, Änderungen im Repository verarbeitet und für den Heap auch diese verbinden kann Repository.

 this.doFindAllTasks = ( repo ) => repo.findAll(); /*...*/ <DataProvider doCalc={ this.doFindAllTasks }> {(data) => <span>...   -,   data...</span>} </DataProvider> 

Der einzige nicht triviale Moment bei der Implementierung dieser Komponente ist, dass doFindAllTasks () Promise ist. Um unsere Arbeit zu erleichtern, erstellen wir eine separate Komponente, die auf die Ausführung von Promise wartet und einen Nachkommen mit einem berechneten Wert aufruft:

PromiseComponent Code
 import { PureComponent } from 'react'; export default class PromiseComponent extends PureComponent { constructor() { super( ...arguments ); this.state = { error: null, value: null, }; this.prevPromise = null; } componentDidMount() { this.subscribe(); } componentDidUpdate( ) { this.subscribe(); } componentWillUnmount() { this.unsubscribe(); } subscribe() { const { cleanOnPromiseChange, promise } = this.props; if ( promise instanceof Promise && this.prevPromise !== promise ) { if ( cleanOnPromiseChange ) this.setState( { error: null, value: null } ); this.prevPromise = promise; promise.then( value => { if ( this.prevPromise === promise ) { this.setState( { error: null, value } ); } } ) .catch( error => { if ( this.prevPromise === promise ) { this.setState( { error, value: null } ); } } ); } } unsubscribe( ) { if ( this.prevPromise !== null ) { this.prevPromise = null; } } render() { const { children, fallback } = this.props; const { error, value } = this.state; if ( error !== null ) { throw error; } if ( value === undefined || value === null ) { return fallback || null; } return children( value ); } } 

( Github-Quellcode )

Diese Komponente ist in ihrer Logik und internen Struktur RepositoryListener sehr ähnlich. Weil sowohl der eine als auch der andere Ereignisse „unterschreiben“, „abhören“ und sie irgendwie verarbeiten müssen. Und denken Sie auch daran, dass sich die Ereignisse, die Sie anhören müssen, ändern können.

Darüber hinaus sieht die bisher sehr magische Komponente DataProvider sehr einfach aus:

 import repository from './RepositoryHolder'; /*...*/ export default class DataProvider extends PureComponent { constructor() { super( ...arguments ); this.handleRepoChanged = () => this.forceUpdate(); } render() { return <RepositoryListener onChange={this.handleRepoChanged} repository={repository}> <PromiseComponent promise={this.props.doCalc( repository )}> {data => this.props.children( data )} </PromiseComponent> </RepositoryListener>; } } 

( Github-Quellcode )

In der Tat haben wir das Repository (und den separaten singlenton RepositoryHolder, der jetzt importiert wird) mit dem Namen doCalc verwendet, um Aufgabendaten an this.props.children zu übertragen und daher eine Liste von Aufgaben zu zeichnen. Singlenton sieht auch einfach aus:

 const repository = new IndexedDbRepository( 'id' ); export default repository; 

Ersetzen Sie nun den Datenbankaufruf von der Hauptkomponente durch den DataProvider-Aufruf:

 import repository from './RepositoryHolder'; /* ... */ export default class Step3 extends PureComponent { constructor() { super( ...arguments ); this.doFindAllTasks = repository => repository.findAll(); /* ... */ } render() { return <DataProvider doCalc={this.doFindAllTasks} fallback={<><Spinner animation="border" aria-hidden="true" as="span" role="status" /><span>  ...</span></>}> { tasks => <TaskList onAdd={this.handleAdd} onDelete={this.handleDelete} tasks={tasks} /> } </DataProvider>; } } 

( Github-Quellcode )

Das könnte aufhören. Es hat sich als ziemlich gut herausgestellt: Wir beschreiben die Regel für den Empfang von Daten, und eine separate Komponente überwacht den tatsächlichen Empfang dieser Daten sowie das Update. Es gab buchstäblich ein paar kleine Dinge:

  • Für jede Datenanforderung müssen Sie irgendwo im Code (aber nicht in der render() -Methode render() die Funktion des Zugriffs auf das Repository beschreiben und diese Funktion dann an den DataProvider übergeben
  • Der Aufruf des DataProviders ist großartig und ganz im Sinne von React, aber aus Sicht von JSX sehr hässlich. Wenn Sie mehrere Komponenten haben, werden Sie zwei oder drei Verschachtelungsebenen verschiedener DataProvider sehr verwirren.
  • Es ist traurig, dass der Empfang von Daten in einer Komponente (DataProvider) und deren Änderung in einer anderen (Hauptkomponente) erfolgt. Ich möchte es mit dem gleichen Mechanismus beschreiben.

Schritt 4. connect ()


Vertraut mit React-Redux durch den Titel des bereits erratenen Titels. Ich werde im Übrigen den folgenden Hinweis geben: Es wäre schön, wenn die DataProvider-Dienstkomponente anstelle des Aufrufs von children () mit Parametern die Eigenschaften unserer Komponente basierend auf den Regeln ausfüllen würde. Und wenn sich etwas im Repository geändert hat, werden die Eigenschaften einfach mit dem Standardreaktionsmechanismus geändert.

Hierfür verwenden wir die Komponente höherer Ordnung. Eigentlich ist es nichts Kompliziertes, es ist nur eine Funktion, die eine Komponentenklasse als Parameter verwendet und eine andere, komplexere Komponente liefert. Daher wird unsere Funktion, die wir schreiben, sein:

  • Nehmen Sie als Argument die Komponentenklasse, in der die Parameter übergeben werden sollen
  • Akzeptieren Sie eine Reihe von Regeln, wie Daten aus dem Repository abgerufen werden und in welchen Eigenschaften sie abgelegt werden sollen
  • In seiner Verwendung ähnelt es der Funktion connect () von react-redux .

Der Aufruf dieser Funktion sieht folgendermaßen aus:

 const mapRepoToProps = repository => ( { tasks: repository.findAll(), } ); const mapRepoToActions = repository => ( { doAdd: ( newTaskText ) => repository.save( { id: counter(), text: newTaskText } ), doDelete: ( idToDelete ) => repository.deleteById( idToDelete ), } ); const Step4Connected = connect( mapRepoToProps, mapRepoToActions )( Step4 ); 

In den ersten Zeilen wird Step4 Zuordnung zwischen dem Eigenschaftsnamen der Step4 Komponente und den Werten angegeben, die aus dem Repository geladen werden. Als nächstes folgt die Zuordnung für Aktionen: Was passiert, wenn Step4 this.props.doAdd(...) oder this.props.doDelete(...) aus der Step4 Komponente heraus Step4 . Und die letzte Zeile bringt alles zusammen und ruft die Verbindungsfunktion auf. Das Ergebnis ist eine neue Komponente (weshalb diese Technik als Komponente höherer Ordnung bezeichnet wird). Und wir werden nicht mehr die ursprüngliche Step4-Komponente aus der Datei exportieren, sondern den Wrapper darum:

 /*...*/ class Step4 extends PureComponent { /*...*/ } /*...*/ const Step4Connected = connect( /*...*/ )( Step4 ); export default Step4Connected; 

Und die Komponente der Arbeit mit TaskList sieht jetzt wie ein einfacher Wrapper aus:

 class Step4 extends PureComponent { render() { return this.props.tasks === undefined || this.props.tasks === null ? <><Spinner animation="border" aria-hidden="true" as="span" role="status" /><span>  ...</span></> : <TaskList onAdd={this.props.doAdd} onDelete={this.props.doDelete} tasks={this.props.tasks} />; } } 

( Github-Quellcode )

Und alle. Keine Konstruktoren, keine zusätzlichen Handler - alles wird durch die Funktion connect () in den Requisiten der Komponente platziert.

Es bleibt abzuwarten, wie die Funktion connect () aussehen soll.

Connect () Code
 import repository from './RepositoryHolder'; /* ... */ class Connected extends PureComponent { constructor() { super( ...arguments ); this.handleRepoChanged = () => this.forceUpdate(); } render() { const { childClass, childProps, mapRepoToProps, mapRepoToActions } = this.props; const promises = mapRepoToProps( repository, childProps ); const actions = mapRepoToActions( repository, childProps ); return <RepositoryListener onChange={this.handleRepoChanged} repository={repository}> <PromisesComponent promises={promises}> { values => React.createElement( childClass, { ...childProps, ...values, ...actions, } )} </PromisesComponent> </RepositoryListener>; } } export default function connect( mapRepoToProps, mapRepoToActions ) { return childClass => props => <Connected childClass={childClass} childProps={props} mapRepoToActions={mapRepoToActions} mapRepoToProps={mapRepoToProps} />; } 
( Github-Quellcode )

Der Code scheint auch nicht sehr kompliziert zu sein ... obwohl, wenn Sie anfangen zu tauchen, Fragen auftauchen werden. Sie müssen vom Ende lesen. Dort wird die Funktion connect() definiert. Es braucht zwei Parameter und gibt dann eine Funktion zurück, die ... wieder eine Funktion zurückgibt? Nicht wirklich. Die letzte Konstruktion von props => <Connected... gibt nicht nur eine Funktion zurück, sondern eine funktionale Reaktionskomponente . Wenn wir ConnectedStep4 in einen virtuellen Baum einbetten, umfasst dies Folgendes:

  • Eltern -> anonyme Funktionskomponente -> Verbinden -> RepositoryListener -> PromisesComponent -> Schritt 4

Bis zu 4 Zwischenklassen, aber jede erfüllt ihre Funktion. Eine unbenannte Komponente übernimmt die an die Funktion übergebenen Parameter connect(), die Klasse der verschachtelten Komponente, Eigenschaften, die beim Aufruf an die Komponente selbst übertragen werden (Requisiten), und übergibt sie bereits an die Komponente Connect. Die Komponente Connectist dafür verantwortlich, einen Satz aus den übergebenen Parametern zu erhalten Promise(ein Wörterbuchobjekt mit Zeilenschlüsseln und Versprechenswerten). PromisesComponentBietet die Berechnung von Werten und gibt sie an die Connect-Komponente zurück, die sie zusammen mit den ursprünglich übertragenen Eigenschaften (Requisiten), berechneten Eigenschaften (Werten) und Aktionseigenschaften (Aktionen) Step4(über einen Aufruf React.createElement(...)) an die Komponente weitergibt . Nun, die KomponenteRepositoryListenerAktualisiert die Komponente und zwingt dazu, das Versprechen neu zu berechnen, wenn sich im Repository etwas geändert hat.

Wenn eine der Komponenten das Repository von IndexDb verwenden möchte, reicht es daher aus, eine Funktion zu verbinden connect(), die Zuordnung zwischen den Eigenschaften und den Funktionen zum Abrufen von Eigenschaften aus dem Repository zu definieren und sie ohne zusätzliche Kopfschmerzen zu verwenden.

Schritt 5. @ vlsergey / react-indexdb-repo


Schließlich ordnen wir dies alles in Form einer Bibliothek an, damit andere Entwickler es in ihren Projekten verwenden können. Das Ergebnis dieses Schritts war:



Anstelle einer Schlussfolgerung: Was bleibt über Bord


Der obige Code ist bereits praktisch genug, um in industriellen Lösungen verwendet zu werden. Vergessen Sie jedoch nicht die Einschränkungen:

  • Nicht immer wechselnde Eigenschaften sollten zu neuen Versprechungen führen. Hier müssen Sie die Memoization-Funktion verwenden, aber das Datenbankänderungsflag berücksichtigen. Hier bietet sich der Stempel von IndexedDbRepository an (möglicherweise schien dieser Code für jemanden überflüssig zu sein).
  • Das Verbinden des Repositorys durch Import, auch wenn es sich an einem Ort befindet, ist falsch. Es ist notwendig, auf die Verwendung von Kontexten zu achten.
  • , IndexedDB .
  • : . . IndexDB «» , — .
  • , IndexDB Redux Storage. IndexDB , .



Online-

Source: https://habr.com/ru/post/de472246/


All Articles