React 上下文需要 2 次状态更新供消费者重新渲染

React context requires 2 state updates for consumers to re-render

所以我有一个直接的应用程序,需要您登录才能查看仪表板。我的身份验证流程基于 https://reactrouter.com/web/example/auth-workflow which in return bases their flow off of https://usehooks.com/useAuth/

目前,当用户登录时,它会调用上下文提供程序中的一个函数来登录,该函数会使用从服务器检索到的用户数据更新上下文的状态。这反映在我的上下文提供者下的 React 开发工具中,如教师属性所示:

当上下文状态成功更新后,我使用 react-router API 中的 useHistory().push("dashboard/main") 转到仪表板页面。仪表板是上下文提供者的消费者,但当我尝试呈现页面时,教师值仍然为空——尽管 React 开发工具清楚地显示该值已更新。当我再次登录时,仪表板将成功呈现,因此,最终需要两次上下文更新才能让我的仪表板反映更改并呈现。请参阅我的以下代码片段(不相关的代码已被编辑):

App.js

const App = () => {

return (
    <AuthProvider>        
        <div className="App">
            <Switch>
                <Route path="/" exact >
                    <Home setIsFetching={setIsFetching} /> 
                </Route>
                <ProtectedRoute path="/dashboard/:page" >
                    <Dashboard
                        handleToaster={handleToaster}
                    />
                </ProtectedRoute>
                <ProtectedRoute path="/dashboard">
                    <Redirect to="/dashboard/main"/>
                </ProtectedRoute>
                <Route path="*">
                    <PageNotFound/>
                </Route>
            </Switch>
            <Toaster display={toaster.display} setDisplay={(displayed) => setToaster({...toaster, display: displayed})}>{toaster.body}</Toaster>
        </div>
    </AuthProvider>   
);}

AuthProvider.js

const AuthProvider = ({children}) => {
const auth = useProvideAuth();

return(
    <TeacherContext.Provider value={auth}>
        {children}
    </TeacherContext.Provider>
);};

AuthHooks.js

export const TeacherContext = createContext();

export const useProvideAuth = () => {
    const [teacher, setTeacher] = useState(null);
    const memoizedTeacher = useMemo(() => ({teacher}), [teacher]);

const signin = (data) => {
    fetch(`/api/authenticate`, {method: "POST", body: JSON.stringify(data), headers: JSON_HEADER})
    .then(response => Promise.all([response.ok, response.json()]))
    .then(([ok, body]) => {
        if(ok){
            setTeacher(body);
        }else{
            return {...body};
        }
    })
    .catch(() => alert(SERVER_ERROR));
};

const register = (data) => {
    fetch(`/api/createuser`, {method: "POST", body: JSON.stringify(data), headers: JSON_HEADER})
    .then(response => Promise.all([response.ok, response.json()]))
    .then(([ok, body]) => {
        if(ok){
            setTeacher(body);
        }else{
            return {...body};
        }
    })
    .catch(() => alert(SERVER_ERROR));
};

const refreshTeacher = async () => {
    let resp = await fetch("/api/teacher");
    if (!resp.ok)
        throw new Error(SERVER_ERROR);
    else
        await resp.json().then(data => {
            setTeacher(data);
        });
};

const signout = () => {
    STORAGE.clear();
    setTeacher(null);
};

return {
    ...memoizedTeacher,
    setTeacher,
    signin,
    signout,
    refreshTeacher,
    register
};
};

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

ProtectedRoute.js

const ProtectedRoute = ({children, path}) => {
    let auth = useAuth();
    return (
        <Route path={path}>
        {
            auth.teacher 
                ? children
                : <Redirect to="/"/>
        }
        </Route>
);
};

Home.js

const Home = ({setIsFetching}) => { 
let teacherObject = useAuth();
let history = useHistory();



const handleFormSubmission = (e) => {
    e.preventDefault();
    const isLoginForm = modalContent === "login";
    const data = isLoginForm ? loginObject : registrationObject;
    const potentialSignInErrors = isLoginForm ? 
    teacherObject.signin(data) : teacherObject.register(data);
    if(potentialSignInErrors)
        setErrors(potentialSignInErrors);
    else{
         *******MY ATTEMPT TO PUSH TO THE DASHBOARD AFTER USING TEACHEROBJECT.SIGNIN********
        history.replace("/dashboard/main");
    }
};

};)};

Dashboard.js

const Dashboard = ({handleToaster}) => {
const [expanded, setExpanded] = useState(true);
return (
    <div className={"dashboardwrapper"}>
        <Sidebar
            expanded={expanded}
            setExpanded={setExpanded}
        />
        <div className={"dash-main-wrapper"}>
        <DashNav/>
            <Switch>
                <Route path="/dashboard/classroom" exact>
                    <Classroom handleToaster={handleToaster} />
                </Route>
                <Route path="/dashboard/progressreport" exact>
                    <ProgressReport/>
                </Route>
                <Route path="/dashboard/help" exact>
                    <Help/>
                </Route>
                <Route path="/dashboard/goalcenter" exact>
                    <GoalCenter />
                </Route>
                <Route path="/dashboard/goalcenter/create" exact>
                    <CreateGoal />
                </Route>
                <Route path="/dashboard/profile" exact>
                    <Profile />
                </Route>
                <Route path="/dashboard/test" exact>
                    <Test />
                </Route>
                <Route path="/dashboard/main" exact>
                    <DashMain/>
                </Route>
            </Switch>
        </div>
    </div>
);
};

让我知道是否有任何让您印象深刻的事情会阻止我的仪表板第一次使用更新后的上下文值进行渲染,而不必更新它两次。如果您需要更深入地了解我的代码或者我错过了什么,请告诉我——我对 SO 也很陌生。此外,任何关于我的应用程序结构的指针都将不胜感激,因为这是我的第一个 React 项目。谢谢。

我认为问题出在 handleFormSubmission 函数中:

const handleFormSubmission = (e) => {
    e.preventDefault();
    const isLoginForm = modalContent === "login";
    const data = isLoginForm ? loginObject : registrationObject;
    const potentialSignInErrors = isLoginForm ? 
    teacherObject.signin(data) : teacherObject.register(data); 
    if(potentialSignInErrors)
        setErrors(potentialSignInErrors);
    else{
        history.replace("/dashboard/main");
    }
};

您调用 teacherObject.signin(data)teacherObject.register(data),然后依次更改历史状态。

问题是在调用 history.replace 之前,您无法确定 teacher 状态是否已更新。

我已经制作了您的主页组件的简化版本,以举例说明您如何解决该问题

function handleSignin(auth) {
  auth.signin("data...");
}

const Home = () => {
  const auth = useAuth();

  useEffect(() => {
    if (auth.teacher !== null) {
      // state has updated and teacher is defined, do stuff
    }
  }, [auth]);

  return <button onClick={() => handleSignin(auth)}>Sign In</button>;
};

所以当 auth 改变时,检查 teacher 是否有值并用它做一些事情。