React + IndexDb + mise Ă  jour automatique = presque AsyncRedux

Dans cet article, je vais vous expliquer étape par étape comment préparer IndexDB (une base de données intégrée à tout navigateur moderne) pour une utilisation dans des projets écrits en ReactJS. Par conséquent, vous pouvez utiliser les données d'IndexDB aussi facilement que si elles se trouvaient dans le magasin Redux de votre application.

IndexDB est un SGBD orientĂ© document, un outil pratique pour le stockage temporaire d'une quantitĂ© relativement petite (unitĂ©s et dizaines de mĂ©gaoctets) de donnĂ©es structurĂ©es cĂŽtĂ© navigateur. La tĂąche standard pour laquelle je dois utiliser IndexDB comprend la mise en cache des donnĂ©es des rĂ©pertoires d'entreprises cĂŽtĂ© client (noms de pays, villes, devises par code, etc.). AprĂšs les avoir copiĂ©s cĂŽtĂ© client, vous ne pouvez tĂ©lĂ©charger qu'occasionnellement des mises Ă  jour Ă  partir de ces rĂ©pertoires depuis le serveur (ou l'ensemble - ils sont petits) et ne pas le faire Ă  chaque fois que vous ouvrez la fenĂȘtre du navigateur.

Il existe des méthodes non standard, trÚs controversées, mais efficaces pour utiliser IndexDB:

  • la mise en cache des donnĂ©es sur tous les objets mĂ©tier de sorte que du cĂŽtĂ© du navigateur, utilisez les capacitĂ©s de tri et de filtrage Ă©tendues
  • stockage de l'Ă©tat de l'application dans IndexDB au lieu de Redux Store

Trois différences clés entre IndexDB et Redux Store sont importantes pour nous:

  1. IndexDB est un stockage externe qui n'est pas effacĂ© lorsque vous quittez la page. De plus, il en va de mĂȘme pour plusieurs onglets ouverts (ce qui conduit parfois Ă  un comportement quelque peu inattendu)
  2. IndexDB est un SGBD complÚtement asynchrone. Toutes les opérations - ouverture, lecture, écriture, recherche - asynchrone.
  3. IndexDB ne peut pas (de maniĂšre triviale) ĂȘtre stockĂ© dans JSON et utiliser des techniques de lavage de cerveau Redux pour crĂ©er des instantanĂ©s, faciliter le dĂ©bogage et voyager dans le passĂ©.

Étape 0: liste des tñches


DĂ©jĂ  un exemple classique avec une liste de tĂąches. Variante avec stockage d'Ă©tat dans l'Ă©tat du composant actuel et unique

Implémentation d'un composant de liste de tùches stockant la liste dans un état de composant
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>; } } 
( code source github )

Jusqu'à présent, toutes les opérations avec des tùches sont synchrones. Si l'ajout d'une tùche prend 3 secondes, le navigateur se fige juste pendant 3 secondes. Bien sûr, alors que nous gardons tout en mémoire, nous ne pouvons pas y penser. Lorsque nous incluons le traitement avec un serveur ou avec une base de données locale, nous devrons également nous occuper du beau traitement de l'asynchronie. Par exemple, bloquer le travail avec une table (ou des éléments individuels) lors de l'ajout ou de la suppression d'éléments.

Afin de ne pas rĂ©pĂ©ter la description de l'interface utilisateur Ă  l'avenir, nous la placerons dans un composant TaskList distinct, dont la seule tĂąche gĂ©nĂ©rera le code HTML de la liste des tĂąches. Dans le mĂȘme temps, nous remplacerons les boutons habituels par un wrapper spĂ©cial autour du bouton d'amorçage, qui bloquera le bouton jusqu'Ă  ce que le gestionnaire de boutons termine son exĂ©cution, mĂȘme si ce gestionnaire est une fonction asynchrone.

Implémentation d'un composant stockant une liste de tùches en état réactif
 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} /> </>; } } 
( code source github )

Implémentation d'un composant qui affiche une liste de tùches et contient un formulaire pour en ajouter une nouvelle
 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>; } } 
( code source github )

