Eugene Kliuchnikov
Eugene Kliuchnikov

Reputation: 61

React/Firebase. How can I save my updated data in my project?

When I am trying to change the data - the changes are getting to my Firestore Database, it's ok, but when I reload the page or logging out and trying to log in again - the user data does not appear in my project, but is also stored in my Firestore Database. How to save and display user data in the project even after I reload the page?

This is my code

    const [email, setEmail] = useState('')
    const [phone, setPhone] = useState('')
    const [language, setLanguage] = useState('')
    const [valute, setValute] = useState('')
    const [instagram, setInstagram] = useState('')
   

    const {user} = UserAuth()


    const createUser = (email, password) => {
        createUserWithEmailAndPassword(auth, email, password)
        .then((userCredential) => {
            const user = userCredential.user;
            const uid = user.uid;
            setDoc(doc(db, 'users', uid), {
                email: email,
                name: name,
                password: password,
                phone: phone,
                language: language,
                instagram: instagram,
                valute: valute,
            });
          })
          .catch((error) => {
            const errorCode = error.code;
            const errorMessage = error.message;
            // ..
          });
    }

    const updatePerson = async(email, phone, language, valute, instagram, name, ) => {
      await updateDoc(doc(db, 'users', user.uid), {email, phone, language, valute, instagram, name})
    }

    <input type="text" placeholder={name || user.displayName || user.email || user.phoneNumber} className='form-control' onChange={(e) => setName(e.target.value)}/>
    <input type="text" placeholder={instagram} className='form-control' onChange={(e) => setInstagram(e.target.value)}/>
    <input type="text" placeholder={email} className='form-control' onChange={(e) => setEmail(e.target.value)}/>
    <input type="text" placeholder={phone} className='form-control' onChange={(e) => setPhone(e.target.value)}/>
    <input type="text" placeholder={language} className='form-control' onChange={(e) => setLanguage(e.target.value)}/>
    <input type="text" placeholder={valute} className='form-control' onChange={(e) => setValute(e.target.value)}/>

Upvotes: 2

Views: 638

Answers (2)

Sergey Sosunov
Sergey Sosunov

Reputation: 4600

Im using custom hooks for that purpose. That is pretty reusable and i can load multiple (specific) users (or anything else) in same time.

useUserData.jsx

import { useEffect, useState } from "react";
import { doc, onSnapshot } from "firebase/firestore";
import { db } from "../firebase";


export function useUserData(userId) {
  const [userData, setUserData] = useState(undefined); // undefined means not loaded

  useEffect(() => {
    if (!userId) {
      // null or undefined is passed
      setUserData(undefined);
      return; 
    }
    const unsubscribe = onSnapshot(doc(db, "users", userId), (doc) => {
      if (doc.exists()) {
        setUserData(doc.data()); // found and setting a user
      } else {
        setUserData(null); // null means user not found in users collection
      }
    });

    return () => unsubscribe();
  }, [userId]);

  return userData;
}

Usage and additional explanations and examples

import React, { useState, useCallback, useEffect } from "react"
import useAuthContext from "../../Context/AuthContext";
import { useUserData } from "../../Hooks/useUserData";

export function Home(props) {
  const {user, updateData} = useAuthContext();

  // NOTE: Do not destructure in this way due to it will cause an exception for null and undefined values
  // const {firstName, lastName} = useUserData(user?.uid); 

  const userData = useUserData(user?.uid); // user might be undefined, so ? is used. Null will be passed but hook will work

  // I prefer storing form data in this way, look at handleChanges function
  // Important: this approach relies on input "name" attribute, it should match
  const [formData, setFormData] = useState({
    firstName: "",
    lastName: ""
  });

  // Just to update the existing doc.
  const updatePersonSubmit = useCallback(async (evt) => {
    evt.preventDefault();
    await updateData(formData.firstName, formData.lastName);
  }, [updateData, formData]);

  // In setter you can get previous (current) value
  // this function that is passed as a parameter should return new object to set
  // new object consists of all ...prev fields it had
  // and one field is overriden by key (name) and will have a new value here
  const handleChange = useCallback((evt) => {
    setFormData(prev => ({...prev, [evt.target.name]: evt.target.value}));
  }, []);

  // listen to the userData updates and update the inputs
  // || prev.XXX is used due to data in userData can be null or undefined
  // inputs in React expects empty strings for bindings, nulls or undefined will
  // break everything, so using old value if any (initialized with "")
  useEffect(() => {
    setFormData(prev => ({
      ...prev,
      firstName: userData?.firstName || prev.firstName,
      lastName: userData?.lastName || prev.lastName
    }));
  }, [userData])

  return (
    <div className="container-fluid">
      {user && (
        <form>
          <p>Those fields are NOT in firestore, they needs to be set/updated with auth functions, not so easy here</p>
          <p>displayName: {user.displayName || "null"}</p>
          <p>email: {user.email || "null"}</p>
          <p>phoneNumber: {user.phoneNumber || "null"}</p>
        </form>
      )}
      <br/>
      <br/>
      <form onSubmit={updatePersonSubmit}>
        <p>Those fields are in firestore, they can be set/updated with firestore setDoc or updateDoc</p>

        <input name="firstName" type="text" placeholder={"firstName"} className='form-control'
               value={formData.firstName} onChange={handleChange}/>

        <input name="lastName" type="text" placeholder={"lastName"} className='form-control'
               value={formData.lastName} onChange={handleChange}/>

        <button type="submit">Save</button>
      </form>
    </div>
  )
}

Upvotes: 1

Eugene Kliuchnikov
Eugene Kliuchnikov

Reputation: 61

I've found the answer on my question.

We are expecting that onSnapshot will magically be called right after our app loads - it needs some time to fetch document from our collection. I would add additional flag which indicate if data is still loading or not:

function useUserData(userId) {
  const [userData, setUserData] = useState(undefined);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    if (!userId) return; // null or undefined is passed, so just do nothing

    const unsubscribe = onSnapshot(doc(db, "users", userId), (doc) => {
      setIsLoading(false);
      if (doc.exists()) {
        setUserData(doc.data()); // found and setting a user
      } else {
        setUserData(null); // null means user not found in users collection
      }
    });

    return () => unsubscribe();
  }, [userId]);

  return { userData, isLoading };
}

Then use it like that:

function Profile() {  /// or props here
 const { isLoading, userData } = useUserData(user.uid); /// if you use props just simple write uid
 if (isLoading) {
  return <span>Loading...</span>;
 }
 if (!userData) {
  return <span>Ooops. Profile not found</span>;
 }
 
 return <span>{userData.firstName}</span>
}

Upvotes: 0

Related Questions