Laczkó Örs
Laczkó Örs

Reputation: 1118

How to treat global user/user data in react?

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

Answers (4)

Lajos Arpad
Lajos Arpad

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

Abhishek
Abhishek

Reputation: 1332

Issue:-

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

Solution:-

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

ckedar
ckedar

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

Praud
Praud

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.

enter image description here

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

Related Questions