How to prevent infinite loop caused by onAuthStateChanged Firebase Auth - javascript

I have a provider component that sets the initial auth context from firebase-auth.
Everything works fine until I try to add persistence in the form of setting up an observer with onAuthStateChanged. This checks for auth and I update my state via dispatch method.
But this is causing an infinite loop. I added an unsubscribe function call, but this makes no difference,
Can anyone advise? thanks
AuthContext.js
import React from "react";
import * as firebase from "firebase/app";
//firebaseauth reducer
import { firebaseAuth } from "../reducers/AuthReducer";
export const Auth = React.createContext();
const initialState = { user: {} };
export const AuthProvider = (props) => {
const [state, dispatch] = React.useReducer(firebaseAuth, initialState);
const value = { state, dispatch };
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
dispatch({
type: "HYDRATE_AUTH",
payload: user,
});
});
unsubscribe();
return <Auth.Provider value={value}>{props.children}</Auth.Provider>;
};

What is happening is:
The component renders
It sets the auth listener
The listener fires and sets the state of the component
State update causes the component to rerender
The component adds another listener and everything repeats
Unsubscribing doesn't help because the component keeps rerendering and adding a new listener every time.
We can tell the component to set the listener only once by using useEffect():
AuthContext.js
import React from "react";
import * as firebase from "firebase/app";
//firebaseauth reducer
import { firebaseAuth } from "../reducers/AuthReducer";
export const Auth = React.createContext();
const initialState = { user: {} };
export const AuthProvider = (props) => {
const [state, dispatch] = React.useReducer(firebaseAuth, initialState);
const value = { state, dispatch };
React.useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
dispatch({
type: "HYDRATE_AUTH",
payload: user,
});
});
}, []);
return <Auth.Provider value={value}>{props.children}</Auth.Provider>;
};
By providing an empty dependency array to useEffect(), we tell it to run the callback only once, when the component initially renders, so the auth listener is set only once.

Thanks to #tomek-ch -- an alternate solution would be to prevent the re-render by storing the state (eg: keep the user bits in the component state) and then do not update the state if its already there (or the same).
In my case I just keep the user the first time, set a boolean state flag, and ignore subsequent events.

Related

Using Redux Toolkit, how do I access the store from a non-react file?

What am I trying to do?
Using Redux Toolkit, I'm trying to access the "store" for a value, specifically "username" which I've created a "slice" for, from a non-React file called SomeFile.js.
What is the code that currently tries to do that?
// userMetadataSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
username: "",
};
const userMetadataSlice = createSlice({
name: "userMetadata",
initialState,
reducers: {
updateUsername: (state, action) => {
const username = action.payload;
state.username = username;
},
},
});
export const { updateUsername } = userMetadataSlice.actions;
export default userMetadataSlice.reducer;
export const selectUsername = (state) => {
return state.userMetadata.username;
}
// SomeFile.js
import { selectUsername } from "../redux/userMetadataSlice";
import { useSelector } from "react-redux";
export const displayUsername = () => {
const username = useSelector(selectUsername);
console.log("Username:", username); // Error.
}
What do I expect the result to be?
To be able to pull the username from the "store".
What is the actual result?
When I try to access the value via "useSelector" from the non-react file an error occurs: React Hook "useSelector" is called in function "selectUsername" which is neither a React function component or a custom React Hook function
What I think the problem could be?
SomeFile.js does not have anything React related within it because it just pulls data from the store and outputs the data.
A solution I've tried that worked was to do this:
// userMetadataSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
username: "",
};
const userMetadataSlice = createSlice({
name: "userMetadata",
initialState,
reducers: {
updateUsername: (state, action) => {
const username = action.payload;
state.username = username;
},
},
});
export const { updateUsername } = userMetadataSlice.actions;
export default userMetadataSlice.reducer;
export const selectUsername = (state) => {
return state.userMetadata.username;
}
// New code here!
export function SelectUsername() {
const username = useSelector(selectUsername);
return username;
}
// SomeFile.js
import { SelectUsername } from "../redux/userMetadataSlice";
export const displayUsername = () => {
console.log("Username:", SelectUsername); // No errors, shows correct output.
}
The solutions I'm looking for is this:
Is my proposed solution the "proper" way to receive info from the "store" in non-React files?
Is there a custom hook solution for this?
Is my proposed solution the "proper" way to receive info from the
"store" in non-React files?
No, it's abusing the Rules of Hooks and React functions. You are directly invoking the SelectUsername React function.
Is there a custom hook solution for this?
No, React hooks work only in React functions and custom React hooks.
You can access your state from your Redux store object.
Store
From your created store object you'll have a getState method to invoke.
getState()​
Returns the current state tree of your application. It is equal to the
last value returned by the store's reducer.
Returns​
(any): The current state tree of your application.
You can export your created store object for import into non-React JS files and they can invoke the getStore method.
import store from '../path/to/store';
...
const state = store.getState();
The useSelector React hook from react-redux won't work outside a React component, but the selectUsername state selector function will.
// SomeFile.js
import store from '../path/to/store';
import { selectUsername } from "../redux/userMetadataSlice";
...
export const displayUsername = () => {
const state = store.getState();
const username = selectUsername(state);
console.log("Username:", username);
return username;
};
See the other Store Methods for subscribing to state changes and dispatching actions to your store from outside React.

