reactfire icon indicating copy to clipboard operation
reactfire copied to clipboard

Add an option to use local Firebase emulators

Open hubgit opened this issue 5 years ago • 6 comments

After creating an app with react-scripts, initialising Firebase and installing ReactFire, there are still quite a few steps needed to be able to develop the app using the local Firebase emulators:

  1. Run firebase emulators:start to start the emulators.
  2. Add "proxy": "http://localhost:5000" to package.json so that requests for /__/firebase/init.json are proxied to the hosting emulator.
  3. Load the config from the hosting emulator, then preload and configure the rest of the emulators, before rendering the app:
import firebase from 'firebase/app'
import ReactDOM from 'react-dom'
import {
  FirebaseAppProvider,
  preloadAuth,
  preloadFirestore,
  preloadFunctions,
} from 'reactfire'

const AUTH_DOMAIN = 'example.com'
const FUNCTIONS_REGION = 'europe-west2'

fetch('/__/firebase/init.json').then(async (response) => {
  const firebaseConfig = await response.json()

  if (process.env.NODE_ENV === 'production') {
    firebaseConfig.authDomain = AUTH_DOMAIN
  }

  const firebaseApp = firebase.initializeApp(firebaseConfig)

  await preloadFirestore({
    firebaseApp,
    setup: async (factory) => {
      const firestore = factory()

      if (process.env.NODE_ENV !== 'production') {
        firestore.useEmulator('localhost', 8080)
      }

      //if (process.env.NODE_ENV === 'production') {
      //  await firestore.enablePersistence({ synchronizeTabs: true })
      //}

      return firestore
    },
  })

  await preloadFunctions({
    firebaseApp,
    setup: (factory) => {
      const functions = factory(FUNCTIONS_REGION)

      if (process.env.NODE_ENV !== 'production') {
        functions.useEmulator('localhost', 5001)
      }

      return functions
    },
  })

  await preloadAuth({
    firebaseApp,
    setup: async (factory) => {
      const auth = factory()

      if (process.env.NODE_ENV !== 'production') {
        auth.useEmulator('http://localhost:9099')
      }

      return auth
    },
  })

  ReactDOM.unstable_createRoot(document.getElementById('root')).render(
    <FirebaseAppProvider firebaseApp={firebaseApp} suspense={true}>
      <App />
    </FirebaseAppProvider>
  )
})

It feels like much of that could be handled inside ReactFire, perhaps with a boolean emulators property (or a config object with values for each emulator as needed) on FirebaseAppProvider. If it could avoid preloading each module until it's needed for the first time that would be ideal.

hubgit avatar Dec 28 '20 09:12 hubgit

I agree with @hubgit suggestion, this would make the development experience better for all firebase products when using reactfire.

This would also align with the push in Firebase Summit 2020 #AskFirebase session which was pushing the emulators for use during dev.

Can you please add this to the backlog for someone to pickup and implement.

Cheers Bullfrog1234

Bullfrog1234 avatar Jan 04 '21 11:01 Bullfrog1234

I love the idea. But there would be one concern. Sometimes, firebase emulators:start throws errors on ports. So ideal would be to have ana ability to change the port too.

subhendukundu avatar Feb 02 '21 06:02 subhendukundu

Thanks @hubgit, @Bullfrog1234, and @subhendukundu for your feedback!

For ReactFire v4, setting up emulators is a bit different than @hubgit's code sample above. It is documented in the Using ReactFire guide. Given the api change and documentation, are convenience functions to connect to the emulators still needed? And if so, what would you want it to look like?

jhuleatt avatar Sep 08 '21 16:09 jhuleatt

Thanks for asking @jhuleatt

My preference is that using emulators during dev is invisible and during production it automatically switches to live services.

How I have been doing this in projects without reactfire is automatically getting ports from the firebase.json file and looking at process.env.NODE_ENV to see if it is 'production', 'test' or 'development'.

In reactfire I would configure reactfire to default to:

  • production: using live services
  • test: using emulators
  • development: using emualtors

Edge cases that should be covered are emulator not configured in firebase.json.

Also I would get the reactfire to listen to env variables for disabling an emulator for an environment: e.g.

REACT_APP_REACTFIRE_EMULATOR_DEV_FIRESTORE = false
REACT_APP_REACTFIRE_EMULATOR_DEV_AUTH = false
REACT_APP_REACTFIRE_EMULATOR_TEST = false

The above would cause reactfire to use live services for firestore auth in the development mode and all services in the test environment.

Look at Create React App site for information on envirnment variables in React.

When using enironment variables and using NODE_ENV when building the code to setup the emualtors is stripped out and not shipped which results in a smaller build.

On @subhendukundu comment:

I love the idea. But there would be one concern. Sometimes, firebase emulators:start throws errors on ports. So ideal would be to have ana ability to change the port too.

This is a result of ports being used and should be fixed in firebase.json so it starts the emualtor on a different port. Connecting reactfire to firebase.json will mean that if you have to change a port. A restart of the dev environment will pickup the change and reconfigure automatically.

I believe this approach has the following advantages:

  • removes the need to manually setup emulators, clean code that resembles what you are building not what you are trying to test
  • automatically enables and disables emualtors based on the environment and the emulators enabled reducing risk of error
  • changes to emulator ports is automatically configured reducing risk of errors
  • smaller build because emualtor setup code is removed during the build process

The disadvantages are:

  • This would be a major version because it would eventually be a breaking change, but I don't think a big issue it would be an improvement for everyone
  • New emulators would need to be implemented promptly into this package

Additionally, this would need to matched with proper documentation on all the available environment variables and their defaults and a mini lesson on how to use.

Thats my 2 cents worth of an idea.

Bullfrog1234 avatar Sep 08 '21 22:09 Bullfrog1234

Why can't the hosting emulator set up all the settings needed for the rest of the emulators to work without me having to add code in my app? Especially code that, at best, does nothing in prod? What is reactfire doing that is so special that it needs to break what should be a simple pattern?

proegssilb avatar Sep 09 '21 13:09 proegssilb

Why can't the hosting emulator set up all the settings needed for the rest of the emulators to work without me having to add code in my app? Especially code that, at best, does nothing in prod? What is reactfire doing that is so special that it needs to break what should be a simple pattern?

ReactFire is not in control of the emulators it just uses them. I would post that suggestion in the firebase-tools repository as a feature request. In reality I am not sure if they would want to do that as it does reduce some flexibility in how some people develop, and would impact more then just react developers it would impact all developers.

I believe if reactfire implements the automatic connecting emulators depnding on envirnment variables with appropriate overrides there would be less issues in the wider community.

Bullfrog1234 avatar Sep 11 '21 00:09 Bullfrog1234