React + IndexDb + atualização automática = quase AsyncRedux

Neste artigo, mostrarei passo a passo como preparar o IndexDB (um banco de dados embutido em qualquer navegador moderno) para uso em projetos escritos no ReactJS. Como resultado, você pode usar os dados do IndexDB tão convenientemente como se estivessem no Redux Store do seu aplicativo.

O IndexDB é um DBMS orientado a documentos, uma ferramenta conveniente para armazenamento temporário de uma quantidade relativamente pequena (unidades e dezenas de megabytes) de dados estruturados no lado do navegador. A tarefa padrão para a qual eu tenho que usar o IndexDB é armazenar em cache os dados do diretório de negócios do lado do cliente (nomes de países, cidades, moedas por código etc.) Depois de copiá-las para o lado do cliente, você só pode ocasionalmente baixar atualizações desses diretórios do servidor (ou o todo - elas são pequenas) e não fazer isso sempre que abrir a janela do navegador.

Existem maneiras não padronizadas, muito controversas, mas úteis de usar o IndexDB:

  • armazenar em cache dados sobre todos os objetos de negócios para que, no lado do navegador, use os extensos recursos de classificação e filtragem
  • armazenando o status do aplicativo no IndexDB em vez do Redux Store

Três diferenças importantes entre o IndexDB e o Redux Store são importantes para nós:

  1. O IndexDB é um armazenamento externo que não é limpo ao sair da página. Além disso, é o mesmo para várias guias abertas (que às vezes levam a um comportamento inesperado)
  2. O IndexDB é um DBMS completamente assíncrono. Todas as operações - abertura, leitura, escrita, pesquisa - assíncronas.
  3. O IndexDB não pode (de maneira trivial) ser armazenado no JSON e usar técnicas de truque cerebral do Redux para criar instantâneos, facilidade de depuração e uma jornada ao passado.

Etapa 0: lista de tarefas


Já é um exemplo clássico com uma lista de tarefas. Variante com armazenamento de estado no estado do componente atual e único

Implementação de um componente da lista de tarefas que armazena a lista em um estado do componente
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>; } } 
( código fonte do github )

Até o momento, todas as operações com tarefas são síncronas. Se a adição de uma tarefa demorar 3 segundos, o navegador congelará por 3 segundos. Obviamente, enquanto mantemos tudo em nossa memória, não conseguimos pensar nisso. Quando incluímos o processamento em um servidor ou em um banco de dados local, teremos que cuidar também do belo processamento da assincronia. Por exemplo, bloquear o trabalho com uma tabela (ou elementos individuais) ao adicionar ou remover elementos.

Para não repetir a descrição da interface do usuário no futuro, colocaremos em um componente TaskList separado, cuja única tarefa gerará o código HTML da lista de tarefas. Ao mesmo tempo, substituiremos os botões usuais por um invólucro especial ao redor do botão de inicialização, que bloqueará o botão até que o manipulador de botões conclua sua execução, mesmo que esse manipulador seja uma função assíncrona.

Implementando um componente que armazena uma lista de tarefas no estado de reação
 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} /> </>; } } 
( código fonte do github )

Implementação de um componente que exibe uma lista de tarefas e contém um formulário para adicionar um novo
 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>; } } 
( código fonte do github )

Já no código de amostra, você pode ver as palavras-chave assíncronas / aguardadas. Construções assíncronas / aguardam podem reduzir significativamente a quantidade de código que funciona com o Promises. A palavra-chave aguardar permite que você aguarde uma resposta de uma função retornando Promise, como se fosse uma função regular (em vez de aguardar um resultado em then ()). Obviamente, uma função assíncrona não se transforma magicamente em uma síncrona e, por exemplo, o encadeamento de execução será interrompido quando a espera for usada. Mas, então, o código se torna mais conciso e compreensível, e aguardar pode ser usado tanto em loops quanto em construções try / catch / finalmente.

Por exemplo, TaskList chama não apenas o manipulador this.props.onAdd , mas usa a palavra-chave this.props.onAdd . Nesse caso, se o manipulador for uma função normal que não retornará nada ou retornará qualquer valor que não seja Promise , o componente TaskList simplesmente continuará o método handleAdd da maneira usual. Mas se o manipulador retornar Promise (incluindo se o manipulador for declarado como uma função assíncrona), o TaskList aguardará o término da execução do manipulador e só então redefinirá os valores das newTaskText e newTaskText .

Etapa 1: adicionar o IndexDB ao componente React


Para simplificar nosso trabalho, primeiro escreveremos um componente simples que implementa métodos Promise para:

  • abrir um banco de dados junto com manipulação trivial de erros
  • procure itens no banco de dados
  • adicionando itens ao banco de dados

O primeiro é o mais "não trivial" - até 5 manipuladores de eventos. No entanto, nenhuma ciência de foguetes:

openDatabasePromise () - abre um banco de dados
 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 - o wrapper IndexDb chama no 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' ); } 

Juntando tudo em uma classe IndexedDbRepository