DĂ©jĂ  dans l'exemple de code, vous pouvez voir les mots-clĂ©s async / wait. Les constructions asynchrones / attendent peuvent rĂ©duire considĂ©rablement la quantitĂ© de code qui fonctionne avec Promises. Le mot-clĂ© wait vous permet d'attendre une rĂ©ponse d'une fonction renvoyant Promise, comme s'il s'agissait d'une fonction rĂ©guliĂšre (au lieu d'attendre un rĂ©sultat dans then ()). Bien sĂ»r, une fonction asynchrone ne se transforme pas par magie en une fonction synchrone et, par exemple, le thread d'exĂ©cution sera interrompu lorsque l'attente est utilisĂ©e. Mais alors le code devient plus concis et comprĂ©hensible, et wait peut ĂȘtre utilisĂ© Ă  la fois dans les boucles et dans les constructions try / catch / finally.

Par exemple, TaskList appelle non seulement le gestionnaire this.props.onAdd , mais le fait à l'aide du mot clé this.props.onAdd . Dans ce cas, si le gestionnaire est une fonction normale qui ne renverra rien ou ne renverra aucune valeur autre que Promise , le composant TaskList continuera simplement la méthode handleAdd de la maniÚre habituelle. Mais si le gestionnaire renvoie Promise (y compris si le gestionnaire est déclaré en tant que fonction asynchrone), alors la TaskList attendra la fin de l'exécution du gestionnaire, et ce n'est newTaskAdding les valeurs des newTaskText newTaskAdding et newTaskText seront réinitialisées.

Étape 1: ajouter IndexDB au composant React


Pour simplifier notre travail, nous allons d'abord écrire un composant simple qui implémente les méthodes Promise pour:

  • ouverture d'une base de donnĂ©es avec gestion des erreurs triviale
  • rechercher des Ă©lĂ©ments dans la base de donnĂ©es
  • ajout d'Ă©lĂ©ments Ă  la base de donnĂ©es

Le premier est le plus «non trivial» - jusqu'à 5 gestionnaires d'événements. Cependant, pas de science fusée:

openDatabasePromise () - ouvre une base de données
 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 - encapsule les appels IndexDb dans Promise
 //    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' ); } 

Mettre tout cela ensemble dans une classe IndexedDbRepository

IndexedDbRepository - wrapper autour d'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 ) ); } } 
( code source github )

Vous pouvez maintenant accéder à IndexDB à partir du code:

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

Connectez ce «rĂ©fĂ©rentiel» Ă  notre composant. Selon les rĂšgles de react, un appel au serveur doit ĂȘtre dans la mĂ©thode componentDidMount ():

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

ThĂ©oriquement, la fonction componentDidMount() peut ĂȘtre dĂ©clarĂ©e async, puis les constructions async / attendent peuvent ĂȘtre utilisĂ©es Ă  la place de then (). Mais componentDidMount() n'est toujours pas «notre» fonction, mais appelĂ© par React. Qui sait comment la bibliothĂšque react 17.x se comportera en rĂ©ponse Ă  une tentative de retour de Promise au lieu d'ĂȘtre undefined ?

Maintenant, dans le constructeur, au lieu de le remplir avec un tableau vide (ou un tableau avec des données de test), nous le remplirons avec null. Et dans le rendu, il traitera ce null comme la nécessité d'attendre le traitement des données. Ceux qui souhaitent, en principe, peuvent mettre cela dans des drapeaux séparés, mais pourquoi produire des entités?

  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></>; /* ... */ } 

Il reste à implémenter les 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() } ); }; } 

Dans les deux gestionnaires, nous nous tournons d'abord vers le référentiel pour ajouter ou supprimer un élément, puis nous effaçons l'état du composant actuel et demandons à nouveau une nouvelle liste au référentiel. Il semble que les appels à setState () se feront l'un aprÚs l'autre. Mais le mot-clé wait dans les derniÚres lignes des gestionnaires provoquera le deuxiÚme appel setState () uniquement aprÚs la résolution de Promise () obtenue à partir de la méthode findAll ().

Étape 2. Écoutez les changements


Un gros dĂ©faut dans le code ci-dessus est que, premiĂšrement, le rĂ©fĂ©rentiel est connectĂ© dans chaque composant. DeuxiĂšmement, si un composant modifie le contenu du rĂ©fĂ©rentiel, l'autre composant n'en est pas informĂ© jusqu'Ă  ce qu'il relise l'Ă©tat suite Ă  des actions de l'utilisateur. C'est gĂȘnant.

Pour lutter contre cela, nous allons introduire le nouveau composant RepositoryListener et le laisser faire deux choses. Ce composant, dans un premier temps, pourra s'abonner aux modifications du référentiel. DeuxiÚmement, RepositoryListener informera le composant qui l'a créé de ces modifications.

Tout d'abord, en ajoutant la possibilité d'enregistrer des gestionnaires dans IndexedDbRepository:

 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 ); } } 

( code source github )

Nous passerons un tampon aux gestionnaires, qui changera à chaque appel à onChange (). Et nous modifions la méthode _tx pour que onChange() appelé pour chaque appel dans une transaction en mode readwrite :

  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 } } 

( code source github )

Si nous utilisions toujours then() / catch() pour travailler avec Promise, nous devrions soit dupliquer l'appel à onChange() , soit utiliser des polyfills spéciaux pour Promise () qui prennent en charge final() . Heureusement, async / wait vous permet de le faire simplement et sans code inutile.