How to avoid "React Hook useEffect has a missing dependency" and infinite loops

I have written a react-js component like this:
import Auth from "third-party-auth-handler";
import { AuthContext } from "../providers/AuthProvider";
export default function MyComponent() {
const { setAuth } = useContext(AuthContext);
useEffect(() => {
Auth.isCurrentUserAuthenticated()
.then(user => {
setAuth({isAuthenticated: true, user});
})
.catch(err => console.error(err));
}, []);
};
With the following AuthProvider component:
import React, { useState, createContext } from "react";
const initialState = { isAuthenticated: false, user: null };
const AuthContext = createContext(initialState);
const AuthProvider = (props) => {
const [auth, setAuth] = useState(initialState);
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{props.children}
</AuthContext.Provider>
)
};
export { AuthProvider, AuthContext };
Everything works just fine, but I get this warning in the developer's console:
React Hook useEffect has a missing dependency: 'setAuth'. Either include it or remove the dependency array react-hooks/exhaustive-deps
If I add setAuth as a dependency of useEffect, the warning vanishes, but I get useEffect() to be run in an infinite loop, and the app breaks out.
I understand this is probably due to the fact that setAuth is reinstantiated every time the component is mounted.
I also suppose I should probably use useCallback() to avoid the function to be reinstantiated every time, but I really cannot understand how to use useCallback with a function from useContext()
If you want to run useEffect call just once when component is mounted, I think you should keep it as it is, there is nothing wrong in doing it this way. However, if you want to get rid of the warning you should just wrap setAuth in useCallback like you mentioned.
const setAuthCallback = useCallback(setAuth, []);
And then put in in your list of dependencies in useEffect:
useEffect(() => {
Auth.isCurrentUserAuthenticated()
.then(user => {
setAuth({isAuthenticated: true, user});
})
.catch(err => console.error(err));
}, [setAuthCallback]);
If you have control over AuthContext Provider, it's better to wrap your setAuth function inside.
After OP edit:
This is interesting, setAuth is a function from useState which should always be identical, it shouldn't cause infinite loop unless I'm missing something obvious
Edit 2:
Ok I think I know the issue. It seems like calling
setAuth({ isAuthenticated: true, user });
is reinstantianting AuthProvider component which recreates setAuth callback which causes infinite loop.
Repro: https://codesandbox.io/s/heuristic-leftpad-i6tw7?file=/src/App.js:973-1014
In normal circumstances your example should work just fine
This is the default behavior of useContext.
If you are changing the context value via setAuth then the nearest provider being updated with latest context then your component again updated due to this.
To avoid this re-rendering behavior you need to memorize your component.
This is what official doc says
Accepts a context object (the value returned from React.createContext)
and returns the current context value for that context. The current
context value is determined by the value prop of the nearest
<MyContext.Provider> above the calling component in the tree.
When the nearest <MyContext.Provider> above the component updates,
this Hook will trigger a rerender with the latest context value passed
to that MyContext provider. Even if an ancestor uses React.memo or
shouldComponentUpdate, a rerender will still happen starting at the
component itself using useContext.
Like this ?
function Button() {
let appContextValue = useContext(AppContext);
let theme = appContextValue.theme; // Your "selector"
return useMemo(() => {
// The rest of your rendering logic
return <ExpensiveTree className={theme} />;
}, [theme])
}
I did finally solve not using useCallback in MyComponent, but in ContextProvider:
import React, { useState, useCallback, createContext } from "react";
const initialState = { authorized: false, user: null };
const AuthContext = createContext(initialState);
const AuthProvider = (props) => {
const [auth, setAuth] = useState(initialState);
const setAuthPersistent = useCallback(setAuth, [setAuth]);
return (
<AuthContext.Provider value={{ auth, setAuth: setAuthPersistent }}>
{props.children}
</AuthContext.Provider>
)
};
export { AuthProvider, AuthContext };
I am not sure this is the best pattern, because code is not so straightforward and self-explaining, but it works, with no infinite loop nor any warning...

