使用React上下文来维护用户状态

时间:2017-07-27 15:19:04

标签: reactjs react-router

我正在尝试使用React的上下文功能来维护整个应用程序中的用户信息(例如,用户ID,将在各种页面的API调用中使用)。我知道这是一个没有文档的,不推荐使用Redux,但我的应用程序非常简单(所以我不想或者不需要Redux的复杂性),这似乎是一个常见且合理的上下文用例。但是,如果有更多可接受的解决方案可以在整个应用程序中保持全局用户信息,那么我愿意使用更好的方法。

但是,我对如何正确使用它感到困惑:一旦用户通过AuthPage(ContextProvider的子代)登录,我如何更新ContextProvider中的上下文,以便它可以到达其他组件,如冰箱页面? (是的,上下文在技术上不应该更新,但这是一次性操作 - 如果有人知道在ContextProvider初始化时这样做的方法,那将更加理想)。路由器是否妨碍了?

我在这里复制了相关的组件。

index.js

import React from 'react'; 
import ReactDOM from 'react-dom';
import { HashRouter, Route, Switch } from 'react-router-dom';

import Layout from './components/Layout.jsx';
import AuthPage from './components/AuthPage.jsx';
import ContextProvider from './components/ContextProvider.jsx';

ReactDOM.render(
    <ContextProvider>
        <HashRouter>
            <Switch>
                <Route path="/login" component={AuthPage} />
                <Route path="/" component={Layout} />
            </Switch>
        </HashRouter>
    </ContextProvider>,
    document.getElementById('root')
);

ContextProvider.jsx

import React from 'react';
import PropTypes from 'prop-types';

export default class ContextProvider extends React.Component {
    static childContextTypes = {
        user: PropTypes.object
    }

    // called every time the state changes
    getChildContext() {
        return { user: this.state.user };
    }

    render() {
        return(
            <div>
                { this.props.children }
            </div>
        );
    }
}

AuthPage.jsx

import React from 'react';
import PropTypes from 'prop-types';

import AuthForm from './AuthForm.jsx';
import RegisterForm from './RegisterForm.jsx';
import Api from '../api.js';

export default class AuthPage extends React.Component { 
    static contextTypes = {
        user: PropTypes.object
    }

    constructor(props) {
        super(props);
        this.updateUserContext = this.updateUserContext.bind(this);
    }

    updateUserContext(user) {
        console.log("Updating user context");
        this.context.user = user;
        console.log(this.context.user);
    }

    render() {
        return (
            <div>
                <AuthForm type="Login" onSubmit={Api.login} updateUser={this.updateUserContext} />
                <AuthForm type="Register" onSubmit={Api.register} updateUser={this.updateUserContext} />
            </div>
        );
    }
}

Layout.jsx

import React from 'react';
import Header from './Header.jsx';
import { Route, Switch } from 'react-router-dom';

import FridgePage from './FridgePage.jsx';
import StockPage from './StockPage.jsx';

export default class Layout extends React.Component {
    render() {
        return (
            <div>
                <Header />
                <Switch>
                    <Route exact path="/stock" component={StockPage} />
                    <Route exact path="/" component={FridgePage} />
                </Switch>
            </div>
        );
    }
}

