Infinite loop React Native Screen due to fetch and useState - react-native

I'm having an infinite loop due to the change of state on react native, the problem is that I tried to fix it with Use Effect Hook and was impossible.
That is my code
import React from 'react';
import { Alert } from 'react-native';
import AsyncStorage from '#react-native-async-storage/async-storage'
import Constants from 'expo-constants';
import Leaderboard from 'react-native-leaderboard';
import { useEffect } from 'react/cjs/react.production.min';
const LeaderBoardScreen = ({ navigation }) => {
const baseUrl = Constants.manifest.extra.backend_url;
const [token, setToken] = React.useState(null);
const [users, setUsers] = React.useState(null);
_retrieveToken = async () => {
try {
const token = await AsyncStorage.getItem('userToken');
setToken(token);
} catch (error) {
Alert.alert("SESIÓN FINALIZADA");
}
};
_retrieveToken();
const requestOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
};
fetch(`${baseUrl}/user/top`, requestOptions)
.then((response) => response.json())
.then(data => {
if (data == undefined) {
Alert.alert(
"Usuario inválido",
"el usuario no existe, o la contraseña o usuario son incorrectas ¿Estás seguro de que tienes cuenta?"
);
}
console.log(data);
setUsers(data);
});
return (
<Leaderboard
data={users}
sortBy='highScore'
labelBy='userName'
icon='icon' />
);
}
export default LeaderBoardScreen;
It starts sending GET to my back end in a infinite loop

Try importing useEffect from React perhaps instead of where you have imported it from.

That's because fetch and get token are being called on every render, you should move the function call inside a useEffect, (Or React Query useQuery if you want a 3rd party library)
The following code gets the token after the first render and then runs the fetches whenever the token changes
const requestOptions = (token) => ({
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
});
const LeaderBoardScreen = ({ navigation }) => {
const baseUrl = Constants.manifest.extra.backend_url;
const [token, setToken] = React.useState(null);
const [users, setUsers] = React.useState(null);
_retrieveToken = useCallback(async () => {
try {
const token = await AsyncStorage.getItem('userToken');
setToken(token)
} catch (error) {
Alert.alert("SESIÓN FINALIZADA");
}
},[])
useEffect(()=>{
_retrieveToken()
},[_retrieveToken])
useEffect(()=>{
if(!!token){
fetch(`${baseUrl}/user/top`, requestOptions(token))
.then((response) => response.json())
.then(data => {
if (data == undefined) {
Alert.alert(
"Usuario inválido",
"el usuario no existe, o la contraseña o usuario son incorrectas ¿Estás seguro de que tienes cuenta?"
);
}
console.log(data);
setUsers(data);
})
}
},[token])
return (
<Leaderboard
data={users}
sortBy='highScore'
labelBy='userName'
icon='icon' />
);
}
export default LeaderBoardScreen;

Related

React Native fetch URL - passing hook value as parameters

