Giter Site home page Giter Site logo

nandorojo / swr-firestore Goto Github PK

View Code? Open in Web Editor NEW
769.0 18.0 65.0 2 MB

Implement Vercel's useSWR for querying Firestore in React/React Native/Expo apps. πŸ‘©β€πŸš’πŸ”₯

License: MIT License

JavaScript 0.56% TypeScript 98.20% Ruby 1.24%

swr-firestore's Introduction

SWR + Firestore

const { data } = useDocument('users/fernando')

It's that easy.

πŸ”₯ This library provides the hooks you need for querying Firestore, that you can actually use in production, on every screen.

⚑️ It aims to be the fastest way to use Firestore in a React app, both from a developer experience and app performance perspective.

πŸ• This library is built on top useSWR, meaning you get all of its awesome benefits out-of-the-box.

You can now fetch, add, and mutate Firestore data with zero boilerplate.

Features

  • Shared state / cache between collection and document queries (instead of Redux??)
  • Works with both React and React Native.
  • Offline mode with Expo (without detaching!)
  • Blazing fast
  • Query collection groups (new in 0.14.x!)
  • set, update, and add update your global cache, instantly
  • TypeScript-ready (see docs)
  • Realtime subscriptions (example)
  • Prevent memory leaks from Firestore subscriptions
  • No more parsing document.data() from Firestore requests
  • Server-side rendering (SSR or SSG) with Next.js (example)
  • Automatic date parsing (no more .toDate())
  • Firebase v8 support (see #59)

...along with the features touted by Vercel's incredible SWR library:

"With SWR, components will get a stream of data updates constantly and automatically. Thus, the UI will be always fast and reactive."

  • Transport and protocol agnostic data fetching
  • Fast page navigation
  • Revalidation on focus
  • Interval polling
  • Request deduplication
  • Local mutation
  • Pagination
  • TypeScript ready
  • SSR support
  • Suspense mode
  • Minimal API

⭐️

If you like this library, give it star and let me know on Twitter!

Installation

yarn add @nandorojo/swr-firestore

# or
npm install @nandorojo/swr-firestore

Install firebase:

# if you're using expo:
expo install firebase

# if you aren't using expo:
yarn add firebase
# or
npm i firebase

Set up

In the root of your app, create an instance of Fuego and pass it to the FuegoProvider.

If you're using Firebase v8, see this solution for creating your instance of Fuego.

If you're using next.js, this goes in your pages/_app.js file.

App.js

import React from 'react'
import 'firebase/firestore'
import 'firebase/auth'
import { Fuego, FuegoProvider } from '@nandorojo/swr-firestore'

const firebaseConfig = {
  // put yours here
}

const fuego = new Fuego(firebaseConfig)

export default function App() {
  return (
    <FuegoProvider fuego={fuego}>
      <YourAppHere />
    </FuegoProvider>
  )
}

Make sure to create your Fuego instance outside of the component. The only argument Fuego takes is your firebase config variable.

Under the hood, this step initializes firebase for you. No need to call firebase.initializeApp.

Basic Usage

Assuming you've already completed the setup...

Subscribe to a document

import React from 'react'
import { useDocument } from '@nandorojo/swr-firestore'
import { Text } from 'react-native'

export default function User() {
  const user = { id: 'Fernando' }
  const { data, update, error } = useDocument(`users/${user.id}`, {
    listen: true,
  })

  if (error) return <Text>Error!</Text>
  if (!data) return <Text>Loading...</Text>

  return <Text>Name: {data.name}</Text>
}

Get a collection

import React from 'react'
import { useCollection } from '@nandorojo/swr-firestore'
import { Text } from 'react-native'

export default function UserList() {
  const { data, update, error } = useCollection(`users`)

  if (error) return <Text>Error!</Text>
  if (!data) return <Text>Loading...</Text>

  return data.map(user => <Text key={user.id}>{user.name}</Text>)
}

useDocument accepts a document path as its first argument here. useCollection works similarly.

Simple examples

Query a users collection:

const { data } = useCollection('users')

Subscribe for real-time updates:

const { data } = useDocument(`users/${user.id}`, { listen: true })

Make a complex collection query:

const { data } = useCollection('users', {
  where: ['name', '==', 'fernando'],
  limit: 10,
  orderBy: ['age', 'desc'],
  listen: true,
})

Pass options from SWR to your document query:

// pass SWR options
const { data } = useDocument('albums/nothing-was-the-same', {
  shouldRetryOnError: false,
  onSuccess: console.log,
  loadingTimeout: 2000,
})

Pass options from SWR to your collection query:

// pass SWR options
const { data } = useCollection(
  'albums',
  {
    listen: true,
    // you can pass multiple where conditions if you want
    where: [
      ['artist', '==', 'Drake'],
      ['year', '==', '2020'],
    ],
  },
  {
    shouldRetryOnError: false,
    onSuccess: console.log,
    loadingTimeout: 2000,
  }
)

Add data to your collection:

const { data, add } = useCollection('albums', {
  where: ['artist', '==', 'Drake'],
})

const onPress = async () => {
  // calling this will automatically update your global cache & Firestore
  const documentId = await add({
    title: 'Dark Lane Demo Tapes',
    artist: 'Drake',
    year: '2020',
  })
}

Set document data:

const { data, set, update } = useDocument('albums/dark-lane-demo-tapes')

const onReleaseAlbum = () => {
  // calling this will automatically update your global cache & Firestore
  set(
    {
      released: true,
    },
    { merge: true }
  )

  // or you could call this:
  update({
    released: true,
  })
}

Use dynamic fields in a request:

If you pass null as the collection or document key, the request won't send.

Once the key is set to a string, the request will send.

Get list of users who have you in their friends list

import { useDoormanUser } from 'react-doorman'

const { uid } = useDoormanUser()
const { data } = useCollection(uid ? 'users' : null, {
  where: ['friends', 'array-contains', uid],
})

Get your favorite song

const me = { id: 'fernando' }

const { data: user } = useDocument<{ favoriteSong: string }>(`users/${me.id}`)

// only send the request once the user.favoriteSong exists!
const { data: song } = useDocument(
  user?.favoriteSong ? `songs/${user.favoriteSong}` : null
)

Parse date fields in your documents

Magically turn any Firestore timestamps into JS date objects! No more .toDate().

Imagine your user document schema looks like this:

type User = {
  name: string
  lastUpdated: {
    date: Date
  }
  createdAt: Date
}

In order to turn createdAt and lastUpdated.date into JS objects, just use the parseDates field:

In a document query

const { data } = useDocument<User>('user/fernando', {
  parseDates: ['createdAt', 'lastUpdated.date'],
})

let createdAt: Date
if (data) {
  // βœ… all good! it's a JS Date now.
  createdAt = data.createdAt
}

data.createdAt and data.lastUpdated.date are both JS dates now!

In a collection query

const { data } = useCollection<User>('user', {
  parseDates: ['createdAt', 'lastUpdated.date'],
})

if (data) {
  data.forEach(document => {
    document.createdAt // JS date!
  })
}

For more explanation on the dates, see issue #4.

Access a document's Firestore snapshot

If you set ignoreFirestoreDocumentSnapshotField to false, you can access the __snapshot field.

const { data } = useDocument('users/fernando', {
  ignoreFirestoreDocumentSnapshotField: false, // default: true
})

if (data) {
  const id = data?.__snapshot.id
}

You can do the same for useCollection and useCollectionGroup. The snapshot will be on each item in the data array.

This comes in handy when you are working with forms for data edits:

With Formik

const { data, set } = useDocument('users/fernando', {
  ignoreFirestoreDocumentSnapshotField: false,
})

if (!data) return <Loading />

<Formik
  initialValues={data.__snapshot.data()}
  ...
/>

With state and hooks

const { data, set } = useDocument('users/fernando', {
  ignoreFirestoreDocumentSnapshotField: false,
})

const [values, setValues] = useState(null);

useEffect(() => {
  if (data) {
    setValues(data.__snapshot.data());
  }
}, [data]);

Paginate a collection:

Video here.

import React from 'react'
import { fuego, useCollection } from '@nandorojo/swr-firestore'

const collection = 'dump'
const limit = 1
const orderBy = 'text'

export default function Paginate() {
  const { data, mutate } = useCollection<{ text: string }>(
    collection,
    {
      limit,
      orderBy,
      // 🚨 this is required to get access to the snapshot!
      ignoreFirestoreDocumentSnapshotField: false,
    },
    {
      // this lets us update the local cache + paginate without interruptions
      revalidateOnFocus: false,
      refreshWhenHidden: false,
      refreshWhenOffline: false,
      refreshInterval: 0,
    }
  )

  const paginate = async () => {
    if (!data?.length) return

    const ref = fuego.db.collection(collection)

    // get the snapshot of last document we have right now in our query
    const startAfterDocument = data[data.length - 1].__snapshot

    // get more documents, after the most recent one we have
    const moreDocs = await ref
      .orderBy(orderBy)
      .startAfter(startAfterDocument)
      .limit(limit)
      .get()
      .then(d => {
        const docs = []
        d.docs.forEach(doc => docs.push({ ...doc.data(), id: doc.id, __snapshot: doc }))
        return docs
      })

    // mutate our local cache, adding the docs we just added
    // set revalidate to false to prevent SWR from revalidating on its own
    mutate(state => [...state, ...moreDocs], false)
  }

  return data ? (
    <div>
      {data.map(({ id, text }) => (
        <div key={id}>{text}</div>
      ))}
      <button onClick={paginate}>paginate</button>
    </div>
  ) : (
    <div>Loading...</div>
  )
}

Query Documents

You'll rely on useDocument to query documents.

import React from 'react'
import { useDocument } from '@nandorojo/swr-firestore'

const user = { id: 'Fernando' }
export default () => {
  const { data, error } = useDocument(`users/${user.id}`)
}

If you want to set up a listener (or, in Firestore-speak, onSnapshot) just set listen to true.

const { data, error } = useDocument(`users/${user.id}`, { listen: true })

API

Imports

import {
  useDocument,
  useCollection,
  useCollectionGroup, // πŸ‘‹ new!
  revalidateDocument,
  revalidateCollection,
  // these all update BOTH Firestore & the local cache ⚑️
  set, // set a firestore document
  update, // update a firestore document
  fuego, // get the firebase instance used by this lib
  getCollection, // prefetch a collection, without being hooked into SWR or React
  getDocument, // prefetch a document, without being hooked into SWR or React
} from '@nandorojo/swr-firestore'

useDocument(path, options)

const {
  data,
  set,
  update,
  deleteDocument,
  error,
  isValidating,
  mutate,
  unsubscribe
} = useDocument(path, options)

Arguments

  • path required The unique document path for your Firestore document.
    • string | null. If null, the request will not be sent. This is useful if you want to get a user document, but the user ID hasn't loaded yet, for instance.
    • This follows the same pattern as the key argument in useSWR. See the SWR docs for more. Functions are not currently supported for this argument.
  • options (optional) A dictionary with added options for the query. Takes the folowing values:
    • listen = false: If true, sets up a listener for this document that updates whenever it changes.
    • You can also pass any of the options available from useSWR.
    • ignoreFirestoreDocumentSnapshotField = true. See elaboration below.
    • parseDates: An array of string keys that correspond to dates in your document. Example.
ignoreFirestoreDocumentSnapshotField

If true, docs returned in data will not include the firestore __snapshot field. If false, it will include a __snapshot field. This lets you access the document snapshot, but makes the document not JSON serializable.

By default, it ignores the __snapshot field. This makes it easier for newcomers to use JSON.stringify without weird errors. You must explicitly set it to false to use it.

// include the firestore document snapshots
const { data } = useDocument('users/fernando', {
  ignoreFirestoreDocumentSnapshotField: false,
})

if (data) {
  const path = data.__snapshot.ref.path
}

The __snapshot field is the exact snapshot returned by Firestore.

See Firestore's snapshot docs for more.

Return values

Returns a dictionary with the following values:

  • set(data, SetOptions?): Extends the firestore document set function.
    • You can call this when you want to edit your document.
    • It also updates the local cache using SWR's mutate. This will prove highly convenient over the regular Firestore set function.
    • The second argument is the same as the second argument for Firestore set.
  • update(data): Extends the Firestore document update function.
    • It also updates the local cache using SWR's mutate. This will prove highly convenient over the regular set function.
  • deleteDocument(): Extends the Firestore document delete function.
    • It also updates the local cache using SWR's mutate by deleting your document from this query and all collection queries that have fetched this document. This will prove highly convenient over the regular delete function from Firestore.
  • unsubscribe() A function that, when called, unsubscribes the Firestore listener.
    • The function can be null, so make sure to check that it exists before calling it.
    • Note: This is not necessary to use. useDocument already unmounts the listener for you. This is only intended if you want to unsubscribe on your own.

The dictionary also includes the following from useSWR:

  • data: data for the given key resolved by fetcher (or undefined if not loaded)
  • error: error thrown by fetcher (or undefined)
  • isValidating: if there's a request or revalidation loading
  • mutate(data?, shouldRevalidate?): function to mutate the cached data

useCollection(path, query, options)

const { data, add, error, isValidating, mutate, unsubscribe } = useCollection(
  path,
  query,
  options
)

Arguments

  • path required string, path to collection.
  • query optional dictionary with Firestore query details
  • options SWR options (see SWR docs)

path

path required The unique document path for your Firestore document.

  • string | null. If null, the request will not be sent. This is useful if you want to get a user document, but the user ID hasn't loaded yet, for instance.
  • This follows the same pattern as the key argument in useSWR. See the SWR docs for more. Functions are not currently supported for this argument.

query

(optional) Dictionary that accepts any of the following optional values:

  • listen = false: if true, will set up a real-time listener that automatically updates.
  • limit: number that limits the number of documents
  • where: filter documents by certain conditions based on their fields
  • orderBy: sort documents by their fields
  • startAt: number to start at
  • endAt: number to end at
  • startAfter: number to start after
  • endBefore: number to end before
  • ignoreFirestoreDocumentSnapshotField = true: If true, docs returned in data will not include the firestore __snapshot field. If false, it will include a __snapshot field. This lets you access the document snapshot, but makes the document not JSON serializable.
where

Can be an array, or an array of arrays.

Each array follows this outline: ['key', 'comparison-operator', 'value']. This is pulled directly from Firestore's where pattern.

// get all users whose names are Fernando
useCollection('users', {
  where: ['name', '==', 'Fernando'],
})

// get all users whose names are Fernando & who are hungry
useCollection('users', {
  where: [
    ['name', '==', 'Fernando'],
    ['isHungry', '==', true],
  ],
})

// get all users whose friends array contains Fernando
useCollection('users', {
  where: ['friends', 'array-contains', 'Fernando'],
})
orderBy

Can be a string, array, or an array of arrays.

Each array follows this outline: ['key', 'desc' | 'asc']. This is pulled directly from Firestore's orderBy pattern.

// get users, ordered by name
useCollection('users', {
  orderBy: 'name',
})

// get users, ordered by name in descending order
useCollection('users', {
  orderBy: ['name', 'desc'],
})

// get users, ordered by name in descending order & hunger in ascending order
useCollection('users', {
  orderBy: [
    ['name', 'desc'], //
    ['isHungry', 'asc'],
  ],
})
ignoreFirestoreDocumentSnapshotField

If true, docs returned in data will not include the firestore __snapshot field. If false, it will include a __snapshot field. This lets you access the document snapshot, but makes the document not JSON serializable.

By default, it ignores the __snapshot field. This makes it easier for newcomers to use JSON.stringify without weird errors. You must explicitly set it to false to use it.

// include the firestore document snapshots
const { data } = useCollection('users', {
  ignoreFirestoreDocumentSnapshotField: false,
})

if (data) {
  data.forEach(document => {
    const path = document?.__snapshot.ref.path
  })
}

The __snapshot field is the exact snapshot returned by Firestore.

See Firestore's snapshot docs for more.

options

(optional) A dictionary with added options for the request. See the options available from SWR.

Return values

Returns a dictionary with the following values:

  • add(data): Extends the Firestore document add function. Returns the added document ID(s).
    • It also updates the local cache using SWR's mutate. This will prove highly convenient over the regular add function provided by Firestore.

The returned dictionary also includes the following from useSWR:

  • data: data for the given key resolved by fetcher (or undefined if not loaded)
  • error: error thrown by fetcher (or undefined)
  • isValidating: if there's a request or revalidation loading
  • mutate(data?, shouldRevalidate?): function to mutate the cached data
  • unsubscribe() A function that, when called, unsubscribes the Firestore listener.
    • The function can be null, so make sure to check that it exists before calling it.
    • Note: This is not necessary to use. useCollection already unmounts the listener for you. This is only intended if you want to unsubscribe on your own.

useCollectionGroup(path, query, options)

Follows an identical API as useCollection, except that it leverages Firestore's collection group query for merging subcollections with the same name.

To see how to use it, follow the instructions from useCollection.

See the Firestore docs on collecttion groups to learn more.

set(path, data, SetOptions?)

Extends the firestore document set function.

  • You can call this when you want to edit your document.
  • It also updates the local cache using SWR's mutate. This will prove highly convenient over the regular Firestore set function.
  • The second argument is the same as the second argument for Firestore set.

This is useful if you want to set a document in a component that isn't connected to the useDocument hook.

update(path, data):

Extends the Firestore document update function.

  • It also updates the local cache using SWR's mutate. This will prove highly convenient over the regular set function.

This is useful if you want to update a document in a component that isn't connected to the useDocument hook.

deleteDocument(path, ignoreLocalMutations = false)

Extends the Firestore document delete function.

  • It also updates the local cache using SWR's mutate by deleting your document from this query and all collection queries that have fetched this document. This will prove highly convenient over the regular delete function from Firestore.
  • Second argument is a boolean that defaults to false. If true, it will not update the local cache, and instead only send delete to Firestore.

revalidateDocument(path)

Refetch a document from Firestore, and update the local cache. Useful if you want to update a given document without calling the connected revalidate function from use useDocument hook.

  • Only argument is the Firestore document path (ex: users/Fernando)

revalidateCollection(path)

Refetch a collection query from Firestore, and update the local cache. Useful if you want to update a given collection without calling the connected revalidate function from use useCollection hook.

  • Only argument is the Firestore document path (ex: users)
  • Note Calling revalidateCollection will update all collection queries. If you're paginating data for a given collection, you probably won't want to use this function for that collection.

fuego

The current firebase instance used by this library. Exports the following fields:

  • db: the current firestore collection instance
  • auth: the firebase.auth variable.
import { fuego } from '@nandorojo/swr-firestore'

fuego.db.doc('users/Fernando').get()

fuego.auth().currentUser?.uid

getDocument(path, options?)

If you don't want to use useDocument in a component, you can use this function outside of the React scope.

Arguments

  • path required The unique document path for your Firestore document.
  • options
    • ignoreFirestoreDocumentSnapshotField = true. If false, it will return a __snapshot field too.
    • parseDates: An array of string keys that correspond to dates in your document. Example.

Returns

A promise with the firestore doc and some useful fields. See the useDocument data return type for more info.

getCollection(path, query?, options?)

If you don't want to use useCollection in a component, you can use this function outside of the React scope.

Arguments

  • path required The unique collection path for your Firestore collection.
    • ignoreFirestoreDocumentSnapshotField = true. If false, it will return a __snapshot field too.
    • parseDates: An array of string keys that correspond to dates in your document. Example.
  • query refer to the second argument of useCollection.
  • options
    • ignoreFirestoreDocumentSnapshotField = true. If false, it will return a __snapshot field too in each document.
    • parseDates: An array of string keys that correspond to dates in your documents. Example.

Features

TypeScript Support

Create a model for your typescript types, and pass it as a generic to useDocument or useCollection.

useDocument

The data item will include your TypeScript model (or null), and will also include an id string, an exists boolean, and hasPendingWrites boolean.

type User = {
  name: string
}

const { data } = useDocument<User>('users/fernando')

if (data) {
  const {
    id, // string
    name, // string
    exists, // boolean
    hasPendingWrites, // boolean
  } = data
}

const id = data?.id //  string | undefined
const name = data?.name // string | undefined
const exists = data?.exists // boolean | undefined
const hasPendingWrites = data?.hasPendingWrites // boolean | undefind

useCollection

The data item will include your TypeScript model (or null), and will also include an id string.

type User = {
  name: string
}

const { data } = useCollection<User>('users')

if (data) {
  data.forEach(({ id, name }) => {
    // ...
  })
}

Shared global state between documents and collections

A great feature of this library is shared data between documents and collections. Until now, this could only be achieved with something like a verbose Redux set up.

So, what does this mean exactly?

Simply put, any documents pulled from a Firestore request will update the global cache.

To make it clear, let's look at an example.

Imagine you query a user document from Firestore:

const { data } = useDocument('users/fernando')

And pretend that this document's data returns the following:

{ "id": "fernando", "isHungry": false }

Remember that isHungry is false here ^

Now, let's say you query the users collection anywhere else in your app:

const { data } = useCollection('users')

And pretend that this collection's data returns the following:

[
  { "id": "fernando", "isHungry": true },
  {
    //...
  }
]

Whoa, isHungry is now true. But what happens to the original document query? Will we have stale data?

Answer: It will automatically re-render with the new data!

swr-firestore uses document id fields to sync any collection queries with existing document queries across your app.

That means that if you somehow fetch the same document twice, the latest version will update everywhere.

License

MIT

swr-firestore's People

Contributors

dependabot[bot] avatar dsernst avatar estebanrao avatar jckw avatar jlmodell avatar kdonovan avatar naimdasb avatar nandorojo avatar praneybehl avatar wayfarerboy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swr-firestore's Issues

Is `useCollection` passing along request.auth to Firestore?

Hi @nandorojo,

I've got a curious situation.

I'm getting unnecessary permissions error trying to read from a subcollection of my users collection.

My Firestore database rules are set to allow reads on any subcollection of collection users IF
request.auth.uid == resource.data.createdBy;

Rule

match /users/{document=**} {
      allow read: if request.auth.uid == resource.data.createdBy;
...
}

useDocument(`users/${userId}`)
Works fine.

However this does not:
useCollection(`users/${userId}/subcollection/{someID}`)

It Throws FirebaseError: Missing or insufficient permissions.

So I'm wondering if perhaps useCollection is not passing along the request.auth to the firebase request for some reason?

Cheers

Export parameter types & return types for hooks

I've had a couple of instances of creating custom hooks that use useDocument() or useCollection() under the hood. I often would like to use the the parameters that these functions take and even the return type of them.

Because of how these options are typed it makes it hard for me to reuse them:

export declare const useCollection: <Data extends object = {}, Doc extends {
    id: string;
} = Document<Data>>(path: string | null, query?: Ref<Data> & {
    /**
     * If `true`, sets up a real-time subscription to the Firestore backend.
     *
     * Default: `false`
     */
    listen?: boolean | undefined;
    /**
     * An array of key strings that indicate where there will be dates in the document.
     *
     * Example: if your dates are in the `lastUpdated` and `user.createdAt` fields, then pass `{parseDates: ["lastUpdated", "user.createdAt"]}`.
     *
     * This will automatically turn all Firestore dates into JS Date objects, removing the need to do `.toDate()` on your dates.
     */
    parseDates?: (string | keyof Doc)[] | undefined;
    /**
     * Use the `useCollectionGroup` hook instead of this.
     */
    __unstableCollectionGroup?: boolean | undefined;
}
...

It would be awesome If I could achieve something like this:

import { fuego, useCollection, CollectionQueryType } from '@nandorojo/swr-firestore';
...
export const useLinksInList = (
  listId: string,
  { listen = true, where, orderBy }:  Pick<CollectionQueryType<Link>, 'listen' | 'where' | 'orderBy'>,
) => {
  const result = useCollection<Link>(
    `${LISTS_COLLECTION_NAME}/${listId}/${LINKS_COLLECTION_NAME}`,
    { listen, where, orderBy },
  );
  if (result.error || (result.loading === false && result.data == null)) {
    throw result.error ?? new Error('Data for firebase collection null or undefined');
  }
  return { ...result, links: result.data ?? [] };
};

instead of requiring that I redefine these types within my project and requiring a function definition that looks like this:

declare type OrderByArray<Doc extends object = {}, Key = keyof Doc> = [
  Key | firebase.firestore.FieldPath,
  firebase.firestore.OrderByDirection,
];
declare type OrderByItem<Doc extends object = {}, Key = keyof Doc> = OrderByArray<Doc> | Key;
declare type OrderByType<Doc extends object = {}> = OrderByItem<Doc> | OrderByArray<Doc>[];
declare type WhereItem<Doc extends object = {}, Key = keyof Doc> = [
  Key | firebase.firestore.FieldPath,
  firebase.firestore.WhereFilterOp,
  unknown,
];
declare type WhereArray<Doc extends object = {}> = WhereItem<Doc>[];
declare type WhereType<Doc extends object = {}> = WhereItem<Doc> | WhereArray<Doc>;

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const useLinksInList = (
  listId: string,
  { listen = true, where, orderBy }: { listen?: boolean; where?: WhereType<Link>; orderBy?: OrderByType<Link> },
) => {
const result = useCollection<Link>(
    `${LISTS_COLLECTION_NAME}/${listId}/${LINKS_COLLECTION_NAME}`,
    { listen, where },
  );
  if (result.error || (result.loading === false && result.data == null)) {
    throw result.error ?? new Error('Data for firebase collection null or undefined');
  }
  return { ...result, links: result.data ?? [] };
}

dependent fetches throw typeError

Hey I'm really enjoying the lib.

I've noticed a typeError being thrown out from {error} = useDocument... whenever I setup up dependent fetches. ie:

const { data: data1, error: error1 } = useDocument('/xxx/doc')
const { data: data2, error: error2 } = useDocument(`/yyy/${data1.prop}`)

When the component loads, error2 returns undefined a few times before starting to throw:

TypeError: Cannot read property 'db' of undefined
    at _callee$ (use-swr-document.js?a80b:1)
    at tryCatch (runtime.js?96cf:45)

It finally does fetch the data, but it this doesn't look like an error you'd expect such as "firestore path does not exist" for example.

πŸ”₯Hot stuff my dude!

Very nice,

I was just about to rig up my own contraption to get the dependent fetching of swr and the subscription power of firestore in a simple hook...
But I thought I might google firestore swr before starting on this... and BOOM, I find you.

my man! mad props

Add out-of-the-box date support

  1. Add support for .toDate() by recursively checking an object for any server dates and converting them into Date objects.
  2. By default, enable the serverTimestamp field (I forget what the field called, this relates to onSnapshot subscriptions creating pseudo timestamps before the local stuff hits the backend.)

exists, id, hasPendingWrites warning from useDocument

I call useDocument to get some data: const { data } = useDocument<Items>("items/123");.
I get the following warning: [get-document] warning: Your document, 123 is using one of the following reserved fields: [exists, id, hasPendingWrites]. These fields are reserved. Please remove them from your documents.

The actual firestore document doesn't contain these fields.
What do I do wrong ? How can I get the actual saved document ?

Mutate corresponding collections/documents

Why

No more Redux reducer jumble for dealing with globally-shared Firestore data. Enough is enough.

What

If we mutate a certain document, say users/fernando, and we have an SWR query for the collection users, then we should also mutate that collection.

It would be great to detect in documents what their collection is, and mutate the document for that path. This means full in-sync Firestore data across the app.

The same should go for updating collection -> documents. If you fetch / listen to a collection, and it updates, it should mutate every document with that path, with shouldRevalidate set to false.

Roadblocks

This would all require that the useSWR(key) key value is restricted to only the path. By including the query object here too, we can't perfectly sync data. The problem is that, if we don't update the query in the key, then changes to the query will not retrigger listeners/fetches, rendering the query stale. This is a fundamental problem to figure out.

Does having a function key solve this?

Update I'm tackling this in a subsequent comment.

Suggestions

Might have to add an issue on SWR, which tells it that the key "changed", even though it's arguments have stayed the same. For instance, even though we might be listening to the same collection, we now have a different set of query options, so we want it to "re-validate".

Likely solution

Run a useEffect hook after useSWR, which listens for changes in queryString, listen, etc. And if it changes, run revalidate, returned by SWR. That should do it! Just make sure that the shouldListen ref and query ref are in a useEffect before useSWR.

`delete` support

Thanks for this AWESOME πŸ”₯πŸ”₯πŸ”₯ project!!!

At the moment, set and update seems to be supported, do you have any plans for delete support as well?

Why is { mutate } not bound to the key by default?

Hey me again :)

I noticed that mutate in this scenario

const { mutate } = useDocument('/foo/bar')

is not bound to the '/foo/bar' document cache the way SWR binds it.
With SWR there is a global mutate that you can import from the lib which you have to pass a key to as first arg. But if you grab mutate from a specific useSWR invocation it is auto-bound to the cache key.

Just wondering if you had reasons for doing it this way :)

