Giter Club home page Giter Club logo

swr-firestore's Introduction

SWR + Firestore

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

This is the fork of swr-firestore with support for Firebase Modular SDK (v9) and improvements.

Breaking changes with recent versions.

The old version of this library, swr-firestore-v9, has been deprecated. Please upgrade to the new package, @lemasc/swr-firestore, and follow the migrations as explained in CHANGELOG.md. Also see the new examples below.

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. See the ongoing discussion.
  • 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())

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

Installation

yarn add @lemasc/swr-firestore

# or
npm install @lemasc/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

This library is tree-shakable. Means that if parts of the library that doesn't import to your code won't be included in your bundle too.

Setup

This library expects you to intialize Firebase app with your own. This depends on your Javascript framework. @lemasc/swr-firestore will automatically use your firebase [DEFAULT] app instance for initializing firestore.

Basic Usage

Subscribe to a document

import React from 'react'
import { useDocument } from '@lemasc/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 '@lemasc/swr-firestore'
import { Text } from 'react-native'

export default function UserList() {
  const { data, 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:

Notice that we are importing from @lemasc/swr-firestore/constraints! For more information, see Query Constraints for more information.

import {
  where,
  limit,
  orderBy
} from "@lemasc/swr-firestore/constraints"

const { data } = useCollection('users', {
  constraints: [
    where('name', '==', 'fernando'),
    orderBy('age', 'desc'),
    limit(10),
  ],
  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',
  {
    constraints: [
      where('artist', '==', 'Drake'),
      where('year', '==', '2020'),
    ],
  },
  {
    listen: true,
    shouldRetryOnError: false,
    onSuccess: console.log,
    loadingTimeout: 2000,
  }
)

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

Query Documents

You'll rely on useDocument to query documents.

import React from 'react'
import { useDocument } from '@lemasc/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

See API. (Haven't updated to v2 yet!)

Features

First-class TypeScript support

Create a model for your typescript types, and pass it as a generic to useDocument or useCollection. The data item returned from the library will automatically be type-safe.

When you first read your document data, properties in your model will not exist, as it hasn't checked as valid yet. Accessing it will throw a TypeError.

To make properties on your model accessible, you must check if the document is exists and validated.

import { useDocument } from '@lemasc/swr-firestore'

type User = {
  name: string
}

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

if (!data) {
  // The data hasn't been fetched by the hook yet. Check your SWR key.
  return null;
}

const id = data.id // string
const exists = data.exists // boolean
const validated = data.validated // boolean
const hasPendingWrites = data.hasPendingWrites // boolean

const name = data.name // error: Propery 'name' doesn't existed on type 'Document<User>'

if (data.exists && data.validated) {
  const name = data.name // string, the property is now accessible.
}

You can check this by your own, or you can use the utility function isDocumentValid for more convenience.

import {
  isDocumentValid,
  useDocument 
  } from '@lemasc/swr-firestore'

type User = {
  name: string
}

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

if (data) {
  // โŒ DON'T DO THIS!
  data.forEach(({ id, name }) => {
    // error: Property 'name' doesn't exist on type `Document<User>`.
  });

  // โœ… Instead, do this.
  data.filter(isDocumentValid).forEach(({ id, name }) => {
    // ...
  })
}

Validate using the schema validator.

For advanced use cases, you might have a document schema using the library of your choice, and you would like to validate it against the schema.

@lemasc/swr-firestore allows you to validate the current document data, transform if necessary, and returned back the corrected one. This is a good practice. You define your document schema, and prevent incompleted documents from breaking your applications.

TypeScript typings will also inferred automatically, if possible.

If validator returns an object, then the object will be used as a document data. If a falsy value or error was returned, the document data will be undefined.

/// You can use any library of your choice.
import { z } from "zod"

const User = z.object({
  username: z.string(),
});

const { data } = useCollection("users", undefined, {
  listen: true,
  validator: async (data) => {
    return User.parse(data)
  }
})
// User collections is now validated against the schema.

To check if the document is valid, you can check the validated prop, or use the utility function isDocumentValid as shown earlier.

Query Constraints

useCollection accepts the constraints to be use in your query. However, Firestore constraints function aren't type-check by default. This cause lack of types when query by field, such as using where or orderBy.

@lemasc/swr-firestore/constraints contains drop in query constraints that can be use existing Firestore queries, and also provide type-check when using the useCollection hook automatically.

import {
  where,
  limit,
  orderBy
} from "@lemasc/swr-firestore/constraints"

type User = {
  username: string
}

// Pass your model to 'useCollection'
const { data } = useCollection<User>('users', {
  constraints: [
    where('name', '==', 'fernando'), // โœ…
    where('somefield' ,'==', 'not exists') // โŒ error!
    limit(10),
  ],
  listen: true,
})

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

Watchers

 avatar

swr-firestore's Issues

'CollectionQueryType' is not exported from './use-swr-collection'

Hey!

First of all - thanks for your v9 port of swr-firestore!

I get the following error in my next.js typescript setup when importing the useCollection hook from swr-firestore-v9.

error - ../node_modules/swr-firestore-v9/lib/module/hooks/index.js
Attempted import error: 'CollectionQueryType' is not exported from './use-swr-collection'.

I'm not too familiar with the correct typescript setup in npm packages but mustn't types like CollectionQueryType included in the lib/module folder?

Or is there something wrong with my project configuration?

Everything works fine when importing the files from swr-firestore-v9/lib/commonjs but I'm loosing the types when doing so.

Hello sir, I'm confused how to use SSG or SSR in our swr-firestore-v9

I have read nandorojo#17.

but this link is swr-firestore, not swr-firestore-v9, it doesn't work.

Here is my code bello
const { data } = useCollection<AvailableOptionObject>( "artists", { // listen: true, orderBy: ["label", "asc"], } );

what i want is very simple, just server request the data and get the data, not client.

If there are any help or suggestions for me, I will be grateful.

Thanks for your great work, thanks for your help

How to use getFuego() in lieu of fuego

Hello,

I am struggling with the change from fuego to getFuego(). What is the proper way to re-write this statement?

const ref = fuego.db.collection(collection)

This is taken from the Paginate a collection example in the readme.

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.