Le composant RepositoryListener lui-mĂȘme connecte un Ă©couteur d'Ă©vĂ©nements dans les mĂ©thodes componentDidMount et componentWillUnmount:

Code RepositoryListener
 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; } } 
( code source github )

Nous allons maintenant inclure le traitement des modifications du référentiel dans notre composant principal et, guidés par le principe DRY , nous supprimerons le code correspondant du handleDelete handleAdd / handleDelete :

  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 } 

( code source github )

Et nous ajoutons un appel à handleRepositoryChanged à partir du RepositoryListener connecté:

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

( code source github )

Étape 3. Retirez le chargement des donnĂ©es et leur mise Ă  jour dans un composant distinct


Nous avons écrit un composant qui peut recevoir des données du référentiel, peut changer des données dans le référentiel. Mais si vous imaginez un grand projet avec plus de 100 composants, il s'avÚre que chaque composant qui affiche des données du référentiel sera obligé de:

  • Assurez-vous que le rĂ©fĂ©rentiel est correctement connectĂ© Ă  partir d'un seul point
  • Fournir le chargement initial des donnĂ©es dans la mĂ©thode componentDidMount()
  • Connectez le composant RepositoryListener , qui fournit un appel de gestionnaire pour recharger les modifications

Y a-t-il trop d'actions en double? Il semble que non. Et si quelque chose est oublié? Se perdre avec le copier-coller?

Ce serait formidable de s'assurer en quelque sorte que nous écrivons une fois la rÚgle pour obtenir la liste des tùches du référentiel, et quelque chose de magique exécute ces méthodes, nous donne des données, traite les changements dans le référentiel, et pour le tas, il peut également connecter ce référentiel.

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

Le seul moment non trivial dans l'implémentation de ce composant est que doFindAllTasks () est Promise. Pour faciliter notre travail, nous allons créer un composant distinct qui attend l'exécution de Promise et appelle un descendant avec une valeur calculée:

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 ); } } 

( code source github )

Ce composant dans sa logique et sa structure interne est trÚs similaire à RepositoryListener. Parce que l'un et l'autre doivent «signer», «écouter» les événements et les traiter d'une maniÚre ou d'une autre. Et gardez également à l'esprit que les événements dont vous devez écouter peuvent changer.

De plus, le composant trÚs magique DataProvider semble jusqu'à présent trÚs simple:

 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>; } } 

( code source github )

En effet, nous avons pris le référentiel (et un RepositoryHolder singlenton séparé, qui est maintenant en importation), appelé doCalc, cela vous permettra de transférer des données de tùche vers this.props.children, et donc de dresser une liste de tùches. Singlenton a aussi l'air simple:

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

Remplacez maintenant l'appel de base de données du composant principal par l'appel DataProvider:

 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>; } } 

( code source github )

Cela pourrait s'arrĂȘter. Cela s'est plutĂŽt bien passĂ©: nous dĂ©crivons la rĂšgle de rĂ©ception des donnĂ©es, et un composant sĂ©parĂ© surveille la rĂ©ception rĂ©elle de ces donnĂ©es, ainsi que la mise Ă  jour. Il y avait littĂ©ralement quelques petites choses:

  • Pour chaque demande de donnĂ©es, quelque part dans le code (mais pas dans la mĂ©thode render() vous devez dĂ©crire la fonction d'accĂšs au rĂ©fĂ©rentiel, puis transmettre cette fonction au DataProvider
  • L'appel au DataProvider est grand et tout Ă  fait dans l'esprit de React, mais trĂšs moche du point de vue de JSX. Si vous avez plusieurs composants, deux ou trois niveaux d'imbrication de diffĂ©rents DataProviders vous embrouilleront beaucoup.
  • Il est regrettable que la rĂ©ception des donnĂ©es se fasse dans un composant (DataProvider) et que leur modification se fasse dans un autre (composant principal). Je voudrais le dĂ©crire avec le mĂȘme mĂ©canisme.

Étape 4. connect ()


Familier avec react-redux par le titre du titre déjà deviné. Je ferai le conseil suivant au reste: ce serait bien si, au lieu d'appeler children () avec des paramÚtres, le composant de service DataProvider remplirait les propriétés de notre composant en fonction des rÚgles. Et si quelque chose a changé dans le référentiel, cela changerait simplement les propriétés avec le mécanisme React standard.

Pour cela, nous utiliserons le composant d'ordre supĂ©rieur. En fait, ce n’est rien de compliquĂ©, c’est juste une fonction qui prend une classe de composants comme paramĂštre et donne un autre composant plus complexe. Par consĂ©quent, notre fonction que nous Ă©crivons sera:

  • Prenez comme argument la classe de composants oĂč passer les paramĂštres
  • Acceptez un ensemble de rĂšgles, comment obtenir des donnĂ©es du rĂ©fĂ©rentiel et dans quelles propriĂ©tĂ©s les mettre
  • Dans son utilisation, elle sera similaire Ă  la fonction connect () de react-redux .

L'appel Ă  cette fonction ressemblera Ă  ceci:

 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 ); 