IndexedDbRepository - wrapper em torno do 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 ) ); } } 
( código fonte do github )

Agora você pode acessar o IndexDB a partir do código:

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

Conecte este "repositório" ao nosso componente. De acordo com as regras de reação, uma chamada para o servidor deve estar no método componentDidMount ():

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

Teoricamente, a função componentDidMount() pode ser declarada como assíncrona, e as construções async / waitit podem ser usadas em vez de then (). Mas ainda componentDidMount() não é a função “nossa”, mas chamada por React. Quem sabe como a biblioteca react 17.x se comportará em resposta a uma tentativa de retornar o Promise vez de undefined ?

Agora, no construtor, em vez de preenchê-lo com uma matriz vazia (ou uma matriz com dados de teste), a preencheremos com nulo. E na renderização, ele processará esse nulo conforme a necessidade de aguardar o processamento de dados. Quem deseja, em princípio, pode colocar isso em bandeiras separadas, mas por que produzir entidades?

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

Resta implementar os handleDelete / 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() } ); }; } 

Nos dois manipuladores, primeiro recorremos ao repositório para adicionar ou remover um item, depois limpamos o estado do componente atual e solicitamos novamente uma nova lista do repositório. Parece que as chamadas para setState () irão uma após a outra. Mas a palavra-chave wait nas últimas linhas dos manipuladores fará com que a segunda chamada setState () ocorra somente após a resolução da Promise () obtida do método findAll ().

Etapa 2. Ouça as alterações


Uma grande falha no código acima é que, primeiramente, o repositório está conectado em cada componente. Em segundo lugar, se um componente altera o conteúdo do repositório, o outro componente não o conhece até reler o estado como resultado de qualquer ação do usuário. Isso é inconveniente.

Para combater isso, apresentaremos o novo componente RepositoryListener e deixaremos fazer duas coisas. Este componente, em primeiro lugar, poderá se inscrever nas alterações no repositório. Em segundo lugar, o RepositoryListener notificará o componente que o criou dessas alterações.

Primeiro de tudo, adicionando a capacidade de registrar manipuladores no 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 ); } } 

( código fonte do github )

Passaremos um carimbo para os manipuladores, que serão alterados a cada chamada para onChange (). E modificamos o método _tx para que onChange() chamado para cada chamada em uma transação com o modo 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 } } 

( código fonte do github )

Se ainda assim usamos then() / catch() para trabalhar com o Promise, teríamos que duplicar a chamada para onChange() ou usar polyfills especiais para Promise () que suportam final() . Felizmente, o assíncrono / espera permite que você faça isso de forma simples e sem código desnecessário.

O próprio componente RepositoryListener conecta um ouvinte de evento nos métodos componentDidMount e componentWillUnmount:

Código 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; } } 
( código fonte do github )

Agora, incluiremos o processamento de alterações do repositório em nosso componente principal e, guiados pelo princípio DRY , removeremos o código correspondente do handleDelete / 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 } 

( código fonte do github )

E adicionamos uma chamada para handleRepositoryChanged a partir do RepositoryListener conectado:

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

( código fonte do github )

Etapa 3. Retire o carregamento dos dados e sua atualização em um componente separado


Escrevemos um componente que pode receber dados do repositório, pode alterar dados no repositório. Mas se você imaginar um projeto grande com mais de 100 componentes, verifica-se que cada componente que exibe dados do repositório será forçado a:

  • Verifique se o repositório está conectado corretamente a partir de um único ponto
  • Forneça o carregamento inicial de dados no método componentDidMount()
  • Conecte o componente RepositoryListener , que fornece uma chamada de manipulador para recarregar as alterações

Existem muitas ações duplicadas? Parece que não. E se algo for esquecido? Se perder com copiar e colar?

Seria ótimo, de alguma forma, escrever uma vez a regra para obter a lista de tarefas do repositório, e algo mágico executar esses métodos, nos fornecer dados, processar alterações no repositório e, para o monte, ele também poderá conectar isso. repositório.

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

O único momento não trivial na implementação desse componente é que doFindAllTasks () é Promise. Para facilitar nosso trabalho, criaremos um componente separado que aguarda a execução do Promise e chama um descendente com um valor calculado:

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

( código fonte do github )

Esse componente em sua lógica e estrutura interna é muito semelhante ao RepositoryListener. Porque um e o outro devem "assinar", "ouvir" os eventos e, de alguma forma, processá-los. E também lembre-se de que os eventos que você precisa ouvir podem mudar.

Além disso, o componente mágico DataProvider até agora parece muito simples:

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

( código fonte do github )

De fato, pegamos o repositório (e um repositório separado RepositoryHolder, que agora está sendo importado), chamado doCalc, isso permitirá que você transfira dados da tarefa para this.props.children e, portanto, desenhe uma lista de tarefas. Singlenton também parece simples:

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

Agora substitua a chamada do banco de dados do componente principal pela chamada 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>; } } 

( código fonte do github )