What is the proper way to pass a useState hook value as a query parameter in a REACT NATIVE fetch url? The function returns that my jwt is malformed it's not reading the value of the hook properly. The two hooks are below, I'm trying to use those as query parameters in the fetch URL AND header authorization. $Are typically JQuery, but not sure the proper syntax for React Native - Expo.
const [user, setUser] = useState();
const [userID, setUserID] = useState();
const [subscriptions, setSubscriptions] = useState();
useEffect(() => {
const fetchSubUploads = async (userID, user) => {
const response = await fetch(
`content/subs/uploads/**${userID}**`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer **${user}**`
},
});
let data = await response.json();
console.log(data);
setSubscriptions(data.subscriptionUploads);
return data;
};
const getUser = async() =>{
const loggedInUser = await AsyncStorage.getItem('fariToken');
if(!loggedInUser){
Alert.alert('Please Login')
}if(loggedInUser){
setUser(JSON.stringify(loggedInUser))
}
}
fetchSubUploads();
}, []);
I suggest spitting the useEffect in two. One effect is obviously dealing with making the fetch request with the appropriate data, user and userID, and so should have a dependency on these values, while the other effect deals with loading some "initial" state values from storage.
Example:
const [user, setUser] = useState();
const [userID, setUserID] = useState();
const [subscriptions, setSubscriptions] = useState();
useEffect(() => {
const getUser = async () => {
const loggedInUser = await AsyncStorage.getItem('fariToken');
if (loggedInUser) {
setUser(JSON.stringify(loggedInUser));
} else {
Alert.alert('Please Login');
}
}
getUser();
}, []);
useEffect(() => {
const fetchSubUploads = async (userID, user) => {
const response = await fetch(
`content/subs/uploads/**${userID}**`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer **${user}**`
},
}
);
const data = await response.json();
console.log(data);
setSubscriptions(data.subscriptionUploads);
return data;
};
if (user && userID) {
fetchSubUploads(userID, user);
}
}, [user, userID]);

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop React Native with FlatList render

I don't understand why I get this error, when rendering screen I use useSelector to get state from store(I'm using redux) I get data from api and pass it to flatList to render the list, everything is normal but I don't know why
const HistoryScreen = () => {
const { loading, histories } = useSelector((state) => state.historiesList)
useEffect(() => {
if (user) {
dispatch(listHistory())
}
}, [dispatch, user])
return (
<FlatList data={histories} renderItem={({ item, i }) => <HistoryCard key={i} onPress={() => console.warn('cliecked')} post={item} ></HistoryCard>}>
</FlatList >
</View >}</>
)
}
export default HistoryScreen
action:
export const listHistory = (skip = 0, limit = 10) => async (dispatch, getState) => {
try {
dispatch({ type: HISTORY_LIST_REQUEST })
const user = await AsyncStorage.getItem('userInfo')
const userInfo = user ? JSON.parse(user) : null
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userInfo.token}`
}
}
const payload = { skip: skip, limit: limit }
const { data } = await axios.post(`${api}/histories/`, payload, config)
dispatch({ type: HISTORY_LIST_SUCCESS, payload: data.data })
console.log(data.data)
} catch (error) {
dispatch({ type: HISTORY_LIST_FAILED, payload: error })
}
}

React Native - Fetch data from API with Authorization Bearer Token

I am trying to fetch data of user profile and I got this error
"Unexpected token U in JSON at position 0" in my Android Simulator
I checked with in my console.log and I manage to get my API properly so I think that is not the issue
This is my Home index.js (the file trying to fetch data)
import AsyncStorage from '#react-native-async-storage/async-storage';
import React, {useEffect, useState} from 'react';
import {StyleSheet, Text, View} from 'react-native';
import {getData} from '../../utils';
export default function Home() {
const [data, setData] = useState([]);
const [token, setToken] = useState('');
useEffect(() => {
getData('token').then(res => {
const res_token = res;
console.log('token response: ', res_token);
setToken(res_token);
});
}, []);
useEffect(() => {
fetch('https://emaillead.aturtoko.id/api/v1/profile', {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token,
},
})
.then(response => response.json())
.then(json => {
console.log('token auth: ' + token);
setData(json);
console.log(json);
})
.catch(error => console.error(error));
}, [token]);
return (
<View>
<Text>Nama: {data.name}</Text>
</View>
);
}
const styles = StyleSheet.create({});
So I got the token from my login page. I tried to make 2 useEffect, one to get the token and the other to get the data from API. I also tried the only 1 useEffect before as well with the same result.
This is the code when I use only 1 useEffect
useEffect(() => {
getData('token').then(res => {
const res_token = res;
console.log('token response: ', res_token);
setToken(res_token);
});
fetch('https://emaillead.aturtoko.id/api/v1/profile', {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token,
},
})
.then(response => response.json())
.then(json => {
console.log('token auth: ' + token);
setData(json);
console.log(json);
})
.catch(error => console.error(error));
}, [token]);
I don't know where is the problem with my code right now

Apollo client onError not retrying request

So according to the apollo docs for apollo-link-error, onError can be used to handle re-authentication if used with forward(operation).
So I wrote the following code
import { ApolloClient } from 'apollo-client'
import { createHttpLink, HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { InMemoryCache } from 'apollo-cache-inmemory'
import AsyncStorage from '#react-native-community/async-storage'
import { refresh } from 'react-native-app-auth'
import { onError } from 'apollo-link-error'
import { ApolloLink, from } from 'apollo-link'
import { RetryLink } from "apollo-link-retry"
import { KC_CONFIG } from '../../config/env'
const httpLink = new HttpLink({
uri: 'graphqlEndpointOfYourchoice'
})
const authLink = setContext(async (_, { headers }) => {
const accessToken = await AsyncStorage.getItem('accessToken')
const unwrappedAccessToken = JSON.parse(accessToken)
return {
headers: {
...headers,
authorization: unwrappedAccessToken ? `Bearer ${unwrappedAccessToken}` : "",
}
}
})
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
AsyncStorage.getItem('refreshToken')
.then(data => {
const refreshToken = JSON.parse(data)
// console.log(data)
refresh(KC_CONFIG, {
refreshToken,
})
.then(({ accessToken, refreshToken }) => {
const oldHeaders = operation.getContext().headers
operation.setContext({
...oldHeaders,
authorization: accessToken
})
console.log(oldHeaders.authorization)
console.log(accessToken)
// console.log(refreshToken)
AsyncStorage
.multiSet([
['accessToken', JSON.stringify(accessToken)],
['refreshToken', JSON.stringify(refreshToken)]
])
// tried putting forward() here <--------------
})
.catch(e => {
if (e.message === 'Token is not active') console.log('logging out')
else console.log('Refresh error: ' + e)
})
})
.then(() => {
console.log('Refreshed the accesstoken')
return forward(operation)
})
.catch(e => {
console.log('Storage error: ' + e)
})
}
if (networkError) {
console.log('network error: ' + networkError)
}
// tried putting forward() here <--------------
})
const retryLink = new RetryLink()
export const client = new ApolloClient({
link: from([
retryLink,
errorLink,
authLink,
httpLink
]),
cache: new InMemoryCache()
})
This does not achieve the desired result.
The error gets caught and runs it's course, refreshing the token as it should, but it never does a second request.
try this:
export const logoutLink = onError(({ networkError, operation, forward }) => {
if (networkError?.statusCode === 401) {
return new Observable(observer => {
(async () => {
try {
const newToken = await getToken();
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${newToken}`,
},
});
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
};
// Retry last failed request
forward(operation).subscribe(subscriber);
} catch (error) {
observer.error(error);
}
})();
});
}
});

Handling Refresh Token in React Native

I have an app authenticating fine and returning the access_token and refresh_token. I store them with AsyncStorage and save/get the access_token with redux. This is the very first app I am building and I am struggling with how and where to use the refresh_token.
This is the axios call in the component loginForm.js
axios({
url: `${base}/oauth/token`,
method: 'POST',
data: formData,
headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
}
})
.then(response => {
setStatus({ succeeded: true });
// console.log(response.data);
deviceStorage.saveKey("userToken", response.data.access_token);
deviceStorage.saveKey("refreshToken", response.data.refresh_token);
Actions.main();
})
.catch(error => {
if (error.response) {
console.log(error);
}
});
This is the service deviceStorage.js
import { AsyncStorage } from 'react-native';
const deviceStorage = {
async saveItem(key, value) {
try {
await AsyncStorage.setItem(key, value);
} catch (error) {
console.log('AsyncStorage Error: ' + error.message);
}
}
};
export default deviceStorage;
This is the token action file
import { AsyncStorage } from 'react-native';
import {
GET_TOKEN,
SAVE_TOKEN,
REMOVE_TOKEN,
LOADING_TOKEN,
ERROR_TOKEN
} from '../types';
export const getToken = token => ({
type: GET_TOKEN,
token,
});
export const saveToken = token => ({
type: SAVE_TOKEN,
token
});
export const removeToken = () => ({
type: REMOVE_TOKEN,
});
export const loading = bool => ({
type: LOADING_TOKEN,
isLoading: bool,
});
export const error = tokenError => ({
type: ERROR_TOKEN,
tokenError,
});
export const getUserToken = () => dispatch =>
AsyncStorage.getItem('userToken')
.then((data) => {
dispatch(loading(false));
dispatch(getToken(data));
})
.catch((err) => {
dispatch(loading(false));
dispatch(error(err.message || 'ERROR'));
});
export const saveUserToken = (data) => dispatch =>
AsyncStorage.setItem('userToken', data)
.then(() => {
dispatch(loading(false));
dispatch(saveToken('token saved'));
})
.catch((err) => {
dispatch(loading(false));
dispatch(error(err.message || 'ERROR'));
});
export const removeUserToken = () => dispatch =>
AsyncStorage.removeItem('userToken')
.then((data) => {
dispatch(loading(false));
dispatch(removeToken(data));
})
.catch((err) => {
dispatch(loading(false));
dispatch(error(err.message || 'ERROR'));
});
This is the token reducer file
import {
GET_TOKEN,
SAVE_TOKEN,
REMOVE_TOKEN,
LOADING_TOKEN,
ERROR_TOKEN
} from '../actions/types';
const INITIAL_STATE = {
token: {},
loading: true,
error: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case GET_TOKEN:
return {
...state,
token: action.token
};
case SAVE_TOKEN:
return {
...state,
token: action.token
};
case REMOVE_TOKEN:
return {
...state,
token: action.token
};
case LOADING_TOKEN:
return {
...state,
loading: action.isLoading
};
case ERROR_TOKEN:
return {
...state,
error: action.error
};
default:
return state;
}
};
And this is the authentication file
import React from 'react';
import {
StatusBar,
StyleSheet,
View,
} from 'react-native';
import { connect } from 'react-redux';
import { Actions } from 'react-native-router-flux';
import { Spinner } from '../common';
import { getUserToken } from '../../actions';
class AuthLoadingScreen extends React.Component {
componentDidMount() {
this.bootstrapAsync();
}
bootstrapAsync = () => {
this.props.getUserToken().then(() => {
if (this.props.token.token !== null) {
Actions.main();
} else {
Actions.auth();
}
})
.catch(error => {
this.setState({ error });
});
};
render() {
return (
<View style={styles.container}>
<Spinner />
<StatusBar barStyle="default" />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
});
const mapStateToProps = state => ({
token: state.token,
});
const mapDispatchToProps = dispatch => ({
getUserToken: () => dispatch(getUserToken()),
});
export default connect(mapStateToProps, mapDispatchToProps)(AuthLoadingScreen);
I believe I need to create an action and reducer to get the refresh_token (is that correct?) but I do not know what to do with it and where to call it (perhaps in the authentication file?).
Any help with this possibly with code examples related to my code would be massively appreciated. Thanks
Below are the steps
Do Login , get accessToken , refreshToken from response and save it to AsyncStorage.
Make common function for API calling
async function makeRequest(method, url, params, type) {
const token = await AsyncStorage.getItem('access_token');
let options = {
method: method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
},
};
if (!token) {
delete options['Authorization'];
}
if (['GET', 'OPTIONS'].includes(method)) {
url += (url.indexOf('?') === -1 ? '?' : '&') + queryParams(params);
} else {
Object.assign(options, {body: JSON.stringify(params)});
}
const response = fetch(ENV.API_URL+url, options);
return response;
}
Make one method in redux for getAceessTokenFromRefreshToken.
Use this method when session is expired
How do you know session is expired?
From each API calling if you get response like (440 response code) in
async componentWillReceiveProps(nextProps) {
if (nextProps.followResponse && nextProps.followResponse != this.props.followResponse) {
if (nextProps.followResponse.status) {
if (nextProps.followResponse.status == 440) {
// call here get acceesstokenfrom refresh token method and save again accesstoken in asyncstorage and continue calling to API
}
}
}
}