FridgePage.jsx(我要访问的地方this.context.user

import React from 'react';
import PropTypes from 'prop-types';

import Api from '../api.js';

export default class FridgePage extends React.Component {
    static contextTypes = {
        user: PropTypes.object
    }

    constructor(props) {
        super(props);

        this.state = {
            fridge: []
        }
    }

    componentDidMount() {
        debugger;
        Api.getFridge(this.context.user.id)
            .then((fridge) => {
                this.setState({ "fridge": fridge });
            })
            .catch((err) => console.log(err));
    }

    render() {
        return (
            <div>
                <h1>Fridge</h1>
                { this.state.fridge }
            </div>
        );
    }
}

4 个答案:

答案 0 :(得分:2)

简单状态提供程序

auth模块提供了两个功能:

withAuth - 更高阶的组件,用于向需要它的组件提供身份验证数据。

update - 更新身份验证状态的功能

工作原理

基本思想是withAuth应该将auth数据添加到传递给包装组件的props。 它分三步完成:获取传递给组件的道具,添加auth数据,将新道具传递给组件。

let state = "initial state"

const withAuth = (Component) => (props) => {
  const newProps = {...props, auth: state }
  return <Component {...newProps} />
}

缺少的一件事是在auth状态改变时重新渲染组件。有两种方法可以重新呈现组件:使用setState()forceUpdate()。由于withAuth不需要内部状态,我们将使用forceUpdate()进行重新渲染。

我们需要在auth状态发生变化时触发组件重新渲染。为此,我们需要将forceUpdate()函数存储在update()函数可访问的位置,该函数将在验证状态发生变化时调用它。

let state = "initial state"

// this stores forceUpdate() functions for all mounted components
// that need auth state
const rerenderFunctions = []

const withAuth = (Component) =>
    class WithAuth extends React.Component {
    componentDidMount() {
        const rerenderComponent = this.forceUpdate.bind(this)
        rerenderFunctions.push(rerenderComponent)
    }
    render() {
      const newProps = {...props, auth: state }
      return <Component {...newProps} />
    }
  }

const update = (newState) => {
    state = newState
  // rerender all wrapped components to reflect current auth state
  rerenderFunctions.forEach((rerenderFunction) => rerenderFunction())
}

最后一步是添加代码,以便在卸载组件时删除重新渲染功能

let state = "initial state"

const rerenderFunctions = []

const unsubscribe = (rerenderFunciton) => {
  // find position of rerenderFunction
  const index = subscribers.findIndex(subscriber);
  // remove it
  subscribers.splice(index, 1);
}

const subscribe = (rerenderFunction) => {
  // for convinience, subscribe returns a function to
  // remove the rerendering when it is no longer needed
  rerenderFunctions.push(rerenderFunction)
  return () => unsubscribe(rerenderFunction)
}

const withAuth = (Component) =>
    class WithAuth extends React.Component {
    componentDidMount() {
        const rerenderComponent = this.forceUpdate.bind(this)

        this.unsubscribe = subscribe(rerenderComponent)
    }
    render() {
      const newProps = {...props, auth: state }
      return <Component {...newProps} />
    }
    componentWillUnmount() {
        // remove rerenderComponent function
        // since this component don't need to be rerendered
        // any more
        this.unsubscribe()
    }
  }

&#13;
&#13;
// auth.js

let state = "anonymous";

const subscribers = [];

const unsubscribe = subscriber => {
  const index = subscribers.findIndex(subscriber);
  ~index && subscribers.splice(index, 1);
};
const subscribe = subscriber => {
  subscribers.push(subscriber);
  return () => unsubscribe(subscriber);
};

const withAuth = Component => {
  return class WithAuth extends React.Component {
    componentDidMount() {
      this.unsubscribe = subscribe(this.forceUpdate.bind(this));
    }
    render() {
      const newProps = { ...this.props, auth: state };
      return <Component {...newProps} />;
    }
    componentWillUnmoount() {
      this.unsubscribe();
    }
  };
};

const update = newState => {
  state = newState;
  subscribers.forEach(subscriber => subscriber());
};

// index.js

const SignInButton = <button onClick={() => update("user 1")}>Sign In</button>;
const SignOutButton = (
  <button onClick={() => update("anonymous")}>Sign Out</button>
);
const AuthState = withAuth(({ auth }) => {
  return (
    <h2>
      Auth state: {auth}
    </h2>
  );
});

const App = () =>
  <div>
    <AuthState />
    {SignInButton}
    {SignOutButton}
  </div>;

ReactDOM.render(<App />, document.getElementById("root"));
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
&#13;
&#13;
&#13;

playground:https://codesandbox.io/s/vKwyxYO0

答案 1 :(得分:0)

我想我不会使用上下文来实现这一点。

即使您的应用很简单(我知道您不想使用Redux),将模型与视图分开也是一种很好的做法。 考虑实现一个非常简单的Flux架构:每次必须更改模型时创建存储和调度操作(例如,存储用户)。您的视图只需要监听商店事件并更新其DOM。

https://facebook.github.io/flux/docs/in-depth-overview.html#content

这是一个带有小助手的样板来管理Flux:https://github.com/christianalfoni/flux-react-boilerplate/blob/master/package.json

答案 2 :(得分:0)

您不更新上下文,更新ContextProvider的状态,该状态将重新呈现子项并通过getChildContext填充上下文;在您的上下文中,您可以放置​​在调用时更新提供程序状态的函数。确保您还创建一个名为withAuthContext的高阶组件(HOC),它将读取上下文并将其转换为道具以供子组件使用,就像withIntl from react-intlwithRouter from react-router一样在许多其他方面,这将使您的组件的开发更简单并且与上下文无关,就像在某些时候您决定转移到redux一样,您不必处理上下文,只需用connect和mapStateToProps替换HOC。

答案 3 :(得分:0)

这是我为我的项目所做的:

// src/CurrentUserContext.js
import React from "react"

export const CurrentUserContext = React.createContext()

export const CurrentUserProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = React.useState(null)

  const fetchCurrentUser = async () => {
    let response = await fetch("/api/users/current")
    response = await response.json()
    setCurrentUser(response)
  }

  return (
    <CurrentUserContext.Provider value={{ currentUser, fetchCurrentUser }}>
      {children}
    </CurrentUserContext.Provider>
  )
}

export const useCurrentUser = () => React.useContext(CurrentUserContext)

然后像这样使用它:

设置提供程序:

// ...
import { CurrentUserProvider } from "./CurrentUserContext"
// ...

const App = () => (
  <CurrentUserProvider>
    ...
  </CurrentUserProvider>
)

export default App

并在组件中使用上下文:

...
import { useCurrentUser } from "./CurrentUserContext"

const Header = () => {
  const { currentUser, fetchCurrentUser } = useCurrentUser()

  React.useEffect(() => fetchCurrentUser(), [])

  const logout = async (e) => {
    e.preventDefault()

    let response = await fetchWithCsrf("/api/session", { method: "DELETE" })

    fetchCurrentUser()
  }
  // ...
}
...

完整的源代码可在github上找到:https://github.com/dorianmarie/emojeet

,可以在http://emojeet.com/

上尝试该项目。