Refactoring a class based component to hooks (gapi API) - reactjs

I am trying to refactor a class based component to a functional, that uses hooks. Am stuck at the right use of useState in gapi when trying to get my login status.
Just to know, what I would like to get:
1) Get a reference to the "auth" object after it is initialized
2) Figure out, if I am currently signed in
3) Print my authentication status on the screen
A similar question was already asked here: LINK but unfortunately I miss to implement the answer into my case.
Here is the class component:
import React from "react";
class GoogleAuth extends React.Component {
componentDidMount() {
window.gapi.load("client: auth2", () => {
window.gapi.client
.init({
clientId: "myVerySecretAPI",
scope: "email"
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance(); //this gets me acces to the auth object
this.setState({ isSignedIn: this.auth.isSignedIn.get() });
});
});
}
renderAuthButton() {
if (this.state.isSignedIn === null) {
return <div>I dont know if we are signed in</div>;
} else if (this.state.isSignedIn) {
return <div>I am signed in!</div>;
} else {
return <div>I am not signed in</div>;
}
}
render() {
return <div>{this.renderAuthButton()}</div>;
}
}
export default GoogleAuth;
An here is my refactored code:
import React, { useEffect, useState } from "react";
const GoogleAuth = () => {
const [ auth, setAuth ] = useState(null);
useEffect(
() => {
window.gapi.load("client:auth2", () => {
window.gapi.client
.init({
clientId: "834243588921-gob95b7q7n3eids3fm8du8oharkv5od0.apps.googleusercontent.com",
scope: "email"
})
.then(() => {
setAuth(window.gapi.auth2.getAuthInstance());
auth({ isSignedIn: setAuth.isSignedIn.get() }); // this is the line where I get an error (please see below)
});
});
},
[ auth ]
);
const renderAuthButton = (isSignedIn) => {
if (isSignedIn === null) {
return <div>I dont know if we are signed in</div>;
} else if (isSignedIn) {
return <div>I am signed in!</div>;
} else {
return <div>I am not signed in</div>;
}
};
return <div>{renderAuthButton()}</div>;
};
export default GoogleAuth;
I get an error in chrome, saying that:
Uncaught TypeError: Cannot read property 'get' of undefined at GoogleAuth.js:16
I know, that I am missing a small something to understand the use of hooks.
Any help is welcome!

So you are basically saving a couple items. The first is the actual auth instance you get from window.gapi.auth2.getAuthInstance() and the second is the authenticated state, isSignedIn. In your code you setAuth with the instance, but then try to call the saved instance and pass it some new object with isSignedIn as the key and the state setter as the value. This won't work.
You can first get the instance, and then use the instance to get the auth state from it to save into your state. You should also use an empty dependency array so the effect is run only once when GoogleAuth is mounted.
Additionally, your renderAuthButton function is defined to take an isSignedIn parameter, but you don't pass it anything in the return. Since this is a functional component though you can simply omit the parameter and use the state value in scope.
const GoogleAuth = () => {
const [isSignedIn, setIsSignedIn ] = useState(null);
useEffect(
() => {
window.gapi.load("client:auth2", () => {
window.gapi.client
.init({
clientId: "834243588921-gob95b7q7n3eids3fm8du8oharkv5od0.apps.googleusercontent.com",
scope: "email"
})
.then(() => {
const authInstance = window.gapi.auth2.getAuthInstance();
setIsSignedIn(authInstance.isSignedIn.get());
});
});
},
[]
);
const renderAuthButton = () => {
if (isSignedIn === null) {
return <div>I don't know if we are signed in</div>;
} else if (isSignedIn) {
return <div>I am signed in!</div>;
} else {
return <div>I am not signed in</div>;
}
};
return <div>{renderAuthButton()}</div>;
};

Related

How to test a component that is conditionally rendered based on a hook value?

