Home > OS >  What's the alternative to use hooks inside non React component?
What's the alternative to use hooks inside non React component?

Time:09-27

I'm new to React and I have this function.

    import Axios from "axios";
    
    const UserService = {
        getUserRole: (access_token: string = "") => {
            return Axios({
                method: "get",
                url: "https://<url>/user/role",
                headers: {
                    "Authorization": `Bearer ${access_token}`
                }
            }).then((response) => {
                return response.data;
            }).catch((error) => {
                console.log(error);
            });
        }
    }

export default UserService

The getUserRole is used constantly by another component, for example

import UserService from "../../../services/authentication/userService";
import { useAuth } from "react-oidc-context";

...

const auth = useAuth();
UserService.getUserRole(auth.user?.access_token);

As you can see, I have to constantly pass the access_token from useAuth. Is there any way I can call useAuth inside my UserService so I don't have to constantly pass the access_token from my component?

CodePudding user response:

The premise of the question is backward, as we shouldn't try to use hooks outside of React, but instead use outside code inside of React.

Quick solution: Custom hook

If the roles are used all over the place, a quick custom hook will get you started. This is the easiest way to wrap custom logic as hooks are meant to wrap stateful logic for reuse in components.

import ­{ useState, useEffect } from "react";
import { useAuth } from "react-oidc-context";
import UserService from "../../../services/authentication/userService";

/**
 * Custom hooks that fetches the roles for the logged in user.
 */
const useRoles = () => {
  const auth = useAuth();
  const [roles, setRoles] = useState();

  useEffect(() => {
    if (!user) return; // pre-condition
    UserService
      .getUserRole(auth.user.access_token)
      .then(setRoles);
  }, [auth.user]);

  return roles;
}

Then in any component:

import useRoles from "../useRoles";

const MyExampleComponent = () => {
  const roles = useRoles();

  if (!roles) return <span>Please login (or something) to see the roles!</span>

  return <div>{/* use roles here */}</div>
}

Better solution: Service provider

If there's a lot of different methods on the user service that needs to be used all over the app, then wrapping the whole service and providing a ready-to-use version through React's context would be best in my opinion.

But first, let's rework the UserService a little so that it uses a local axios instance instead of the global axios instance.

// I also made it a class, but it would also work with an object.
class UserService {
  constructor(axios) {
    this.axios = axios;
  }

  getUserRole(){
    // use the local axios instance
    return this.axios({
      method: "get",
      // Use the default URL from local axios instance 
      url: "user/role",
    })
      .then(({ data }) => data)
      .catch(console.log),
  }

  getSomethingElse() {
    // ...
  }
}

Then, we can setup the React's context for the user service.

// UserServiceContext.js
import React from 'react';
import { useAuth } from "react-oidc-context";
import UserService from "../../../services/authentication/userService";

const UserServiceContext = React.createContext(null);

// Convenience hook
export const useUserService = () => useContext(UserServiceContext);

// Local axios instance
const axiosInstance = axios.create({
  baseURL: 'https://<url>', // set the base URL once here
  headers: {
    "Authorization": `Bearer ${access_token}`
  }
});

const userServiceInstance = new UserService(axiosInstance);

export const UserServiceProvider = (props) => {
  const auth = useAuth();

  useEffect(() => {
    // If the user changes, update the token used by our local axios instance.
    axiosInstance.defaults.headers
      .common['Authorization'] = auth.user?.access_token;
  }, [auth.user]);

  return <UserServiceContext.Provider value={userServiceInstance} {...props} />;  
}

Then anywhere, but commonly at the App's root:

import { AuthProvider } from "react-oidc-context";
import { UserServiceProvider } from "./UserServiceContext";

const App = () => (
  <AuthProvider>
    <UserServiceProvider>
      <Content />
    </UserServiceProvider>
  </AuthProvider>
);

Now everything is ready to be used in any component!

import { useUserService } from '../UserServiceContext';

const MyExampleComponent = () => {
  const userService = useUserService();
  const [roles, setRoles] = useState();

  // e.g. load roles once on mount.
  useEffect(() => {
    userService // use the service from the context
      .getUserRole() // no auth token needed anymore!
      .then(setRoles);
  }, []);

  if (!roles) return <span>Please login (or something) to see the roles!</span>

  return <div>{/* use roles here */}</div>
}

Note that a custom hook could still be used to wrap the roles fetching logic. Both the context and hooks can be used together to wrap logic to each's own preferences.


Disclaimer: I've used the minimal code to demonstrate a working solution, though there are some pros and cons to each implementation and the one in my answer may not address all constraints of your situation. For example, the axios instance could be created inside the provider within a useMemo, same thing goes for the UserService instance, etc.

  • Related