Skip to content

Reducer gets called twice while dispatch is called once #30

Closed
@ghivert

Description

@ghivert

Hi!

I’m starting with React Basic Hooks those days for a Firebase project for my company and I find the package really amazing.
But I’m struggling with a specific point (not a really blocking point btw) : I’m using a Reducer and a Context at a root level component in order to require it from anywhere, and it’s working great. I’m using Firebase Authentication and I’m listening to the authentication which signals the application when a user logs in or out to ping the store and update it accordingly. But strangely, for the user auth, the reducer gets called twice while the dispatch is called once.

Here’s a Minimal Reproducible Example:

-- Main.purs
module Main where

import Prelude

import Data.Maybe (Maybe(..), maybe)
import Data.Nullable (Nullable)
import Data.Nullable as Nullable
import Effect (Effect)
import Effect.Class.Console as Console
import Effect.Exception (throw)
import Effect.Unsafe (unsafePerformEffect)
import React.Basic.DOM (render)
import React.Basic.DOM as DOM
import React.Basic.Events (handler_)
import React.Basic.Hooks (type (/\), Component, ReactContext, component, useEffectOnce, useReducer, (/\))
import React.Basic.Hooks as RB
import React.Basic.Hooks as React
import Web.DOM.NonElementParentNode (getElementById)
import Web.HTML as HTML
import Web.HTML.HTMLDocument (toNonElementParentNode)
import Web.HTML.Window as Window

type User = { uid :: String }

foreign import foreignAuthStateChanged :: (Nullable User -> Unit) -> Effect (Effect Unit)
foreign import foreignSignInWithPopup :: String -> Array String -> Effect Unit
foreign import foreignSignOut :: Effect Unit

type State = { user :: Maybe User}
data Action
  = UpdateUser (Maybe User)

init :: State
init = { user: Nothing }

reduce :: State -> Action -> State
reduce state (UpdateUser user) =
  let _ = unsafePerformEffect $ Console.warn "UpdateUser" in
  state { user = user }

onAuthStateChanged :: (Maybe User -> Effect Unit) -> Effect (Effect Unit)
onAuthStateChanged callback =
  foreignAuthStateChanged $ Nullable.toMaybe >>> callback >>> unsafePerformEffect

context :: ReactContext (State /\ (Action -> Effect Unit))
context =
  unsafePerformEffect
  $ RB.createContext
  $ init /\ (const $ Console.warn "Nothing implemented")

mkApp :: Component Unit
mkApp = do
  component "App" \props -> React.do
    store <- useReducer init reduce
    let state /\ dispatch = store
    useEffectOnce $ onAuthStateChanged $ \user -> do
      Console.warn "Dispatching"
      dispatch $ UpdateUser user
    pure $ RB.provider context store
      [ DOM.pre_ [DOM.text $ show state.user]
      , DOM.button
        { onClick: handler_ $ maybe (foreignSignInWithPopup "Google" ["openid"]) (const foreignSignOut) state.user
        , children:
          [ DOM.text $ maybe "Connect" (const "Disconnect") state.user]
        }
      ]

main :: Effect Unit
main = do
  document <- Window.document =<< HTML.window
  container <- getElementById "app" $ toNonElementParentNode document
  case container of
    Nothing -> throw "No Element \"app\" found."
    Just node -> do
      root <- mkApp
      render (root unit) node
// Main.js
exports.foreignAuthStateChanged = callback => () => {
  return firebase.auth().onAuthStateChanged(callback)
}

const selectProvider = provider => {
  switch (provider) {
    case 'Google':
      return new firebase.auth.GoogleAuthProvider()
  }
}

exports.foreignSignInWithPopup = provider => scopes => () => {
  const authProvider = selectProvider(provider)
  scopes.forEach(scope => authProvider.addScope(scope))
  return firebase.auth().signInWithPopup(authProvider)
}

exports.foreignSignOut = () => {
  return firebase.auth().signOut()
}

When running this, we got:

Capture d’écran 2020-06-19 à 13 02 19

Is there anything I’m doing wrong or is it a React problem? I saw that React.StrictMode sometimes calls the reducer twice, but I don’t use it… Because everything is pure it’s not so painful, but I don’t understand what’s happening.
For information, connect / disconnect just run the reducer once after the first time. It’s really only the first time. I also tried it in pure React and couldn’t reproduce the bug.
I created a repo with the example in order to test it easily: ghivert/example-bug-purs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions