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

Posted

技术标签:

【中文标题】React 上下文需要 2 次状态更新才能让消费者重新渲染【英文标题】:React context requires 2 state updates for consumers to re-render 【发布时间】:2021-04-08 17:58:14 【问题描述】:

所以我有一个简单的应用程序,需要您登录才能查看仪表板。我基于https://reactrouter.com/web/example/auth-workflow 的身份验证流程,这反过来又基于https://usehooks.com/useAuth/ 的流程。

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

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

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/clas-s-room" exact>
                    <Clas-s-room handleToaster=handleToaster />
                </Route>
                <Route path="/dashboard/progres-s-report" 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 项目。谢谢。

【问题讨论】:

如果有简单的代码沙箱和调试代码会很好 【参考方案1】:

我认为问题出在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 状态是否已更新。

我已经为你的 home 组件制作了一个简化版本,以举例说明你如何解决这个问题

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 是否有值并对其进行处理。

【讨论】:

你成功了,非常感谢你花时间帮助我。有趣的是,我的大脑在午夜后无法正常工作。我刚刚在 Home.js 中添加了 useEffect,如果上下文教师值存在,它会重定向到仪表板。

以上是关于React 上下文需要 2 次状态更新才能让消费者重新渲染的主要内容,如果未能解决你的问题,请参考以下文章

React Native - 在消费者中更新上下文仅在第一次工作

如何更新使用两个上下文消费者的组件的状态

React Context 未将类更新为值

React:第二次点击时更新上下文

在 React 上下文中更新嵌套状态的最佳方法

在消费者之外更新反应上下文?