cheers

Replace lodash with individual function packages

It's great package by the way. Currently it uses lodash as a dependency and thus imports the whole package πŸ“¦, although it just uses get & set methods from lodash.

I've added a PR #34 to fix this. It would help bring the package bundle size by ~60%

Hope it helps.

Cheers!

useCollection nested field issue for where clause

Hello,

I have the following code to perform a lookup of all places based on an organisation id that gets passed in but would like to use fields of a nested object (rather than a top level field):

interface Place {
  name:string;
  description:string;
  organisation: {
    id: string;
   name: string;
  }
}

export default function Organisation({ props }) {
  const { data, error } = useCollection<Place>(`places`, { 
    where: ['organisation.id', '==', orgId]
  });

This code does run, however it throws a warning in my IDE:

Type '"organisation.id"' is not assignable to type '"name" | "description" | "organisation" | WhereItem<Place, "name" | "description" | "organisation"> | FieldPath'.

Can you update the WhereType (or what is needed) to allow it to use nested properties?

Regards,

Martin

Issue with refetching data when auth changes

I'm using this with react-redux-firebase and having an issue once the user logs in.

The rules in the firestore is set to only allow authenticated users to access the resource. Non users cannot see the data initially, and one would expect that once they login and get redirected back to the page, it would fetch the new data. However, that does not seem to be the case, since I have to do a proper page reload in order for the data to show. It's like it is not performing another network fetch when I re-render the component,

Also, even if I go to another page and then back (which will re-render the page and run the hook), the data does not show.

Am I doing something wrong? I would expect that the hook would re-fetch the data every time the hook runs (cache and network strategy)? If I use react-redux-firebase method of fetching data, it is synced perfectly fine.

Here's the snippet of code I am using

  const { data: shares, add, revalidate } = useCollection<{ name: string }>(
    "share",
    {
      listen: true,
    }
  );

And to login:

  const loginWithGoogle = () => {
    return firebase.login({ provider: "google", type: "popup" });
  };

The same issue applies when logging out as well, however in that case I want to clear the entire cache on logout. Using cache.clear() from swr does not seem to work either.

Usage of @react-native-firebase/firestore

I came across your library because I'm looking for something to bridge the gap between usage of firestore in react & in react native.

I see two different packages published on npmjs.com:

The second one appears to depend on @react-native-firebase/firestore.

I have 2 questions:

  1. Am I correct in assuming that this library only makes use of the js firebase sdk even when used in react native?
  2. @Lucas-Geitner, where is the code hosted for your package? I see that it has the dependency that I want but see no reference made to it within this repo. (It looks like your package.json still points towards this github repo even though the source seems to differ).

Something wrong with setup?

I really like the idea of this project, but ran into issues right away. With the following setup code in my nextJS ("next": "9.1.5",) project, I am getting the below error when trying to initialize.

import React from 'react'
import App from 'next/app'
import { Fuego, FuegoProvider } from '@nandorojo/swr-firestore'
import prod from "../credentials/prod.json"

const fuego = new Fuego(prod)

class MyApp extends App {
    render() {
        const { Component, pageProps } = this.props
        return (
            <Component {...pageProps} />
        )
    }
}

export default <FuegoProvider fuego={fuego}>{ <MyApp/> }</FuegoProvider>`

"Error: The default export is not a React Component in page: "/_app""

With the last row changed to:

export default function LaApp() { return <FuegoProvider fuego={fuego}><MyApp/></FuegoProvider> }

I am instead getting this error:

"Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports."

Ideas of what could be wrong?

Too slow to get document with Next.js

Hi! I'm using your library with Next.js.
Today, I started to using your library, but I've found an issue.
See below.

When using swr

import React from 'react'
import { NextPage } from 'next'
import firebase from 'firebase/app'
import useSWR from 'swr'

const SWR: NextPage = () => {
  const fetcher = () =>
    firebase
      .firestore()
      .collection('dump')
      .doc('tE55NEOFZ2pe0nm1nOhs')
      .get()
      .then((doc) => doc.data() as { text: string })

  const { data } = useSWR<{ text: string }>('swr-demo', fetcher)

  return (
    <div>
      <p>swr:</p>
      {data ? <p>{data?.text}</p> : <p>Loading...</p>}
    </div>
  )
}

export default SWR

swr

No problem.

When using swr-firestore

import React from 'react'
import { NextPage } from 'next'
import { useDocument } from '@nandorojo/swr-firestore'

const firestoreSWR: NextPage = () => {
  const { data } = useDocument<{ text: string }>('dump/tE55NEOFZ2pe0nm1nOhs')

  return (
    <div>
      <p>swr-firestore:</p>
      {data ? <p>{data?.text}</p> : <p>Loading...</p>}
    </div>
  )
}

export default firestoreSWR

swr-firestore

Too slow...

Thanks.

How to use pagination?

Hi! It's a great library to use Firestore with React.

Do you have a feature to use pagination for infinite scroll?

Specify custom id in `useDocument`

Is there a correct way to specify a custom id in useDocument to coincide with the set option?

Right now, it gives me a

Invalid document reference. Document references must have an even number of segments, but spaces has 1

error.

Sample code;

  const [spaceId, setSpaceId] = React.useState<string | null>(null);

  const {set} = useDocument(
    spaceId ? `spaces/${spaceId}` : null,
  );
  setSpaceId("myNewSpaceId");
  set({data})

Usage alongside Firebase Storage

I would like to use swr-firestore in an app where I also have downloads from Firebase Storage. Since swr-firestore executes firebase.initializeApp(), is this possible? I will be testing out what happens if I try to add import 'firebase/storage' to my App component

Bug with useCollection and where by timeStamp.

Love using this lib so far. I am trying to make a paginated list of messages for a messaging app, and I think I stumbled on a bug. Because of the JSON serialization / maybe something else in useCollection, where: ['time', '<', new Data(Date.now())] doesn't return anything, but a regular get on a collection ref does.

const collectionPath = `path/to/collection`
const orderBy = ['time', 'desc']

const currentDate = new Date(Date.now())

fuego.db
  .collection(collectionPath)
  .orderBy('time', 'desc')
  .where('time', '<', currentDate)
  .get()
  .then((data) => console.log(data.empty)) // data.empty === false

const { data } = useCollection(
  collectionPath,
  {
    orderBy: ['time', 'desc'],
    where: ['time', '<', currentDate],
    listen: true,
  },
)

// data always is empty array.

[React Native/ Expo only] Add offline support πŸ‘€

This issue only applies to mobile apps built with Expo. Firestore persistence works fine with this library with normal React.

Data should cache to AsyncStorage something like redux persist, and any mutations should somehow bubble up to happen later...πŸ™ƒ

If anyone stumbles upon this, would love help.

Disable default deduping for listeners

Deduping when listen is true can have unintended consequences, where listener 1 can be unmounted when listener 2 was deduped but still expects updates.

Why use @firebase/firestore-types?

Seems like it is an opportunity for the @firebase/firestore-types & firebase libraries to get out of sync with each other.

Did you ever think about importing the types from the firebase library instead?
image

Server Side Generation with Next.js support

Hey!

Is there anyway to support server side fetching of data so that the site can be populated and rendered on the server? This is for a Next.js SSG site for better SEO.

Thanks a lot!

PS: This library is a blessing!! Thanks for sharing it!

Chaining Requests

Hi,

I am trying to do 2 requests in my component but it seems like that the 2nd request is never succeeding.

const { data, error } = useCollection("paths", {
    where: ["slug", "==", slug],
  });

  const path = data && data[0];
  const { creator, isLoadingCreator, isCreatorError } = useDocument(path?.author ? `authors/${path.author}` : null);

return (
          <Box>
            <PathMetaData path={path} creator={creator}/>
          </Box>
)

If I mount my 2nd request in the child component PathMetaData, it works. However, I need to do it at this level as it is needed by another child component.

What am I missing? I have tried https://swr.vercel.app/docs/conditional-fetching as well,

const { creator, isLoadingCreator, isCreatorError } = useDocument(() => `authors/${path.author}`);

but no luck.

Module 'swr-firestore' has no exported member 'add'.

Readme describes an exported function add:

and

image

But trying to use it gives me this Typescript error:

image

Just to be safe, I tried using it anyway β€” in case just the type definition was missing β€” but this still led to a runtime error: TypeError: undefined is not a function.

Am I doing something wrong?

I tried updating to the latest npm version @nandorojo/[email protected], but no luck.

update() typings don't accept FieldValues

πŸ”₯ This library is fantastic! Great work.

Noticed a minor typescript issue:

import { useDocument } from '@nandorojo/swr-firestore'
import { useState } from 'react'
import { firestore } from 'firebase'

type Pageviews = { views: number }

export default function ViewCounter() {
  const { data, update, error } = useDocument<Pageviews>(`meta/pageviews`, { listen: true })
  const [incremented, setIncremented] = useState(false)

  if (error) return console.error('error loading page views', error)

  if (data && !incremented) {
    update({ views: firestore.FieldValue.increment(1) })
    setIncremented(true)
  }

  return <p>{data?.views} views</p>
}

The line:

    update({ views: firestore.FieldValue.increment(1) })

throws the typescript error:

Type 'FieldValue' is not assignable to type 'number'.  ts(2322)

The expected type comes from property 'views' which is declared here on type 'Partial<Pageviews>'",

Have any utils for merging sub collections and other root level collections?

https://firebase.google.com/docs/firestore/manage-data/structure-data#root-level_collections

Screen Shot 2020-10-12 at 11 02 08 AM

Is there a util or helper to query rooms collection with from field as users document joined?

const rooms = useCollection('rooms').(with something)

rooms.data = [{
  id: 'software',
  messages: [{
    id: 'message1',
    from: {
      id: 'alovelace',
      first: 'Ada',
      last: 'Lovelace',
      born: 1815
    }
  }]
}]

Or should I fetch each data separately and loop & merge manually?

Add Firestore ref to useDocument and useCollection.

What

It would be useful to be able to access the original Firestore ref object for each document.

For documents:

const { data } = useDocument('users/fernando')

const onPress = () => {
  data?._ref.set({ hasFreeWill: false }, { merge: true })
}

For collections:

const { data } = useCollection('users')

if (data) {
  data.forEach((document) => {
    document._ref // Firestore ref
  })
}

Why

It's kind of up to the user. Never know when you'll need this.

A good example is paginating: in order to do startAfter, you have to pass a document snapshot (see #7 (comment)). Could you do this with just the ref?

  • It seems not. Maybe instead of _ref, or in addition, we also pass the Firestore _snapshot object? I'll look into it

Opt-out

Maybe there could be an API to opt-out:

const { data } = useDocument('user/fernando', { excludeRefField: true })

This would be useful if you need the value to be JSON serializable. Not sure if this should be default enabled or not.

support for FirstoreDataConverter

I'm curious if you have thought about adding support for withConverter by allowing users of this libary to provide their own type converters?

It might enable usage such as this:

const postConverter = {
  toFirestore(post: Post): firebase.firestore.DocumentData {
    return {title: post.title, author: post.author};
  },
  fromFirestore(
    snapshot: firebase.firestore.QueryDocumentSnapshot,
    options: firebase.firestore.SnapshotOptions
  ): Post {
    const data = snapshot.data(options)!;
    if(data.title == null || data.author == null) {
      throw new Error('Data from firestore malformed');
    }
    return new Post(data.title, data.author);
  }
};
...
const {error, data}= useCollection('posts', {converter: postConverter});

where data ends up being Post[] and add only accepts objects of type Post (that are then run through the converter)

https://firebase.google.com/docs/reference/js/firebase.firestore.FirestoreDataConverter

Reserved keyword "id" and how to work with it?

For my data models I often have "id" as a field in my documents. I assume this is a common thing (I have no stats to back this up tho). Is there or would you consider a way to transform "id" from the document into another name with some configurable option as to avoid this conflict? Alternatively, is it possible to configure the library to itself look into a field other than id?

Compound queries

Hi, awesome lib!

I am using it with RN with some geohashing like this:

  const { data } = useCollection<IBusiness>('businesses', {
    where: [
      ['geohash', '>=', range.lower],
      ['geohash', '<=', range.upper],
    ],
  });

But when I add 1 more where item, I get too many rerenders error and sometimes undefined.

  const { data } = useCollection<IBusiness>('businesses', {
    where: [
      ['geohash', '>=', range.lower],
      ['geohash', '<=', range.upper],
      ['type', '==', 'Delivery'],
    ],
  });

Am I doing it correctly or am I missing something?

Prefetching queries

API for prefetching outside of rendering would be awesome. For example, if I have some critical data that makes no sense to show anything before it loads, I would load it before calling ReactDOM.render. RactQuery has support for this, take a look.

import { queryCache } from 'react-query'
 
const prefetchTodos = async () => {
    const queryData = await queryCache.prefetchQuery('todos', () => fetch('/todos'))
    // The results of this query will be cached like a normal query
}

get multiple collection

hello @nandorojo , thanks for working Great Projects.

But I have questions, can we get multiple collection ?

example:
I have an collection = ['LORM', 'KIND'];
In the same time I will get all data from above collection and grouping in one array, the function look like this :

collection.forEach((item: string) => {
const { data } = useCollection(${item}, { listen: true });
console.log('data inside map', data);
});

but it not work, can you suggest me the right implementations?
Thanks

Unsubscribing from a document listener

I am struggling to find documentation on how to unsubscribe a listener. It seems the unsubscribe functions is not returned when using useDocument for example. Any way to achieve this today or is this an improvement that needs to be made? If it is, I am happy to take a crack at it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.