How to authenticate user with Context API? - javascript

I am having trouble logging in to my application using the Context API. When I run applications without having any token in my localStorage in the variable session I get a lot of errors like below:
Uncaught TypeError: Cannot read properties of null (reading 'name')
I think that this problem exists because my currentAccount from ApplicationContext is null.
dashboard/index.tsx
const { currentAccount } = useContext(ApplicationContext);
return (
<span>{currentAccount.name}</span>
);
On the routes.login login page I am also getting these exceptions even though this error should only be on routes.dashboard :/ Refreshing the page or clearing localStorage does not help. I;m having also an infinite loop over checkLogin in ApplicationContextProvider :(
login/index.tsx
const { setCurrentAccount } = useContext(ApplicationContext);
const onFinish = async (email: string; password: string) => {
try {
const response = await axios.post("/auth/login", {
email: email,
password: password
});
const token = response["token"];
const account = response["account"];
if (token && account) {
localStorage.setItem("session", token);
setCurrentAccount(account);
history.push(routes.dashboard)
}
} catch (error) {
}
};
App.tsx
return (
<div className="App">
<Switch>
<ApplicationContextProvider>
<Route path={route.login} component={Login} />
<Main>
<Route path={route.dashboard} component={Dashboard} />
</Main>
</ApplicationContextProvider>
</Switch>
</div>
);
ApplicationContextProvider.tsx
export type AccountContext = {
currentAccount?: Account;
setCurrentAccount: (user: Account) => void;
checkLogin: () => void;
};
export const ApplicationContext = React.createContext<AccountContext>(null);
interface ProviderProps {
children: React.ReactNode
}
export const ApplicationContextProvider = ({ children }: ProviderProps) => {
const [currentAccount, setCurrentAccount] = useState<Account>(null);
useEffect(() => {
checkLogin();
}, [currentAccount]);
const checkLogin = async () => {
const token = localStorage.getItem("session");
if (token) {
const token = localStorage.getItem("session");
const decode = jwt(token);
const query = {
id: decode["id"]
}
const response: Account = await api.get("/auth/account", query);
setCurrentAccount(response);
} else {
setCurrentAccount(null);
}
};
const stateValues = {
currentAccount,
setCurrentAccount,
checkLogin
};
return (
<ApplicationContext.Provider value={stateValues}>
{children}
</ApplicationContext.Provider>
);
Can someone tell me what is wrong with my context logic to authentication user to application?
Thanks for any help!

I already know what's going on, maybe it will be useful to someone in the future.
I have to add at two important things. For the first useEffect() function need to have an empty dependency array like below:
useEffect(() => {
checkLogin();
}, [ ]);
and for second I have to add also state to store loading like below:
const [loading, setLoading] = useState(false)
and set correct values when i've got data from api and so on

Related

I have a type script error in a use of useContext in react

I initialized a value of createContext with {}, then I provided a context with a new value {username: 'sasuke1'}. Then when I try to access the property of Context.username TypeScript gives me this error:
Property 'username' does not exist on type '{}'. return <div> {userInfo.username === userInfoContext.**username**? </div>
export const UserContext = createContext({})
function App() {
const userInfoUrl = 'http://localhost:3000/api/user-info'
const [userInfo, setUserInfo] = useState({username:''})
const [finishedLoading, setFinishedLoading] = useState(false)
const navigate = useNavigate() // redirect to other components
const location = useLocation() // react router hook
useEffect(()=>{
fetch(userInfoUrl, {credentials: 'include'}).then(async res=>{
if(res.status!==200){
navigate('/login')
return
}
setUserInfo(await res.json())
navigate('/feed')
}).catch(err=>{
navigate('/login')
}).finally(()=>{
setFinishedLoading(true)
})
},[])
useEffect(()=>{
if (finishedLoading && !userInfo && location.pathname !== '/login') {
navigate('/login')
}
},[location])
return <div style={{ backgroundColor:'#FAFAFA', height: '100vh'}}>
{/* <Navbar/> */}
<UserContext.Provider value={userInfo}>
<nav>
<ul>
<li><Link to={'/register'}>Registration</Link></li>
<li><Link to={'/login'}>Login</Link></li>
<li><Link to={'/feed'}>Feed</Link></li>
<li><Link to={'/user/:username'}>User</Link></li>
</ul>
</nav>
<Outlet></Outlet>
</UserContext.Provider>
</div>;
}
export function User({ user }: { user?: {} }) {
const userParams = useParams()
const [userInfo, setUserInfo] = useState({username: '', email: '', following: []})
const userInfoContext = useContext(UserContext)
let noUser = ''
useEffect(() => {
console.log(userInfoContext)
const username = userParams.username
// const searchUserAPI = async () => { }
fetch(`http://localhost:3000/api/users/${username}`, { credentials: 'include', mode: 'cors', })
.then((res) => {
return res.json()
})
.then(async (res) => {
await setUserInfo(res)
return res
})
.then(()=>{
console.log(userInfo)
})
.catch(()=>{
noUser = "Sorry, this page isn't available.\nThe link you followed may be broken, or the page may have been removed. Go back to Instagram."
})
}, [])
return <div>
{userInfo.username === userInfoContext.username?
<h1>{JSON.stringify(userInfo)}</h1> : <h1>{noUser}</h1>}
</div>
Seems like createContext is defaulting its type to whatever it was initialized with, in this case an empty object {} which has no properties.
To fix this error you can set the type explicitly e.g createContext<{username: string}>({}) or just set the type to any createContext<any>({}). Obviously you won't get the benefits of typescript setting the type to any, but it will fix the error and behave as expected.
Hope this helps! :)

Private Route Conditional Operator in ReactJS

Seems like a trivial issue.
I have an app, where people can subscribe through "stripe". Would like to give access to a few URLs based on subscription, otherwise taking them back to the "profile" page.
A couple of things are not working.
Firebase query to get subscription is not giving results on the subscription. Somehow the onSnapShot does not fetch anything. Probably since UID is null at the start of rendering of page.
Conditional operator on is not working. Not sure what the problem is on this one.
function PaidRoutes(props) {
const [subscription, setSubscription] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const unsub = auth.onAuthStateChanged((authObject) => {
unsub();
if (authObject) {
setLoading(true);
const uid = auth.currentUser?.uid;
console.log('UID ==>', uid);
let docRef = query(collection(db, 'customers', uid, 'subscription'));
console.log('DOC REF ==> ', docRef);
onSnapshot(docRef, (snap) => {
snap.forEach((doc) => {
console.log('Role of Subscription', doc.data().role);
setSubscription(doc.data().role);
});
});
} else {
console.log('not logged in');
setLoading(false);
}
});
return () => {
unsub();
};
}, []);
return (
<Route
{...props}
render={(props) =>
subscription ? (
<Component {...props} />
) : (
<Redirect to='/profileScreen' />
)
}
/>
);
Thanks Drew for comment. The installed version of react-router-dom is 5.2.0
Issue(s)
Potential issues I see in the PaidRoute code:
The unsubscribe function is called in the auth state handler, this might allow the auth check to work once on an initial auth change, but then will unsubscribe itself and stop working until the component remounts.
The loading state is initially false so any check based on it on the initial render won't do what you want. The loading also isn't used to hold off on the conditional rendering.
The subscription state is initially an empty array which is still truthy, so the conditional logic checking it will likely allow access to the protected route anyway, regardless of any auth status.
Solution
Don't unsubscribe from the auth change listener in the callback.
Start with loading initially true to handle the initial render cycle. Conditionally render null or some loading indicator.
Start with null subscription state.
Code:
function PaidRoutes(props) {
const [subscription, setSubscription] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((authObject) => {
if (authObject) {
setLoading(true);
const uid = auth.currentUser?.uid;
console.log('UID ==>', uid);
const docRef = query(collection(db, 'customers', uid, 'subscription'));
console.log('DOC REF ==> ', docRef);
onSnapshot(docRef, (snap) => {
snap.forEach((doc) => {
console.log('Role of Subscription', doc.data().role);
setSubscription(doc.data().role);
});
});
} else {
console.log('not logged in');
setSubscription(null); // <-- reset auth state
}
setLoading(false); // <-- clear loading state outside if-else
});
return unsubscribe;
}, []);
if (loading) {
return null; // or loading indicator, spinner, etc...
}
return subscription ? (
<Route {...props} />
) : (
<Redirect to='/profileScreen' />
);
}

