Skip to content

Read Data

There are two ways to retrieve data from your remote stores. Either of these methods can be used with documents and collections.

  • Execute a function to fetch data once
  • Set up a "stream" to receive realtime updates

Fetch Data Once

When you get data by executing fetch(), the data will be fetched from a server by your "remote" store plugin and then added to your module's data by your "cache" store plugin.

For displaying fetched data in the DOM see the Displaying data in the DOM.

Fetch a Single Document

When you call fetch() on a document module, your remote store will go and fetch the document from your database and add it to your local cache store.

javascript
const bulbasaur = magnetar.doc('pokedex/001')

// bulbasaur's data is not yet in cached data
// bulbasaur.data ≈ {}

await bulbasaur.fetch()

// now it is available:

const data = bulbasaur.data
// bulbasaur.data ≈ { name: 'Bulbasaur' }

The fetch action returns whatever data was fetched. When fetching a single document, the data will be an object.

js
// create the module and immediately use the fetched data
const bulbasaurData = await magnetar.doc('pokedex/001').fetch()

// bulbasaurData ≈ { name: 'Bulbasaur' }

Fetching is optimistic by default!

js
const bulbasaur = magnetar.collection('pokedex').doc('001')

await bulbasaur.fetch()
// ⤷ does nothing if already fetched once

await bulbasaur.fetch({ force: true })
// ⤷ makes API call to remote store

Fetch Multiple Documents

javascript
const pokedexModule = magnetar.collection('pokedex')

// pokedexModule.data ≈ Map<> // empty!
// pokedexModule.data.values() ≈ [] // empty!

await pokedexModule.fetch()

// now they are available locally:
// pokedexModule.data ≈ Map<
//   '001': { name: 'Bulbasaur' },
//   /* etc...*/
// >

const allPokemon = pokedexModule.data.values()
// allPokemon ≈ [{ name: 'Bulbasaur' }, /* etc...*/ ]

The fetch action returns whatever data was fetched. When fetching a collection, the data will be a Map .

js
// create the module and immediately use the fetched data
const pokedexData = await magnetar.collection('pokedex').fetch()

// pokedexData ≈ Map<
//   '001': { name: 'Bulbasaur' },
//   /* etc...*/
// >

Fetching is optimistic by default!

js
const pokedex = magnetar.collection('pokedex')

pokedex.fetch()
// ⤷ does nothing if already fetched once

pokedex.fetch({ force: true })
// ⤷ makes API call to remote store

Fetch and get document data

You can fetch and immediately return a document's data. This is optimistic by default — meaning it will only make an API call once and otherwise return the already fetched data. Add the force option to force multiple fetch calls.

js
// this will only fetch the data once, and from there on always return the already fetched data:
const bulbasaurData = await magnetar.collection('pokedex').doc('001').fetch()

// every time this is executed, it will (re)fetch and return the data:
const bulbasaurData = await magnetar.collection('pokedex').doc('001').fetch({ force: true })

// after fetching the data is also available at `.data`:
magnetar.collection('pokedex').doc('001').data

Fetch Collection Count

You can also fetch the count of documents in a collection:

js
const count = await magnetar.collection('pokedex').fetchCount() // 151

// after fetching, the count is also available at `.count`:
magnetar.collection('pokedex').count // 151

Fetch the Sum of a field

You can also fetch the sum of a field in a collection:

js
const sum = await magnetar.collection('pokedex').fetchSum('base.HP') // 9696

// after fetching, the sum is also available at `.sum`:
magnetar.collection('pokedex').sum // { base: { HP: 9696 } }

Fetch an Average of a field

You can also fetch the average of a field in a collection:

js
const average = await magnetar.collection('pokedex').fetchAverage('base.HP') // 64.2

// after fetching, the average is also available at `.average`:
magnetar.collection('pokedex').average // { base: { HP: 64.2 } }

Stream Realtime Updates