I am working on a React Native application and am very new to testing. I am trying to mock a hook that returns a true or false boolean based on the current user state. I need to mock the return value of the authState variable, and based on that, I should check if the component is rendered or not. But the jest mock is returning the same value only
useAuth.ts
export const useAuthState = () => {
const [authState, setAuthState] = useState<AuthState>();
useEffect(() => {
return authentication.subscribe(setAuthState);
}, []);
return authState;
};
MyComponent.tsx
export const MyComponent = () => {
const authState = useAuthState();
if (!authState) {
return null;
}
return <AnotherComponent />
}
MyComponent.test.tsx
import { MyComponent } from "./MyComponent"
jest.mock('../use-auth-state', () => {
return {
useAuthState: () => false,
};
});
const TestComponent = () => <MyComponent />
describe('MyComponent', () => {
it('Should return null if the authState is null', () => {
let testRenderer: ReactTestRenderer;
act(() => {
testRenderer = create(<TestComponent />);
});
const testInstance = testRenderer.getInstance();
expect(testInstance).toBeNull()
})
})
This is working fine. But, I am not able to mock useAuthState to be true as this false test case is failing. Am I doing it right? I feel like I am messing up something.
You want to change how useAuthState is mocked between tests, right? You can set your mock up as a spy instead and change the mock implementation between tests.
It's also a little more ergonomic to use the render method from react-testing-library. The easiest way would be to give your component a test ID and query for it. Something like the below
import { MyComponent } from "./MyComponent"
import * as useAuthState from '../use-auth-state';
const authStateSpy = jest.spyOn(useAuthState, 'default');
describe('MyComponent', () => {
it('Should return null if the authState is null', () => {
// you can use .mockImplementation at any time to change the mock behavior
authStateSpy.mockImplementation(() => false);
const { queryByTestId } = render(<MyComponent />;
expect(queryByTestId('testID')).toBeNull();
})

React protected route not redirecting to children when authorisation finished

I'm trying to put in a layer of authentication in my protected routes and I've been doing this using the following:
function ProtectedRoute ({ children }) {
const { user } = useContext(AuthContext);
return user.isVerifying ? (
<p>Is loading...</p>
) : user.verified ? children : <Navigate to="/login" />;
}
export default ProtectedRoute;
My user context is initialised with the following information:
{
"user": null,
"verified": false,
"isVerifying": false
}
At the moment, the user authorisation state is checked by a function (authUser), inside of my AuthContext.Provider, that at the moment runs on every render of my components (shown below). This is an asynchronous function that makes an API call to check to see if the user JWT token stored in localStorage is valid.
import React, { createContext, useEffect, useState } from 'react';
import axios from 'axios';
const AuthContext = createContext();
function AuthProvider({children}) {
const [user, setUser] = useState({
"user": null,
"verified": false,
"isVerifying": true
});
async function authUser() {
const userLocal = localStorage.getItem('user');
if (userLocal) {
const userLocalJSON = JSON.parse(userLocal);
const verifyResponse = await verifyToken(userLocalJSON);
if (verifyResponse) {
setUser({"user": userLocalJSON, "verified": true, "isVerifying": false});
} else {
setUser({"user": userLocalJSON, "verified": false, "isVerifying": false});
}
}
}
async function verifyToken(userJSON) {
try {
setUser({"user": null, "verified": false, "isVerifying": true});
// Make API call
if (response.status === 200) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
useEffect(() => {
authUser();
}, [])
return (
<AuthContext.Provider value={{user, authUser}}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
export { AuthProvider };
Upon validating the token, the user context is then updated to verified = true with the information about the user. During this process, the user context isVerifying = true (an equivalent to isLoading) and gets set back to false once the API call is complete.
Now there are a few problems with this:
On every re-render of the DOM, the context information is set back to its defaults. Is there a way around this so that useEffect() only fires upon localStorage changes? I've tried implenting the following solution, which adds a listener to localStorage and then triggers the useEffect when that changes. I could not get this to fire upon localStorage changes. Maybe I did something wrong (see below)?
useEffect(() => {
window.addEventListener('storage', authUser)
return () => {
window.removeEventListener('storage', authUser)
}
}, [])
Because the initial isVerifying = false and the switch to true doesn't occur before the DOM is rendered, the protected route navigates to the login page. Even after the user context is finally set, it remains on the login page. The loading page is never displayed and instead there is an instant redirection to the login page. Unfortunately, I can't set isVerifying = true initially as non-logged in users get stuck on the loading page.
Any suggestions would be really helpful.
To solve question 2:
Your ternary isn't quite doing what you want. It's kind of like saying "if user.isVerifying or !user.verified, navigate". That's not what we want.
Embedded ternaries can be tricky and I usually try to avoid them if I can. Since this component is so small, you don't need to do this with ternaries at all actually. I think this should work:
Set isVerifying to start out true. Then:
function ProtectedRoute ({ children }) {
const { user } = useContext(AuthContext);
if (user.isVerifying) {
return <p>Is loading...</p>
}
if (user.verified === false) {
return <Navigate to="/login" />
}
return children
}
export default ProtectedRoute;
EDIT: You also need to update the verification state in authUser. Noticed how you are only updating the loading state if userLocal is truthy. Best bet is to start out loading, and always call setUser at the end. This isn't the only way to do it but I think it should work:
async function authUser() {
setUser(currentState => {...currentState, isVerifying: true})
const userLocal = localStorage.getItem('user');
if (!userLocal) {
setUser({
user: null,
verified: false,
isVerifying: false,
})
}
const userLocalJSON = JSON.parse(userLocal);
const verifyResponse = await verifyToken(userLocalJSON);
setUser({
user: userLocalJSON,
verified: verifyResponse,
isVerifying: false,
})
}

React hooks - remove from react-router location state when we refresh page

When I am entering one page of the app I pass data through location state using react-router. Then I access it via location.state.myDataObject. When I refresh the page it is still there, while I would like it to be empty. Here's what I've try to use:
const resetLocation = () => {
history.replace({
...location,
state: undefined,
});
};
useEffect(() => {
window.addEventListener('onbeforeunload', resetLocation);
}, []);
Or adding it to unmount action within useEffect but I guess it is not called when refreshing the page:
useEffect(() => {
return function cleanLocationState() {
history.replace({
...this.props.location,
state: undefined,
});
};
}, []);
I think this is the desired behavior of react router. If you want to reset the state then you need to do something like
import React, { useEffect, useCallback } from "react";
import { useLocation, useHistory } from "react-router-dom";
function Home() {
const location = useLocation();
const history = useHistory();
const replaceHistory = useCallback(() => {
history.replace({ ...location, state: undefined });
}, [history]);
useEffect(() => {
window.addEventListener("beforeunload", () => replaceHistory);
return () => {
window.removeEventListener("beforeunload", replaceHistory);
};
}, []);
return (
<div>
<h2>Home</h2>
</div>
);
}
export default Home;
Working example
How about you try the contrary? Store the value on component did mound and delete it from the location. I'm not sure that this is the prettiest solution, but i guess it's the easiest
const [state,setState]=useState();
useEffect(()=>{
setState(location.state);
location.state=undefined;
}, [location])
Try this way:
import React, { useEffect } from "react";
import { useHistory } from "react-router-dom";
function Home() {
const history = useHistory();
function replaceHistory(e) {
if (e) {
e.preventDefault();
delete e.returnValue;
}
history.replace({ ...history.location, state: undefined });
}
console.log("history", history.location);
useEffect(() => {
window.addEventListener("beforeunload", () => replaceHistory);
return () => {
// Reset Location state if we leave this page
replaceHistory();
window.removeEventListener("beforeunload", replaceHistory);
};
}, []);
return (
<div>
<h2>Home</h2>
</div>
);
}
export default Home;
CodesandBox Demo
The default behavior of the react-router will not save the history state after refresh the page so we need to know more about your code to really solve this issue. However, if the state do save, byour first attempt seem to have some flaw by using the history and location of window instead from the props.
function Page(props){
useEffect(() => {
const unloadFunc = () => {
//use the history and the location from the props instead of window
props.history.replace({
...props.location,
state: undefined,
});
}
window.addEventListener('onbeforeunload',unloadFunc);
return ()=>{
window.removeEventListener('onbeforeunload' unloadFunc);
//make history and location as the dependencies of the hook
}, [props.history, props.location]);
return <div></div>
}

Converting a class based component to hooks (gapi API)

I have this class based component using the gapi (Google Auth) API that renders a button and it works:
import React from 'react';
class GoogleAuth extends React.Component {
state = { isSignedIn: null };
componentDidMount() {
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: process.env.REACT_APP_CLIENT_ID,
scope: 'email',
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
this.handleAuthChange();
this.auth.isSignedIn.listen(this.handleAuthChange);
});
});
}
handleAuthChange = () => {
this.setState({ isSignedIn: this.auth.isSignedIn.get() });
};
handleSignIn = () => {
this.auth.signIn();
};
handleSignOut = () => {
this.auth.signOut();
};
renderAuthButton() {
if (this.state.isSignedIn === null) {
return null;
} else if (this.state.isSignedIn) {
return <button onClick={this.handleSignOut}>Sign Out</button>;
} else {
return <button onClick={this.handleSignIn}>Sign in with Google</button>;
}
}
render() {
return <div>{this.renderAuthButton()}</div>;
}
}
export default GoogleAuth;
I'm having a tough time trying to convert this to use hooks. The main issue is this.auth... That's how the class has a reference to window.gapi.auth2.getAuthInstance()
I have tried many different ways including keeping auth in state like:
export default function GoogleAuth() {
const [isSignedIn, setIsSignedIn] = useState(null);
const [auth, setAuth] = useState(null);
useEffect(() => {
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: process.env.REACT_APP_CLIENT_ID,
scope: 'email',
})
.then(() => {
setAuth(window.gapi.auth2.getAuthInstance());
setIsSignedIn(auth.isSignedIn.get());
auth.isSignedIn.listen(() => setIsSignedIn(auth.isSignedIn.get()));
});
});
}, [auth]);
It's only 8 months later but try useRef with auth like below. It works for me.
const GoogleAuth = () => {
const [isSignedIn, setSignedIn] = useState(null)
const auth = useRef(null);
useEffect(() => {
window.gapi.load('client:auth2', () => {
window.gapi.client.init({
clientId:
'jcu.apps.googleusercontent.com',
scope: 'email'
}).then(() => {
auth.current = window.gapi.auth2.getAuthInstance();
setSignedIn(auth.current.isSignedIn.get());
auth.current.isSignedIn.listen(onAuthChange)
});
});
}, [isSignedIn]);
const onAuthChange = () => {
setSignedIn(auth.current.isSignedIn.get())
}
if (isSignedIn === null) {
return (
<div>I don't know if we are signed in!</div>
);
} else if ( isSignedIn ){
return (
<div>I am signed in!</div>
);
} else {
return ( <div>I am not signed in. :(</div>);
}
}
Couple issues - you're referencing auth immediately after you're setting the state - auth won't be set until it re-renders with its new state.
I'm playing with similar code, and I had to resort to utilizing window.gapi in the initial setup to properly access the returned auth instance.
I imagine it may throw an error if a user clicks quickly they could catch it before auth is set, but I found the sign in/out functions to be able to handle this.
I also found it easiest to test in Incognito, as cookies and cacheing of the api seemed to create an unpredictable local testing environment.
My current component state
Just line in useEffect after auth.current = ...
setSignedIn(auth.current.isSignedIn.get()); is de facto function onAuthChange so call it like that:
.then(() => {
auth.current = window.gapi.auth2.getAuthInstance();
onAuthChange();
auth.current.isSignedIn.listen(onAuthChange);
});

Unable accessing properties on React component return

I have a React component getting an item's info and returning JSX:
const detail = props => {
const service = new Services()
const detail = service.findItem(props.match.params.id)
.then(item => {
console.log(item) // logs correct details, including the title property
return item
})
.catch(err => console.log(err))
return (
<h1>{detail.title}</h1> // !! prints nothing inside the <h1> tag
)
}
As seen above, returning object logs correctly all properties, but when trying to access them through JSX, no info is shown.
There are no console errors.
Its because the detail has not yet resolved, you can have React.Component and use
export class detail extends React.Component {
state = {
item: {}
}
componentDidMount(){
const service = new Services()
const detail = service.findItem(props.match.params.id)
.then(item => {
this.setState({ item:item });
})
.catch(err => console.log(err))
}
render() {
return <h1>{this.state.item.title}</h1>
}
}

Resources