Get values from url using useParams

I have a React form with Material-UI. I would like to get the id from the URL link using useParams and make some API requests in order to populate form-data:
http://localhost:3001/profile?ids=9459377
Main app.tsx:
function App() {
return (
<Router>
<Navbar />
<Switch>
<Route path='/ticket-profile/:ids' component={TicketProfile} />
</Switch>
</Router>
);
}
I use this code to open a new page and pass ids param:
history.push(`/ticket-profile/ids=${id}`)
I need to get the data into this page:
export default function TicketProfile(props: any) {
let { ids } = useParams();
const [ticket, setTicket] = useState<TicketDTO[]>([]);
useEffect(() => {
getSingleTicket();
}, []);
const getSingleTicket = async () => {
getTicket(ids)
.then((resp) => {
setTicket(resp.data);
})
.catch((error) => {
console.error(error);
});
}
But for this line let { ids } I get:
TS2339: Property 'ids' does not exist on type '{}'.
Do you know how I can fix this issue?
So this is the url
http://localhost:3001/profile?ids=9459377
In your code
const MyComponent = () => {
const params = new URLSearchParams(window.location.search);
That's it! Now we should move on to getting the value and checking the existence of the query strings
Check if it has the query;
params.has('ids')
or get the value that is inside the query string
params.get('ids')
You can also show them conditionally
console.log(params.has('ids')?params.get('ids'):"")
Update:
Check out the working example
https://codesandbox.io/s/peaceful-https-vz9y3?file=/src/App.js\
This is how we should use it in your case
export default function TicketProfile(props: any) {
const params = new URLSearchParams(window.location.search);
const ids = params.get('ids');
const [ticket, setTicket] = useState<TicketDTO[]>([]);
useEffect(() => {
getSingleTicket();
}, []);
const getSingleTicket = async () => {
getTicket(ids)
.then((resp) => {
setTicket(resp.data);
})
.catch((error) => {
console.error(error);
});
}
I guess you are using useParams from react-router. If so then try so here
let { ids } = useParams<{ ids: Array<any>}>();
You are pushing a wrong path to the history. Try it like this:
history.push(`/ticket-profile/${id}`)

Routing with Spotify API

I am developing a spotify clone with the ability to play a preview of the songs and display user's different top tracks and artists. I have already made standalone pages for the website after authorizing with the help spotify-web-api-node package, but i am kinda facing a problem connecting the routers, after i login with spotify i reach my profile page where i have links to other pages, but when i try to go to another page i get an error on the server that it is an invalid authorization code and on the web console, the package throws an error that no access token was provided. I have tried every possible way to correct this but i am not able to do anything. Please help me out. The relevant code as well the whole GitHub repository is linked below:
The Github repository for this project is https://github.com/amoghkapoor/Spotify-Clone
App.js
const code = new URLSearchParams(window.location.search).get("code")
const App = () => {
return (
<>
{code ?
<Router>
<Link to="/tracks">
<div style={{ marginBottom: "3rem" }}>
<p>Tracks</p>
</div>
</Link>
<Link to="/">
<div style={{ marginBottom: "3rem" }}>
<p>Home</p>
</div>
</Link>
<Switch>
<Route exact path="/">
<Profile code={code} />
</Route>
<Route path="/tracks">
<TopTracks code={code} />
</Route>
</Switch>
</Router> : <Login />}
</>
)
}
TopTracks.js
const spotifyApi = new SpotifyWebApi({
client_id: "some client id"
})
const TopTracks = ({ code }) => {
const accessToken = useAuth(code)
console.log(accessToken) // undefined in console
console.log(code) // the correct code as provided by spotify
useEffect(() => {
if (accessToken) {
spotifyApi.setAccessToken(accessToken)
return
}
}, [accessToken])
'useAuth' custom Hook
export default function useAuth(code) {
const [accessToken, setAccessToken] = useState()
const [refreshToken, setRefreshToken] = useState()
const [expiresIn, setExpiresIn] = useState()
useEffect(() => {
axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
setAccessToken(res.data.accessToken)
setRefreshToken(res.data.refreshToken)
setExpiresIn(res.data.expiresIn)
window.history.pushState({}, null, "/")
})
.catch((err) => {
// window.location = "/"
console.log("login error", err)
})
}, [code])
You don't appear to be persisting your access/refresh tokens anywhere. As soon as the component is unloaded, the data would be discarded. In addition, a sign in code is only usable once. If you use it more than once, any OAuth-compliant service will invalidate all tokens related to that code.
You can persist these tokens using localStorage, IndexedDB or another database mechanism.
For the purposes of an example (i.e. use something more secure & permanent than this), I'll use localStorage.
To help manage state across multiple views and components, you should make use of a React Context. This allows you to lift common logic higher in your component tree so that it can be reused.
Furthermore, instead of using setInterval to refresh the token periodically, you should only perform refresh operations on-demand - that is, refresh it when it expires.
// SpotifyAuthContext.js
import SpotifyWebApi from 'spotify-web-api-node';
const spotifyApi = new SpotifyWebApi({
clientId: 'fcecfc72172e4cd267473117a17cbd4d',
});
export const SpotifyAuthContext = React.createContext({
exchangeCode: () => throw new Error("context not loaded"),
refreshAccessToken: () => throw new Error("context not loaded"),
get hasToken: spotifyApi.getAccessToken() !== undefined,
api: spotifyApi
});
export const useSpotify = () => useContext(SpotifyAuthContext);
function setStoredJSON(id, obj) {
localStorage.setItem(id, JSON.stringify(obj));
}
function getStoredJSON(id, fallbackValue = null) {
const storedValue = localStorage.getItem(id);
return storedValue === null
? fallbackValue
: JSON.parse(storedValue);
}
export function SpotifyAuthContextProvider({children}) {
const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
const hasToken = tokenInfo !== null
useEffect(() => {
if (tokenInfo === null) return; // do nothing, no tokens available
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
})
// persist tokens
setStoredJSON('myApp:spotify', tokenInfo)
}, [tokenInfo])
function exchangeCode(code) {
return axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
// TODO: Confirm whether response contains `accessToken` or `access_token`
const { accessToken, refreshToken, expiresIn } = res.data;
// store expiry time instead of expires in
setTokenInfo({
accessToken,
refreshToken,
expiresAt: Date.now() + (expiresIn * 1000)
});
})
}
function refreshAccessToken() {
return axios
.post("http://localhost:3001/refresh", {
refreshToken
})
.then(res => {
const refreshedTokenInfo = {
accessToken: res.data.accessToken,
// some refreshes may include a new refresh token!
refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
// store expiry time instead of expires in
expiresAt: Date.now() + (res.data.expiresIn * 1000)
}
setTokenInfo(refreshedTokenInfo)
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: refreshedTokenInfo.accessToken,
refreshToken: refreshedTokenInfo.refreshToken,
})
return refreshedTokenInfo
})
}
async function refreshableCall(callApiFunc) {
if (Date.now() > tokenInfo.expiresAt)
await refreshAccessToken();
try {
return await callApiFunc()
} catch (err) {
if (err.name !== "WebapiAuthenticationError")
throw err; // rethrow irrelevant errors
}
// if here, has an authentication error, try refreshing now
return refreshAccessToken()
.then(callApiFunc)
}
return (
<SpotifyAuthContext.Provider value={{
api: spotifyApi,
exchangeCode,
hasToken,
refreshableCall,
refreshAccessToken
}}>
{children}
</SpotifyAuthContext.Provider>
)
}
Usage:
// TopTracks.js
import useSpotify from '...'
const TopTracks = () => {
const { api, refreshableCall } = useSpotify()
const [ tracks, setTracks ] = useState([])
const [ error, setError ] = useState(null)
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopTracks()) // <- calls getMyTopTracks, but retry if the token has expired
.then((res) => {
if (disposed) return
setTracks(res.body.items)
setError(null)
})
.catch((err) => {
if (disposed) return
setTracks([])
setError(err)
});
return () => disposed = true
});
if (error != null) {
return <span class="error">{error.message}</span>
}
if (tracks.length === 0) {
return <span class="warning">No tracks found.</span>
}
return (<ul>
{tracks.map((track) => {
const artists = track.artists
.map(artist => artist.name)
.join(', ')
return (
<li key={track.id}>
<a href={track.preview_url}>
{track.name} - {artists}
</a>
</li>
)
}
</ul>)
}
// Login.js
import useSpotify from '...'
const Login = () => {
const { exchangeCode } = useSpotify()
const [ error, setError ] = useState(null)
const code = new URLSearchParams(window.location.search).get("code")
useEffect(() => {
if (!code) return // no code. do nothing.
// if here, code available for login
let disposed = false
exchangeCode(code)
.then(() => {
if (disposed) return
setError(null)
window.history.pushState({}, null, "/")
})
.catch(error => {
if (disposed) return
console.error(error)
setError(error)
})
return () => disposed = true
}, [code])
if (error !== null) {
return <span class="error">{error.message}</span>
}
if (code) {
// TODO: Render progress bar/spinner/throbber for "Signing in..."
return /* ... */
}
// if here, no code & no error. Show login button
// TODO: Render login button
return /* ... */
}
// MyRouter.js (rename it however you like)
import useSpotify from '...'
import Login from '...'
const MyRouter = () => {
const { hasToken } = useSpotify()
if (!hasToken) {
// No access token available, show login screen
return <Login />
}
// Access token available, show main content
return (
<Router>
// ...
</Router>
)
}
// App.js
import SpotifyAuthContextProvider from '...'
import MyRouter from '...'
const App = () => {
return (
<SpotifyAuthContextProvider>
<MyRouter />
</SpotifyAuthContextProvider>
);
}

How to get data from server and use a hook to retrieve it in multiple components?

I have a rather basic use-case: I want to get the user info from the server when the app loads and then using a hook to get the info in different components.
For some reason, I run into an infinite loop and get Error: Maximum update depth exceeded.
getMe gets called recursively until the app crashes.
Is that a correct hook behavior?
This is the relevant part of the hook:
export default function useUser () {
const [user, setUser] = useState(null)
const [authenticating, setAuthenticating] = useState(true)
// ...
const getMe = (jwt) => {
console.log('set user')
axios.get(baseURL + endpoints.currentUser, { headers: {
'X-Access-Token': jwt,
'Content-Type': 'application/json'
}}).then(response => {
setUser({
name: response.data.name,
img: response.data.avatar_url
})
})
}
useEffect(() => {
getMe(jwt)
}, [])
return { user, authenticating }
}
This is the first call
function App () {
const { user, authenticating } = useUser()
const c = useStyles()
return (
authenticating ? (
<div className={c.wrapper}>
<Loader size={60}/>
</div>
) : (
<div className={c.wrapper}>
<div className={c.sidebar}>
<img className={c.lamp} src={ user ? user.img : lamp } />
And I also call need the user in the Router component
const Routes = () => {
const { user } = useUser()
return (
<Router history={history}>
<Switch>
// ...
<Route
path={pages.login}
render={routeProps => (
user ?
<Redirect to={pages.main}/> :
<Login {...routeProps}/>
)}
/>
You shouldn't be requesting the server each time you call the hook since it pretty much unnecessary. You could use Redux or context for this (for this paradigm redux would be better). However, if you insist on this method, it seems you have to wrap your getMe function in a useCallback hook since it must be re-rendering each time the function runs.
Read more on the useCallback hook:
https://reactjs.org/docs/hooks-reference.html#usecallback
You're now making a request via the useEffect in your custom hook - why not let the component do that programatically?
Change getMe to a useCallback function and export it:
export default function useUser () {
const [user, setUser] = useState(null)
const [authenticating, setAuthenticating] = useState(true)
// ...
const getMe = useCallback((jwt) => {
console.log('set user')
axios.get(baseURL + endpoints.currentUser, { headers: {
'X-Access-Token': jwt,
'Content-Type': 'application/json'
}}).then(response => {
setUser({
name: response.data.name,
img: response.data.avatar_url
})
})
}, [])
return { user, authenticating, doFetch: getMe }
}
..and use that function in your components (import doFetch and call it on mount), e. g.:
function App () {
const { user, authenticating, doFetch } = useUser()
const c = useStyles()
useEffect(() => doFetch(), [doFetch])
return (
authenticating ? (
<div className={c.wrapper}>
<Loader size={60}/>
</div>
) : (
<div className={c.wrapper}>
<div className={c.sidebar}>
<img className={c.lamp} src={ user ? user.img : lamp } />
You now avoid the infinite loop and your component takes control of the request logic instead of the reusable hook.

Categories