Isso pode parar. Acabou bem: descrevemos a regra para o recebimento de dados e um componente separado monitora o recebimento real desses dados, bem como a atualização. Havia literalmente algumas pequenas coisas:

  • Para cada solicitação de dados, em algum lugar do código (mas não no método render() é necessário descrever a função de acessar o repositório e depois passar essa função para o DataProvider
  • A chamada para o DataProvider é ótima e bastante no espírito do React, mas muito feia do ponto de vista do JSX. Se você tiver vários componentes, dois ou três níveis de aninhamento de diferentes DataProviders o confundirão muito.
  • É triste que o recebimento de dados seja feito em um componente (DataProvider) e suas alterações sejam feitas em outro (componente principal). Eu gostaria de descrevê-lo com o mesmo mecanismo.

Etapa 4. connect ()


Familiarizado com react-redux pelo título do título já adivinhou. Farei a seguinte dica para o resto: seria bom se, em vez de chamar children () com parâmetros, o componente de serviço DataProvider preenchesse as propriedades de nosso componente com base nas regras. E se algo mudou no repositório, simplesmente alteraria as propriedades com o mecanismo React padrão.

Para isso, usaremos o componente de ordem superior. Na verdade, não é nada complicado, é apenas uma função que usa uma classe de componente como parâmetro e fornece outro componente mais complexo. Portanto, nossa função que escrevemos será:

  • Tome como argumento a classe de componente para onde passar os parâmetros
  • Aceite um conjunto de regras, como obter dados do repositório e em quais propriedades colocá-lo
  • Em seu uso, será semelhante à função connect () do react-redux .

A chamada para esta função terá a seguinte aparência:

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

As primeiras linhas Step4 mapeamento entre o nome da propriedade do componente Step4 e os valores que serão carregados no repositório. A seguir, vem o mapeamento das ações: o que acontecerá se Step4 chamar this.props.doAdd(...) ou this.props.doDelete(...) no componente Step4 . E a última linha reúne tudo e chama a função de conexão. O resultado é um novo componente (e é por isso que essa técnica é chamada de componente de ordem superior). E não exportaremos mais do arquivo o componente Step4 original, mas o wrapper em torno dele:

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

E o componente de trabalhar com o TaskList agora parece um invólucro simples:

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

( código fonte do github )

E é isso. Sem construtores, sem manipuladores adicionais - tudo é colocado pela função connect () nos props do componente.

Resta ver como deve ser a função connect ().

Código 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} />; } 
( código fonte do github )

O código também parece não ser muito complicado ... embora se você começar a mergulhar, surgirão dúvidas. Você precisa ler a partir do final. É aí que a função connect() é definida. São necessários dois parâmetros e, em seguida, retorna uma função que retorna ... novamente uma função? Na verdade não. A última construção de props => <Connected... retorna não apenas uma função, mas um componente React funcional . Assim, quando incorporamos o ConnectedStep4 em uma árvore virtual, ele inclui:

  • pai -> componente funcional anônimo -> Conectar -> RepositoryListener -> PromisesComponent -> Etapa4

Até 4 classes intermediárias, mas cada uma desempenha sua função. Um componente não nomeado pega os parâmetros passados ​​para a função connect(), a classe do componente aninhado, propriedades que são transferidas para o próprio componente quando chamado (props) e os passa para o componente Connect. O componente Connecté responsável por obter um conjunto dos parâmetros passados Promise(um objeto de dicionário com chaves de linha e valores prometidos). PromisesComponentfornece o cálculo dos valores, transmitindo-os de volta ao componente Connect, que, juntamente com as propriedades transferidas originais (adereços), propriedades calculadas (valores) e propriedades-ações (ações), os transmite ao componente Step4(por meio de uma chamada React.createElement(...)). Bem, o componenteRepositoryListeneratualiza o componente, forçando a recalcular promis se algo mudou no repositório.

Como resultado, se algum dos componentes quiser usar o repositório do IndexDb, será suficiente para ele conectar uma função connect(), determinar o mapeamento entre as propriedades e as funções de obter propriedades do repositório e usá-las sem nenhuma dor de cabeça adicional.

Etapa 5. @ vlsergey / react-indexdb-repo


Finalmente, organizamos tudo isso na forma de uma biblioteca para que outros desenvolvedores possam usá-lo em seus projetos. O resultado desta etapa foi:



Em vez de uma conclusão: o que resta ao mar


O código acima já é prático o suficiente para uso em soluções industriais. Mas ainda assim, não se esqueça das limitações:

  • Nem sempre a alteração das propriedades deve levar a novas promessas. Aqui você precisa usar a função de memorização, mas leve em consideração o sinalizador de alteração do banco de dados. É aqui que o carimbo do IndexedDbRepository é útil (talvez para alguns esse código parecesse redundante).
  • Conectar o repositório através da importação, mesmo que em um local, esteja errado. É necessário olhar para o uso de contextos.
  • , IndexedDB .
  • : . . IndexDB «» , — .
  • , IndexDB Redux Storage. IndexDB , .



Online-

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


All Articles