When you set up a stream for a document or collection, just like fetch(), your the data will be fetched from a server by your remote store plugin and then added to your module's cache data.

Afterwards, any changes to this document remotely will automatically be reflected in your module's cache data while the stream is open.

Please note: a streaming promise will never resolve as long as your stream is open! There is no way to know when or how many documents will be loaded in, as this depends on your remote store.

For displaying streamed data in the DOM see the Displaying data in the DOM.

Firestore vs Magnetar

The Firestore JS SDK has a built-in feature for realtime updates via a method called onSnapshot. The main pain points are:

  • The syntax you have to use is very convoluted and complex
  • If your not careful, you can open the same stream twice and they both will use memory
  • You need to write a lot of code to capture the documents that comes in and save them in local cache
  • You need to organize where and how to temporarily save the function you get back to stop the stream

Magnetar's Firestore Plugin uses onSnapshot under the hood but does these things for you:

  • The syntax is super clean. It's just .stream()
  • A stream cannot be opened twice on accident, the already open stream will be returned in case you open it again
  • No need to write any extra code, Magnetar captures the documents that comes in and saves them in local cache for you
  • No need to keep around the function to stop the stream, Magnetar does this for you

Stream a Collection

javascript
const pokedexModule = magnetar.collection('pokedex')
// open the stream
pokedexModule.stream().catch((error) => {
  // the stream was closed because of an error
})

// ... some time later

// incoming data from the remote store will be
// added to your module's data and stay in sync

// pokedexModule.data ≈ Map<
//   '001': { name: 'Bulbasaur' },
//   /* etc...*/
// >
// pokedexModule.data.values() ≈ [{ name: 'Bulbasaur' }, /* etc...*/ ]

That's it! You don't need any other logic!!

Stream a Single Document

javascript
const bulbasaur = magnetar.doc('pokedex/001')
// open the stream
bulbasaur.stream().catch((error) => {
  // the stream was closed because of an error
})

// ... some time later

// incoming data from the remote store will be
// added to your module's data and stay in sync

// bulbasaur.data ≈ { name: 'Bulbasaur' }

That's it! You don't need any other logic!!

Closing a Stream

You can close a stream again simply by executing closeStream on the same module:

javascript
const pokedexModule = magnetar.collection('pokedex')

// close the collection stream:
pokedexModule.closeStream()

// close the doc stream:
pokedexModule.doc('001').closeStream()

The Streaming Promise

You don't need to await a stream, because the promise won't resolve as long as its open!

js
// open the stream
await magnetar.collection('pokedex').stream()

// The code here will only get executed after the stream was closed!
console.log('closed!')

Instead, for a stream, it might be better to just use .then() and .catch()

js
// open the stream
magnetar
  .collection('pokedex')
  .stream()
  .then(() => {
    // the stream was closed via `closeStream()` !
  })
  .catch(() => {
    // the stream was closed because of an error! !
  })

// The code here will get executed immediately
console.log('The stream was opened!')

Query Data (filter, order by, limit...)

There are founr methods to query more specific data in a collection:

  • where
  • orderBy
  • limit
  • query

You can execute and chain these methods on collections to create a queried module that is just like a regular module but with your query applied.

When you apply a query it affects both the remote and cache stores:

  • If you make a fetch() call with a queried module, it will pass the queries to the remote store, which will make sure that your query is applied to the API call.
  • If you access module data with a queried module, the local cache store will also make sure that your query is applied to whatever data it returns.
js
const pokedexModule = magnetar.collection('pokedex')
const fireTypePokemon = pokedexModule.where('type', '==', 'fire')

await fireTypePokemon.fetch()

fireTypePokemon.data.values() // returns just the queried docs that were fetched
pokedexModule.data.values() // returns all docs fetched so far, including those fire types you just fetched

Where

where needs 3 parameters.

  1. the prop name
  2. the operator
  3. the value to compare with

