header 导航中的 ReactJS 组件在上下文值更改后不会再次 re-render

ReactJS component in header navigation does not re-render again after context value changed

我有身份验证上下文提供程序来存储登录用户信息并在 header 导航中显示用户照片。

我有个人资料编辑页面来编辑用户的显示名称和照片。通过 api 将用户信息更新到 firebase 和数据库后,我再次将用户的更新信息设置为上下文值。但是 header 导航上的图像不会改变,直到手动刷新页面或单击其他路线。我知道刷新上下文值有什么问题。

auth_prodiver.jsx

import React, { useState, useEffect, useContext, createContext } from "react";
import { useLocation, Navigate } from "react-router-dom";
import { signIn, signUp, updateCurrentUser } from "../helpers/gql_auth_helpers";
import paths from "../routes/paths";
import { store } from "../store/configure_store";
import { saveUser } from "../store/slice/auth_slice";
import { auth } from "../helpers/init-firebase";

import {
  getAuth,
  sendEmailVerification,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  sendPasswordResetEmail,
  onAuthStateChanged,
  signInWithPopup,
  GoogleAuthProvider,
  FacebookAuthProvider,
  signOut,
  confirmPasswordReset,
  updateProfile,
  verifyPasswordResetCode,
  applyActionCode,
  checkActionCode,
  updatePassword,
} from "firebase/auth";

const AuthContext = createContext(null);
 let accessToken = "";

export const getAccessToken = () => accessToken;
export const setAccessToken = (token) => {
  accessToken = token;
};