React JS useState initial value not updating inside JS fetch API

I am novice to React JS. I have useState and fetchAPI inside contextAPI hooks but the initial state is not updating.
// code
import React,{useState, createContext} from 'react'
export const contextApi = createContext()
export const ContextApiProvider = (props) => {
const [query, setQuery] = useState('chicken')
const [recipes, setRecipes] = useState([])
const api_props = {
APP_ID: '84cf712e',
APP_KEY:'asdcb2b8b842f3e543casjakfa710de4fb343592a64d',
APP_QUERY: query
}
fetch(`https://api.edamam.com/search?q=${api_props.APP_QUERY}&app_id=${api_props.APP_ID}&app_key=${api_props.APP_KEY}`)
.then(res => res.json()).then(data => setRecipes(data.hits))
return (
<contextApi.Provider value={{recipes}}>
{props.children}
</contextApi.Provider>
)
}
First look up the useEffect hook that is where you want to do your data fetching. From there you could set the state using the setState hook that you are running. This might create an endless loop because your are setting state which reruns the component which then trys to set state again.
Hope that helps let me know if you have questions.

UseSelector State is Undefined

The page does not render, citing TypeError: state is undefined, tracing back to this line in SelectForm.js: const filter = useSelector(state => state.filter);.
I've spent hours trying to figure out what I'm doing wrong. I've tried createSelector but that didn't work. I've tried dispatching a "Fetch Initial State" action, and that didn't work. The component is wrapped in provider tags. I'm not sure why I don't have access to the state. At this point I'm unable to see any flaws I've been looking at it for so long.
Code Snippets
reducer.js
let initialState = {
filter: {
country: null,
state: null,
level: null,
team: null
},
isDBConnecting: false,
isDBConnected: false,
isDBError: false
}
const SelectorReducer = (state=initialState, action) => {
switch (action.type) {
case 'DB_CONNECT_INIT':
return {
...state,
isDBConnecting: true,
isDBConnected: false,
isDBError: false,
};
...
...
}
export default SelectorReducer;
actions.js
export const initializeDBConnection = () => {
return {
type: 'DB_CONNECT_INIT'
}
};
ParentComponent.js
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux'; //import provider to provide component access to the state
//Component imports
import SelectForm from './components/SelectForm'
import SelectorReducer from '.../reducer.js'
const SelectorStore = createStore(SelectorReducer);
const ParentComponent = () => {
return (
<div className="page-container">
<div id="carousel">
<div id="wrapper">
<Provider store={SelectorStore}>
<SelectForm />
</Provider>
</div>
</div>
</div>
)
}
SelectForm.js (Child Component, wrapped in Provider tags above)
//IMPORTS
import React from 'react'; //import react
import { useSelector, useDispatch } from 'react-redux';
//COMPONENT IMPORTS
import FormGroup from '../FormGroup';
import { * as actions } from '.../actions.js';
const SelectForm = (props) => {
//STATEFUL IMPORTS
//filter
const filter = useSelector(state => state.filter);
Credit to #NicholasTower for the answer in the comments. My reducer did not have a default case in which
default: return state
Putting that in solved the issue.
let filter = useSelector(state => {
console.log('State: ', state);
return state.pieChart.filter;
});
Make sure that you are using the correct object for state.
Add a console for debugging and check the state object. Many times we use multiple reducers which makes our component state nested in global object.
We should import useSelector form react-redux and then select user details form store in the following way
import { useSelector } from 'react-redux'
const User = () => {
const userInfo= useSelector(state => state.user)
return <div>{userInfo.name}</div>
}
Inside ParentComponent.js you are saying <Provider store={TeamSelectorStore}>. I think you meant to say <Provider store={SelectorStore}>
In my case I was storing the data coming from Redux state at didMount stage whereas the Redux states weren't fully uploaded yet.
const currentUser = useAuth();
useEffect(() => {
form.setFieldValue('email', currentUser.email);
}, []);
Adding currentUser to useEffect dependency array, get it resolved for me:
useEffect(() => {
form.setFieldValue('email', currentUser.email);
}, [currentUser]);
Sometimes having a break in the switch block of the reducer can cause the prop not defined in the state.
This error can also happen because of an uppercase / lowercase mistake.
let horns = useSelector(state => state.largeRhinohorns);
let horns = useSelector(state => state.largeRhinoHorns);

React Redux Mapping state to props not working

So I'm trying to learn React with Redux and so far I think I've been able to work out most of the code needed to make it work but I'm having an issue with getting my state passed down to my component. I am using Visual Studio 2017's ASP.NET Core project template that has react and redux boilerplate codes and they used this:
export default connect(
state => state.weatherForecasts,
dispatch => bindActionCreators(actionCreators, dispatch)
)(FetchData);
I tried doing the same thing with my own component like so:
export default connect(
state => state.lecture,
dispatch => bindActionCreators(actionCreators, dispatch)
)(LectureTable);
but when trying to access the contents of my props, the properties I want to get are tagged as undefined. I checked through Redux devtools that my initial state exists but my component is unable to see the props I'm trying to pass to it. The weird thing is I just imitated the boilerplate code but it isn't working yet the boilerplate code works just fine (ie I can go to the component and log out its initial state).
Since I'm following the format used by Visual Studio,my actioncreators, reducers, and constants are in one file shown below:
const GET_LECTURES = "GET_LECTURES";
const initialState = {
lectures: [],
selectedLecture: {},
isLoading: false,
test: 0
};
export const actionCreators = {
requestLectures: isLoading => async (dispatch) =>
{
if (!isLoading) {
// Don't issue a duplicate request (we already have or are loading the requested data)
return;
}
dispatch({ type: GET_LECTURES });
const url = `api/lecture/`;
const response = await fetch(url);
const lectures = await response.json();
dispatch({ type: RECEIVE_LECTURES, payload: lectures });
}
};
export const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_LECTURES:
return { ...state, isLoading: true };
default:
return state;
}
};
I'm sorry if its all messy. I'm really just starting to begin to understand redux..
Edit
My component code:
import React, { Component } from 'react';
import {Button, Table, Label, Menu, Icon} from 'semantic-ui-react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {actionCreators} from './../../store/Lecture';
export class LectureTable extends Component {
componentWillMount(){
// this.props.requestLectures(this.props.isLoading);
console.log(this.props.test);
}
render() {
return (
<Table size='large'>
{/*removed to make it cleaner..currently only has static data too lol*/}
</Table>
)
}
}
export default connect(
state => state.lecture,
dispatch => bindActionCreators(actionCreators, dispatch)
)(LectureTable);
where my store is configured:
import { applyMiddleware, combineReducers, compose, createStore } from 'redux';
import thunk from 'redux-thunk';
import { routerReducer, routerMiddleware } from 'react-router-redux';
import * as Lecture from './Lecture';
import * as Counter from './Counter';
import * as WeatherForecasts from './WeatherForecasts';
export default function configureStore(history, initialState) {
const reducers = {
lecture: Lecture.reducer,
counter: Counter.reducer,
weatherForecasts: WeatherForecasts.reducer
};
const middleware = [
thunk,
routerMiddleware(history)
];
// In development, use the browser's Redux dev tools extension if installed
const enhancers = [];
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment && typeof window !== 'undefined' && window.devToolsExtension) {
enhancers.push(window.devToolsExtension());
}
const rootReducer = combineReducers({
...reducers,
routing: routerReducer
});
return createStore(
rootReducer,
initialState,
compose(applyMiddleware(...middleware), ...enhancers)
);
}
my index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import { createBrowserHistory } from 'history';
import configureStore from './store/configureStore';
import App from './pages/App';
import registerServiceWorker from './registerServiceWorker';
// Create browser history to use in the Redux store
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
const history = createBrowserHistory({ basename: baseUrl });
// Get the application-wide store instance, prepopulating with state from the server where available.
const initialState = window.initialReduxState;
const store = configureStore(history, initialState);
const rootElement = document.getElementById('root');
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
rootElement);
registerServiceWorker();
The first argument to connect() should be a function that returns an object - with the props you want added as keys, and their value being the value from state. e.g.
state => ({ lecture: state.lecture })
I found the solution. First of all I'm a noob both to stackoverflow and to react so I apoligize for all my inconsistencies (if thats the right term?).
What I found out:
I am using react router
I was doing the connect method to a subcomponent of the component being rendered by the router
I placed the connect method to the parent component and it worked
Some notes:
state => state.lecture still works
I will take all of your advices to heart and change my code accordingly
The only reason I was adamant with solving the problem using the code I had was because I couldn't accept the fact that boilerplate code wouldn't work unless I had done something specifically different from what the boilerplate did. I just didn't take into account that the router played a huge role with it.
I repeat...I'm a react noob so I'm sorry for wasting your time T_T
Edit again:
I was able to connect a different child component with the Redux store. I'm trying to look at why I still can't do it for that specific component that caused me to ask this question. I'll update my answer once I find the reason.
I think in their example weatherForecasts is an object. In your example lectures seems to be an array so I suggest to rewrite your mapStateToProps function like this if you only need to get the lectures prop
state => ({ lectures: state.lectures})
if you need the whole state you can have state => state so you can access the props this.props.test and this.props.lectures
Keep in mind that mapStateToProps should return an object, not an array. By the way, in your reducer the field name is lectures (plural) not lecture so state => state.lecture will be undefined
Rick, your connect argument should be something like:
export default connect( state => {
return {
test: state.lecture // Or any value
}
})(LectureTable);
You're trying to console log the test prop, so you should include it in your connect call.
I think by doing the following steps, you can solve the issue:
First you need to call two functions when you want to connect your component to application state, one is mapDispatchToProps and another one is mapStateToProps, for your code to be clean, its better to define these functions separately and then pass them by name to connect, but if you want to use your own way you should do these changes: (assuming your reducer name is lecture from your combineReducers, and assuming you are calling requestLectures with this syntax: this.props.lectureActions.requestLectures() and importing lectureActions from the file you have written lecture related actions) :
export default connect(
state => state.lecture.lectures,
dispatch => {lectureActions: bindActionCreators(lectureActions, dispatch)}
)(LectureTable);
from above code, you do not need to export an object that contains the actions like actionCreators, you should export the requestLectures function out of it independently
add below case to your reducer so that when getting the lectures succeeds the state of the application gets updated with the lectures:
case RECEIVE_LECTURES:
return { ...state, isLoading: false, lectures: payload.lectures };
default:
return state;
}
You have two problems here.
You are defining mapStateToProps function as the first argument to connect wrong. As many of answers explain this now you should use it like, this:
export default connect(
state => ( { lecture: state.lecture } ),
dispatch => bindActionCreators(actionCreators, dispatch)
)(LectureTable);
Now, you have a lecture prop as your state. You can reach it with this.props.lecture. But in your componentWillMount method, you are trying to log it like this.props.test. It should be this.props.lecture.test.
By the way, try to use componentDidMount instead of componentWillMount since it will be deprecated in the future releases.

Categories

Resources