Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using React context to maintain user state

I'm trying to use React's context feature to maintain information about the user throughout the application (e.g. the user ID, which will be used in API calls by various pages). I'm aware that this is an undocumented and not recommended over Redux, but my application is pretty simple (so I don't want or need the complexity of Redux) and this seems like a common and reasonable use case for context. If there are more acceptable solutions for keeping user information globally throughout the application, though, I'm open to using a better method.

However, I'm confused about how it's to be used properly: once the user logins in through the AuthPage (a child of the ContextProvider), how do I update the context in ContextProvider so it can get to other components, like the FridgePage? (Yes, context is technically not supposed to be updated, but this is a one-time operation -- if anyone knows a way to do this when ContextProvider is initialized, that would be more ideal). Does the router get in the way?

I've copied the relevant components here.

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 (where I want to access 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>
        );
    }
}
like image 569
Jess Avatar asked Jul 27 '17 15:07

Jess


2 Answers

Simple state provider

auth module provides two functions:

withAuth - higher order component to provide authentication data to components that need it.

update - function for updating authentication status

How it works

The basic idea is that withAuth should add auth data to props that are being passed to a wrapped component. It is done in three steps: take props that being passed to a component, add auth data, pass new props to the component.

let state = "initial state"

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

One piece that is missing is to rerender the component when the auth state changes. There are two ways to rerender a component: with setState() and forceUpdate(). Since withAuth doesn't need internal state, we will use forceUpdate() for rerendering.

We need to trigger a component rerender whenever there is a change in auth state. To do so, we need to store forceUpdate() function in a place that is accesible to update() function that will call it whenever auth state changes.

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())
}

Last step is to add code that will remove rerender function when a component is going to be unmounted

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()
    }
  }

// 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"));
<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>

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

like image 167
marzelin Avatar answered Sep 22 '22 14:09

marzelin


here is what i did for my project:

// 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)

and then use it like this:

setting up the provider:

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

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

export default App

and using the context in components:

...
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()
  }
  // ...
}
...

the full source code is available on github: https://github.com/dorianmarie/emojeet

and the project can be tried live at: http://emojeet.com/

like image 27
Dorian Avatar answered Sep 20 '22 14:09

Dorian