export const AuthProvider = ({ user: usr, children }) => {
  const [user, setUser] = useState(usr);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      if (user && user.emailVerified) {
        setUser(user);
        setAccessToken(user.getIdToken(true));
        store.dispatch(saveUser(user));
      } else {
        setUser(null);
        setAccessToken(null);
        store.dispatch(saveUser(null));
      }
    });
    return () => {
      unsubscribe();
    };
  }, []);

  async function login(email, password) {
    return signInWithEmailAndPassword(auth, email, password).then(
      async function (res) {
         const auth = getAuth();
        return await auth.currentUser
          .getIdToken(true)
          .then(async function (token) {
            return await signIn(token);
          });
      }
    );
  }

  async function register(name, email, password) {
return createUserWithEmailAndPassword(auth, email, password).then(
  async function (res) {
    const auth = getAuth();
    return await updateProfile(auth.currentUser, {
      displayName: name,
    })
      .then(async function () {
        const auth = getAuth();
        if (auth.currentUser != null) {
          return await sendEmailVerification(auth.currentUser).then(
            async function (res) {
              return await auth.currentUser
                .getIdToken(true)
                .then(async function (token) {
                  await signUp(token);
                });
            }
          );
        }
      })
      .catch((error) => {
        throw error;
      });
  }
);
}

  async function resendEmailVerification() {
    return sendEmailVerification(auth.currentUser);
  }

  async function forgotPassword(email) {
    return sendPasswordResetEmail(auth, email, {
      url: `https://myanmargita.com/login`,
    });
  }

  async function confirmPassword(oobCode, newPassword) {
    return confirmPasswordReset(auth, oobCode, newPassword);
  }

  async function updateCurrentUserPassword(newPassword) {
    const auth = getAuth();
    const user = auth.currentUser;
    return updatePassword(user, newPassword);
  }

  async function updateUser(updateUserId, displayName, photoUrl) {
   const auth = getAuth();
   const currentUser = getAuth().currentUser;
   return await updateProfile(auth.currentUser, {
      displayName: displayName,
      photoUrl: photoUrl,
    })
  .then(async function () {
    const auth = getAuth();
    if (auth.currentUser != null) {
      updateCurrentUser(updateUserId, displayName, photoUrl)
        .then(async(updatedUser) => {
          if (updatedUser) {
            await currentUser.reload();
            setUser(currentUser);
            setAccessToken(currentUser.getIdToken(true));
            store.dispatch(saveUser(currentUser));
            console.log("User Name : ", user.displayName)
          } else {
            setUser(null);
            setAccessToken(null);
            store.dispatch(saveUser(null));
          }
        })
        .catch((error) => {
          throw error;
        });
    }
  })
  .catch((error) => {
    throw error;
  });
}

  async function resetPassword(actionCode) {
    return verifyPasswordResetCode(auth, actionCode);
  }

  async function applyAction(actionCode) {
    return applyActionCode(auth, actionCode);
  }

  async function checkAction(actionCode) {
    return checkActionCode(auth, actionCode);
  }

  async function logout() {
    return signOut(auth);
  }

  async function signInWithGoogle() {
    const provider = new GoogleAuthProvider();
    provider.setCustomParameters({
      display: "popup",
    });
    return signInWithPopup(auth, provider);
  }

  async function signInWithFacebook() {
    const provider = new FacebookAuthProvider();
    provider.setCustomParameters({
      display: "popup",
    });
   return signInWithPopup(auth, provider);
 }



  const value = {
    user,
    accessToken,
    login,
    register,
    forgotPassword,
    confirmPassword,
    logout,
    signInWithGoogle,
    signInWithFacebook,
    resetPassword,
    applyAction,
    checkAction,
    resendEmailVerification,
    updateCurrentUserPassword,
    updateUser,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

export const useAuth = () => {
   return useContext(AuthContext);
};

export const RequireAuth = ({ children }) => {
  let auth = useAuth();
  let location = useLocation();

  if (!auth?.user) {
    return <Navigate to={paths.login} state={{ from: location }} />;
  }

  return children;
};

profile_edit_page.jsx

import React, {  useEffect,useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as yup from "yup";
import PicturePicker from "../../components/picture_picker";
import ProgressBar from "../../controls/progress_bar";
import LoadingIndicator from "../../components/loading_indicator";
import {getUserWithId } from "../../gql/user";
import { useAuth } from "../../auth/auth_provider";
import { useQuery } from "@apollo/client";
import paths from '../../routes/paths';
import { storage} from "../../helpers/init-firebase";
import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";

const formSchema = yup.object().shape({
   name: yup.string().required("Name is required."),
   email: yup.string().required("Email is required."),
});

const formData = { 
  name: "",
  email: "",
  photoURL: "",
};

const ProfileEditPage = (props) => {
  const { id } =  useParams();
  const [uploadProgress, setUploadProgress] = useState(0);
  const [pictureUrl, setPictureUrl] = useState();
  const [loading, setLoading] = useState(false);
  const navigate = useNavigate();
  const { data, loading: dataLoading, error } = useQuery(getUserWithId,{variables:{
    "where": {
      "id": id
    }
  }});
  let auth = useAuth();

  const handleUpdate = async (edit_data) => {
    console.log("Id : "+ id);
    const user_name = edit_data.name.trim();
    const email = edit_data.email.trim();
    const file = edit_data.photoURL;

    if (pictureUrl === file) {
      auth.updateUser(id, user_name, edit_data.photoURL);
    } else {
      const file = edit_data.photoURL;
      const storageRef = ref(storage, "images/users/" + file.name);
      const  uploadTask = uploadBytesResumable(storageRef,file);
      uploadTask.on(
        "state_changed",
        (snapshot) => {
          var progress = Math.round(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      console.log("Progrss: ", progress);
      setUploadProgress({ progress });
    },
    (error) => {
      console.log("File Upload Error: ", error.message);
      throw error;
    },
    () => {
      getDownloadURL(uploadTask.snapshot.ref).then(async(url) => {
        await auth.updateUser(id, user_name, url);
        navigate(paths.getProfile(id));
      });
        }
      );
    }
  };

  const handleBack = () => {
    navigate(-1);
  };

  useEffect(()=>{
if(!dataLoading && !error){
  if(data?.user) {
  setPictureUrl(data?.user?.photoURL);
  formData.name = data?.user.displayName;
  formData.email = data?.user.email;
  formData.photoURL = data?.user.photoURL;
  }
  }
  },[dataLoading,error]);

  const EntryForm = () => {
    return (
      <Formik initialValues={formData} enableReinitialize={true} onSubmit={handleUpdate} validationSchema={formSchema}>
    {({ dirty, values, errors, touched, handleChange, handleSubmit, handleReset, setFieldValue }) => {
      return (
        <Form autoComplete="off">
          <div className="text-sm">
          <div className="flex flex-nowrap">
              <div className="w-48 p-2 m-2">Email</div>
              <div className="p-2 m-2">
                {values.email}
              </div>
            </div>
            <div className="flex flex-nowrap">
              <div className="w-48 p-2 m-2">Name</div>
              <div className="p-2 m-2">
                <Field 
                  type="text" 
                  className="p-2 w-96 textarea textarea-primary" 
                  id="name" 
                  name="name" 
                  placeholder="Display Name" 
                  value={values.name} 
                  onChange={handleChange}
                />
                <ErrorMessage name="name" component="span" className="text-sm text-red-500 px-2" />
              </div>
            </div>
            
            <div className="flex flex-nowrap">
              <div className="w-48 p-2 m-2">Profile Image</div>
              <div className="flex flex-col items-left align-middle">
                <div className="flex flex-col px-4 mt-8 mx-4 h-60 items-cente p-2 m-2" style={{ height: "250px", width: "200px" }}>
                  <PicturePicker url={pictureUrl} onChange={(file) => setFieldValue("photoURL", file)} value={values.photoURL} />
                  <ProgressBar className="px-2 pt-2 pb-1" percent={uploadProgress} />
                  <span className="text-red-600 self-center text-sm">{touched.photoURL && errors.photoURL}</span>
                  <ErrorMessage name="photoURL" component="span" className="text-sm text-red-500 px-2" />
                </div>
              </div>
            </div>
            <div className="flex flex-nowrap p-3 mt-5">
              <button type="submit" onClick={handleSubmit} disabled={!dirty} className="btn btn-primary btn-sm mx-2 my-1 w-[66px]">
                Update
              </button>
              <button disabled={!dirty} className="btn btn-primary btn-sm mx-2 my-1 w-[66px]" onClick={handleReset}>
                Clear
              </button>
            </div>
          </div>
        </Form>
      );
        }}
      </Formik>
    );
  };

  return (
    <div className="card">
      <header className="px-5 py-4 border-b border-gray-100">
        <h2 className="font-semibold text-gray-800">Edit Profile</h2>
      </header>
      <EntryForm />
      <LoadingIndicator loading={loading} color="#000099" label="Uploading..." />
    </div>
  );
};

export default ProfileEditPage;

profile.jsx(在 header.jsx 中使用的配置文件组件)

import { useState, useRef, useEffect } from "react";
import {
  ProfileIcon,
  ResetPasswordIcon,
  LogoutIcon,
} from "../assets/icons/svg_icons";
import { useAuth } from "../auth/auth_provider";
import { useNavigate } from "react-router-dom";
import paths from "../routes/paths";
import ProfileModal from "./profile_modal";

const Profile = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [modal, setModal] = useState({
    open: false,
    id: "",
  });

  const wrapperRef = useRef(null);
  const auth = useAuth();
  const navigate = useNavigate();

  const handleLogout = (e) => {
    e.preventDefault();
    auth.logout();
 };

  const handleResetPassword = (e) => {
    e.preventDefault();
    navigate(paths.resetpassword, { replace: true });
  };

  const handleProfile = (e) => {
    e.preventDefault();
    navigate(paths.getProfile(auth.user.uid));
  };

  const handleModalClose = async () => {
    setModal({ ...modal, open: false });
  };

  const handleModalOpen = async () => {
    setModal({ ...modal, id: auth.user.uid, open: true });
  };

  useOutsideClick(wrapperRef);
  
  function useOutsideClick(ref) {
    useEffect(() => {
  
      function handleClickOutside(event) {
        if (ref.current && !ref.current.contains(event.target)) {
         setIsOpen(false);
       }
     }

  // Bind the event listener
  document.addEventListener("mousedown", handleClickOutside);
  return () => {
    // Unbind the event listener on clean up
    document.removeEventListener("mousedown", handleClickOutside);
      };
    }, [ref]);
  }

  return (
<div className="flex justify-end" ref={wrapperRef}>
  <div className="relative">
    <div className="cursor-pointer" onClick={() => setIsOpen(!isOpen)}>
      <div className="avatar online placeholder">
        <div className="bg-neutral-focus text-neutral-content rounded-full w-12 h-12">
          {auth.user && (
            <div className="avatar online placeholder">
              <div className="bg-neutral-focus text-neutral-content rounded-full w-12 h-12">
                {auth.user ? (
                  auth.user?.photoURL == null ? (
                    <ProfileIcon className="w-10 h-10" />
                  ) : (
                    <img src={auth.user?.photoURL} />
                  )
                ) : (
                  <div></div>
                )}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>

    {isOpen && (
      <div
        className="origin-top-right absolute right-0 mt-2 w-80 rounded-md shadow-lg 
  bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-100
  focus:outline-none cursor-pointer z-50"
      >
        <div className="py-5">
          <div className="flex justify-center" onClick={handleModalOpen}>
            <div className="relative text-gray-700">
              {auth.user ? (
                auth.user?.photoURL == null ? (
                  <ProfileIcon className="w-20 h-20" />
                ) : (
                  <img
                    className="object-cover w-20 h-20 rounded-full"
                    src={auth.user?.photoURL}
                    alt={auth.user?.displayName}
                  />
                )
              ) : (
                <div></div>
              )}

              <div className="flex absolute right-0 bottom-0 w-8 h-8 bg-gray-400 rounded-full justify-center items-center">
                <svg
                  className="w-6 h-6"
                  xmlns="http://www.w3.org/2000/svg"
                  aria-hidden="true"
                  role="img"
                  width="1em"
                  height="1em"
                  preserveAspectRatio="xMidYMid meet"
                  viewBox="0 0 1024 1024"
                >
                  <path
                    fill="currentColor"
                    d="M864 260H728l-32.4-90.8a32.07 32.07 0 0 0-30.2-21.2H358.6c-13.5 0-25.6 8.5-30.1 21.2L296 260H160c-44.2 0-80 35.8-80 80v456c0 44.2 35.8 80 80 80h704c44.2 0 80-35.8 80-80V340c0-44.2-35.8-80-80-80zM512 716c-88.4 0-160-71.6-160-160s71.6-160 160-160s160 71.6 160 160s-71.6 160-160 160zm-96-160a96 96 0 1 0 192 0a96 96 0 1 0-192 0z"
                  />
                </svg>
              </div>
            </div>
          </div>

          <div className="pt-2" onClick={handleProfile}>
            {auth.user ? (
              <h1 className="text-sm text-gray-700 p-0 m-0">
                {auth.user.displayName}
              </h1>
            ) : (
              <div></div>
            )}
            {auth.user ? (
              <h2 className="text-sm text-gray-700 p-0 m-0">
                {auth.user.email}
              </h2>
            ) : (
              <div></div>
            )}
          </div>
          <div className="pt-4">
            <button
              type="submit"
              className="btn btn-sm btn-ghost rounded-lg mx-6 my-1 px-4 w-auto text-black border border-gray-400"
              onClick={handleProfile}
            >
              Manage your account
            </button>
          </div>
        </div>
        <div className="py-1 mx-2">
          <a
            className="group flex items-center px-4 py-2 text-sm text-gray-700
        hover:bg-primary hover:text-white"
            onClick={handleResetPassword}
          >
            <ResetPasswordIcon className="w-8 h-8" />
            <span className="px-1">Reset Password</span>
          </a>
        </div>
        <div className="divide-x" />
        <div className="py-1 mx-2">
          <a
            className="group flex items-center px-4 py-2 text-sm text-gray-700
        hover:bg-primary hover:text-white"
            onClick={handleLogout}
          >
            <LogoutIcon className="w-8 h-8" />
            <span className="px-1">Logout</span>
          </a>
        </div>
      </div>
    )}
  </div>
  {modal.open && (
    <ProfileModal id={auth.user.uid} onClose={handleModalClose} />
  )}
    </div>
  );
};

export default Profile;

如果您确定 url 已更改将密钥添加到 img 标签