Cannon Moyer
Cannon Moyer

Reputation: 3164

Call Context function in helper function - React

Most of my project is built with React class components so I am looking for a solution that won't require me to refactor my class components to functional components. The problem I am having is that I have a file with some helper functions that enable calling an API. In that helper file, I also want to handle errors. One of my errors could result in logging the user out. In my App.js component, I have the following state and functions for logging a user out:

export const AppContext = React.createContext("app");
class App extends Component {
  constructor() {
    super();
    this.state = {
      loggedInStatus: "NOT_INITIALIZED",
      user: {},
    };
  }

 handleLogout = () => {
    request.delete("/logout").then(response => {
      this.setState({
        loggedInStatus: "NOT_LOGGED_IN",
        user: {},
      });
     }).catch(error => {
       console.log("error", error);
     });
  };

  render() {
    return(
        <AppContext.Provider
          value={{
              state: this.state,
              handleLogout: this.handleLogout
          }}
        >
          <BrowserRouter>
             {code that renders certain pages}
          </BrowserRouter>
        </AppContext.Provider>
    )
  }
}

I've also created this HOC:

import React from 'react';
import {AppContext} from '../App.js'

export function withAppContext(Component) {
    return function WrapperComponent(props) {
        return (
            <AppContext.Consumer>
                {state => <Component {...props} context={state} />}
            </AppContext.Consumer>
        );
    };
}

I can import and use this context in any of my class components and call the handleLogout function that is provided via the context. However, I have this helper file and I want to access the handleLogout function in the helper file rather than some child component. Here is my helper file:

import axios from 'axios';

const request = (function(){
  function url(){
    return window.$url;
  }

  return {
    get: function(uri, params={}){
      return axios.get(url() + uri,  {withCredentials: true, params: params})
    },
    post: function(uri, data){
      return axios.post(url() + uri, data, {withCredentials: true})
    },
    patch: function(uri, data){
      return axios.patch(url() + uri, data, {withCredentials: true})
    },
    delete: function(uri){
      return axios.delete(url() + uri, {withCredentials: true})
    },
    handleError: function(error){
      //I want to call the handleLogout function here
    }

  }
})();

export default request;

I currently call these other helper functions from within a class component like so:

import request from "./RequestHelper";


class CustomerForm extends Component{

  constructor(props){
    super(props);
    this.state = {
      isSubmitting: false
    }
  }
    
  handleSubmit = () => {
       request.get("/someurl").catch((err) => {
           //I call request.handleError(err) here
           //which will log the user out depending on the error. 
       });
  }
}

Upvotes: 1

Views: 3368

Answers (2)

Mr. Hedgehog
Mr. Hedgehog

Reputation: 2885

Context is meant to pass props from one component to another. Since in your case you have handleLogout tied to your component, you can't use it outside of components tree.

If you want to use it everywhere in your code and not only in react, you can create some service that you can subscribe to in components, so when you logging out, your components would be notified about it. The most plain and simple implementation would be this:

class LogoutService {
  // you maintain list of subscribers
  subscrribers = new Set();
  // Any part of code can subscribe with callback to do necessary stuff
  subscribe(fn) {
    this.subscribers.add(fn);
    // wheen component unmounts, it should unsubscribe
    return function unsubscribe() {
      this.subscribers.delete(fn);
    };
  }
  // your main function that do stuff and then triggers all subscribers
  logout() {
    return new Promise((resolve) => {
      resolve();
    }).then(() => this.subscribers.forEach((fn) => fn()));
  }
}

// you export this instance which can be imported wherever in your application as a singleton
let logoutService = new LogoutService();

Your compoents now can subscribe to such service and will be notified if logout event occured. And your non components can trigger logout anywhere.

Here is an example where I trigger logout from one react app and it notifies another

Upvotes: 1

Drew Reese
Drew Reese

Reputation: 202696

Instead of creating a self-invoked request, expose out the ability to pass a React context (or any other config object, etc) to it.

For example:

const request = (context = {}) => {
  function url() {
    return window.$url;
  }

  return {
    get: function (uri, params = {}) {
      return axios.get(url() + uri, { withCredentials: true, params: params });
    },
    post: function (uri, data) {
      return axios.post(url() + uri, data, { withCredentials: true });
    },
    patch: function (uri, data) {
      return axios.patch(url() + uri, data, { withCredentials: true });
    },
    delete: function (uri) {
      return axios.delete(url() + uri, { withCredentials: true });
    },
    handleError: function (error) {
      // Access the enclosed context
      if (error === 404) context.logout();
    }
  };
};

Usage

import { context } from './path/to/context';

...

const requestWithContext = request(context);

requestWithContext
  .get("/somePath")
  .catch(requestWithContext.handleError);

Edit call-context-function-in-helper-function-react

I will advise that this appears to unnecessarily couple your axios request utility to your UI/context, in a sub-optimal way. It may be easier/cleaner to incorporate request into your context provider instead, and simply handle the axios failures directly in the .catch block of the Promise chain.

Upvotes: 2

Related Questions