The possible operators include:

  • '=='
  • '!='
  • '<'
  • '<='
  • '>'
  • '>='
  • 'array-contains'
  • 'array-contains-any'
  • 'in'
  • 'not-in'

Eg. "all Fire Pokemon above level 16"

js
magnetar
  .collection('pokedex')
  .where('type', '==', 'fire')
  .where('level', '>', 16)

For now read the Firestore documentation on Simple and Compound Queries. The concept is inspired by Firestore, but with Magnetar every cache and remote store plugin implements the proper logic to work with these kind of queries!

More Magnetar specific information on this will come soon.

Query

If you need OR in your where queries, you need to use .query:

js
magnetar
  .collection('pokedex')
  .query({
    or: [
      ['name', '==', 'flareon'],
      ['evolution', '==', 'flareon']
    ]
  })

You can also combine ors and ands:

js
magnetar
  .collection('pokedex')
  .query({
    or: [
      { and: [['type', '==', 'fire'], ['level', '>', 16]] },
      { and: [['type', '==', 'water'], ['level', '<', 16]] }
    ]
  })

Order by

You can order docs coming in by a specific field, ascending or descending. orderBy needs 2 parameters.

  1. the prop name to order by
  2. 'asc' or 'desc'

Eg. "all Pokemon sorted alphabetically"

js
magnetar
  .collection('pokedex')
  .orderBy('name', 'asc')

Limit

Limit is mainly for the remote store to limit the amount of records fetched at a time. limit needs 1 parameter.

  1. The count to limit by

Eg. "get the first 10 Pokemon sorted alphabetically"

js
magnetar
  .collection('pokedex')
  .orderBy('name', 'asc')
  .limit(10)

More Examples

You can combine where with orderBy:

js
magnetar
  .collection('pokedex')
  .where('type', '==', 'fire')
  .orderBy('name', 'asc')

You can either stream or fetch a queried module:

js
const fireTypePokemon = magnetar
  .collection('pokedex')
  .where('type', '==', 'fire')
  .orderBy('name', 'asc')

fireTypePokemon.fetch()
// or
fireTypePokemon.stream()

Here is a complete example for a search function:

javascript
const pokedexModule = magnetar.collection('pokedex')

/**
 * @param {string} type - the Pokemon Type that's being searched
 * @returns {Record<string, any>[]}
 */
async function searchPokemon(type) {
  const queriedPokedex = pokedexModule.where('type', '==', type).orderBy('name', 'asc')
  await queriedPokedex.fetch()

  // return all Pokemon for just this query
  return queriedPokedex.data.values()

  // OR
  // return all Pokemon fetched so far (eg. over multiple searches)
  // ↓
  // return pokedexModule.data.values()
}

Query Limitations

Based on your remote store plugin there might be limitations to what/how you can query data.

For Cloud Firestore be sure check the Query Limitations in their official documentation.

Opening & Closing Multiple Streams

You can have multiple streams open with different queries.

js
const pokedexModule = magnetar.collection('pokedex')

// open two streams with different filters:
pokedexModule.where('type', '==', 'fire').stream()
pokedexModule.where('type', '==', 'water').stream()

// access the documents from the two streams separately:
pokedexModule.where('type', '==', 'fire').data.values()
pokedexModule.where('type', '==', 'water').data.values()

// access all the documents at once:
pokedexModule.data.values()

// close the streams once by one:
pokedexModule.where('type', '==', 'fire').closeStream()
pokedexModule.where('type', '==', 'water').closeStream()

// close all streams at once:
pokedexModule.closeAllStreams()

Collection Groups (WIP)

This chapter is still being written

If you need to query data from multiple sub modules you can do so by using a Collection Group.

js
const waterAttacks = magnetar.collectionGroup(`pokedex/*/attacks`).where('type', '==', 'water')

waterAttacks.fetch()

This syntax is not fully implemented yet!