Reputation: 1118
I'm building a ReactJS project and I'm using something like this, to provide user data trough the app:
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
function Comp2(props) {
const { user, setUser } = useContext(UserProvider);
return (
<div>
{user.exists}
</div>
)
}
function Comp3(props) {
const { user, setUser } = useContext(UserProvider);
return (
<div>
{user.exists}
</div>
)
}
//User Provider
import React from 'react';
const UserProvider = React.createContext();
export default UserProvider;
So, in this case, Comp1 provides user data to Comp2 & Comp3. The only problem is that when the user state changes or the page loads, it creates an infinite loop. If I'm not using an useState for storing the user data, then when it changes, the components do not get re-rendered. I also tried to do something like this in the index.js file:
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
ReactDOM.render(<Comp1 user={user} />, document.getElementById('root'));
} else {
ReactDOM.render(<Comp1 user={{exists: false}} />, document.getElementById('root'));
}
});
But this worked a bit weirdly sometimes, and it's kinda messy. What solutions are there? Thanks.
Edit: I'm triening to do it in the wrong way? How should I provide all user data with only one firebase query?
Upvotes: 2
Views: 4058
Reputation: 76436
Take a look at this logic:
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
You are calling setUser
in any case. Instead, you should check whether user
is already set and if so, whether it matches _user
. Set user
to _user
if and only if _user
differs from user
. The way it goes, setUser
is triggered, which triggers Comp2
and Comp3
change, which triggers the event above which triggers setUser
.
Upvotes: 1
Reputation: 1332
The issue for the loop to happen was due to the way Comp1 was written.
Any statement written within the Comp1
functional component will get executed after ever change in prop or state. So in this case whenever setUser
was called Comp1
gets re-render and again it subscribes to the auth change listener which again executes setUser on receiving the data.
function Comp1() {
const [user, setUser] = useState({});
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
return (
<div>
<UserProvider.Provider value={{user, setUser}}>
<Comp2 />
<Comp3 />
</UserProvider.Provider>
</div>
);
}
You can use useEffect to make statements execute on componentDidMount
, componentDidUdate
and componentWillUnmount
react's life cycles.
// [] is passed as 2 args so that this effect will run once only on mount and unmount.
useEffect(()=> {
const unsubscribe = firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
// this method gets executed on component unmount.
return () => {
unsubscribe();
}
}, []);
I created a replica for the above case, you can check it running here
Upvotes: 2
Reputation: 1909
In Comp1
, a new onAuthStateChanged
observer is added to firebase.Auth
on every render.
Put that statement in a useEffect
like:
useEffect(() => {
firebase.auth().onAuthStateChanged(function (_user) {
if (_user) {
// User is signed in.
// do some firestroe queryes to get all the user's data
setUser(_user);
} else {
setUser({ exists: false });
}
});
}, []);
Upvotes: 3
Reputation: 297
I suggest using some state container for the application to easily manipulate with a user. The most common solution is to use Redux
. With redux, you will be able to have a global state of your app. Generally, all user data stored in it. https://redux.js.org/
The other solution is to use MobX
with simple store access. It doesn't use Flux pattern if you don't like it.
If you don't want to use a global store you can use HOCs
to propagate your user data. Finally, you can use Context
to React, but it is bad approach.
Let's, for example, choose the most popular representer of Flux architecture - Redux.
The first layer is the View layer. Here we will dispatch some action to change global, e.g user data.
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { logIn, logOut } from 'actions'
export default class Page extends React.Component {
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
logIn(user)
} else {
logOut()
})
}, [])
render () {
...
}
}
const mapDispatchToProps = dispatch => bindActionCreators({
logIn,
logOut
}, dispatch)
export default connect(null, mapDispatchToProps)(App)
The second layer are actions. Here we work we with our data, working with api, format state and so on. The main goal of actions is to create data to pass it to the reducer.
actions.js
export const logIn = user => dispatch => {
// dispatch action to change global state
dispatch({
type: 'LOG_IN',
payload: user
})
}
export const logOut = user => dispatch => {
dispatch({ type: 'LOG_OUT' })
}
The last thing is the reducers. The only goal of them is to change state. We subscribe here for some actions. Reducer always should be a pure function. State shouldn't be mutated, only overwritten.
appReducer.js
const initialState = {
user: null
}
export default function appReducer (state = initialState, action) {
const { type, payload } = action
switch(type) {
case 'LOG_IN':
return {
...state,
user: payload
}
case: 'LOG_OUT':
return {
...state,
user: null
}
}
}
Then, we can work with the global app state whenever we want.
To do it, we should use react-redux
library and Provider
HOC
const App = () =>
<Provider store={store}>
<Navigation />
</Provider>
Now, we can have access to any stores inside any component though react-redux connect
HOF. It works with React Context API inside of it.
const Page2 = ({ user }) => {
//... manipulate with user
}
// this function gets all stores that you have in the Provider.
const mapStateToProps = (state) => {
user: state.user
}
export default connect(mapStateToProps)(Page2)
By the way, you should choose middleware to work with async code in redux. The most popular that is used in my example is redux-thunk
.
More information you can find in the official documentation.
There you can find information about how to make initial store configuration
Upvotes: 4