Les premiÚres lignes Step4 mappage entre le nom de propriété du composant Step4 et les valeurs qui seront chargées à partir du référentiel. Vient ensuite le mappage des actions: que se passera-t-il si Step4 appelez this.props.doAdd(...) ou this.props.doDelete(...) partir du composant Step4 . Et la derniÚre ligne rassemble tout et appelle la fonction de connexion. Le résultat est un nouveau composant (c'est pourquoi cette technique est appelée composant d'ordre supérieur). Et nous n'exporterons plus du fichier le composant Step4 d'origine, mais le wrapper qui l'entoure:

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

Et le composant de travail avec TaskList ressemble maintenant Ă  un simple wrapper:

 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} />; } } 

( code source github )

Et c'est tout. Aucun constructeur, aucun gestionnaire supplémentaire - tout est placé par la fonction connect () dans les accessoires du composant.

Reste Ă  voir Ă  quoi devrait ressembler la fonction connect ().

Code Connect ()
 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} />; } 
( code source github )

Le code ne semble pas non plus trÚs compliqué ... bien que si vous commencez à plonger, des questions se poseront. Vous devez lire depuis la fin. C'est là que la fonction connect() est définie. Il prend deux paramÚtres, puis renvoie une fonction qui renvoie ... encore une fonction? Pas vraiment. La derniÚre construction d' props => <Connected... renvoie non seulement une fonction, mais un composant fonctionnel React . Ainsi, lorsque nous intégrerons ConnectedStep4 dans une arborescence virtuelle, cela inclura:

  • parent -> composant fonctionnel anonyme -> Connect -> RepositoryListener -> PromisesComponent -> Step4

Jusqu'Ă  4 classes intermĂ©diaires, mais chacune remplit sa fonction. Un composant sans nom prend les paramĂštres passĂ©s Ă  la fonction connect(), la classe du composant imbriquĂ©, les propriĂ©tĂ©s qui sont transfĂ©rĂ©es au composant lui-mĂȘme lorsqu'il est appelĂ© (props) et les transmet dĂ©jĂ  au composant Connect. Le composant Connectest chargĂ© d'obtenir un ensemble Ă  partir des paramĂštres transmis Promise(un objet dictionnaire avec des clĂ©s de ligne et des valeurs de promesse). PromisesComponentfournit le calcul des valeurs, en les renvoyant au composant Connect, qui, avec les propriĂ©tĂ©s transfĂ©rĂ©es d'origine (accessoires), les propriĂ©tĂ©s calculĂ©es (valeurs) et les propriĂ©tĂ©s-actions (actions), les transmet au composant Step4(via un appel React.createElement(...)). Eh bien, le composantRepositoryListenermet Ă  jour le composant, forçant Ă  recalculer promis'y si quelque chose a changĂ© dans le rĂ©fĂ©rentiel.

Par consĂ©quent, si l'un des composants souhaite utiliser le rĂ©fĂ©rentiel depuis IndexDb, il lui suffira de connecter une fonction connect(), de dĂ©terminer le mappage entre les propriĂ©tĂ©s et les fonctions d'obtention de propriĂ©tĂ©s du rĂ©fĂ©rentiel et de les utiliser sans aucun mal de tĂȘte supplĂ©mentaire.

Étape 5. @ vlsergey / react-indexdb-repo


Enfin, nous organisons tout cela sous la forme d'une bibliothÚque afin que d'autres développeurs puissent l'utiliser dans leurs projets. Le résultat de cette étape a été:



Au lieu d'une conclusion: ce qui reste Ă  la mer


Le code ci-dessus est dĂ©jĂ  suffisamment pratique pour ĂȘtre utilisĂ© dans des solutions industrielles. Mais n'oubliez pas les limitations:

  • Les propriĂ©tĂ©s qui ne changent pas toujours devraient conduire Ă  de nouvelles promesses. Ici, vous devez utiliser la fonction de mĂ©morisation, mais prenez en compte l'indicateur de changement de base de donnĂ©es. C'est lĂ  que le tampon de IndexedDbRepository est utile (peut-ĂȘtre que ce morceau de code semblait redondant pour quelqu'un).
  • La connexion du rĂ©fĂ©rentiel via l'importation, mĂȘme en un seul endroit, est incorrecte. Il faut regarder vers l'utilisation des contextes.
  • , IndexedDB .
  • : . . IndexDB «» , — .
  • , IndexDB Redux Storage. IndexDB , .



Online-

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


All Articles