How to create component in render part from fetched API? - react-native

I'm trying to fetch data from api and I used componentDidMount lifecycle for that, But I have a list in my view which need to be created from that API, so I use map function for received data to get all items and show in render part, But when I run my code I get
this.state.matchInfo.map in not a function
Please help me to solve this problem, I knew that componentDidMount will run after first render so I create an empty state first and hoped that after fetching data, component will render again and will show my data. but it keeps getting me the error
here is my code:
constructor(props) {
super(props);
this.state = {
userName: '',
userToken: '',
userID: '',
firstTime: true,
loading: true,
showAlert : false,
alertType : true,
alertMessage : '',
animatedMatchBtn : new Animated.Value(1),
matchInfo: []
};
}
componentDidMount() {
this._bootstrapAsync(true);
}
_bootstrapAsync = async (timeOutStat = null) => {
const userToken = await AsyncStorage.getItem('userToken');
const userName = await AsyncStorage.getItem('userName');
const userID = await AsyncStorage.getItem('userID');
await this.setState({
userName: userName,
userToken: userToken,
userID: userID,
})
if(timeOutStat) {
this.timeOut = setTimeout(() => {
this.setState({
loading: false,
showAlert: true,
alertType: true,
alertMessage: ErrorList.matchMakingError
});
}, 20000)
}
console.log('token', userToken)
await fetch(getInitUrl('getMatchInfo','',this.props.navigation.getParam('matchID', 0)), {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization : `Bearer ${userToken}`
}
}).then(response => Promise.all([response.ok, response.status ,response.json()]))
.then(([responseOk,responseStatus, body]) => { //
this.setState({loading : false});
clearTimeout(this.timeOut);
if (responseOk) {
console.log('match Info', body);
this.setState({
matchInfo : body
})
} else {
console.log(responseStatus, body);
this.setState({
showAlert : true,
alertType : true,
alertMessage : ErrorList[body.tag]
});
}
})
.catch(error => {
console.error(error);
});
};
render() {
//console.log(puzzleSizes)
let rows = this.state.matchInfo.map((item , index)=>{
return
<MatchDetailBox
/>
})
console.log(rows)
<View>
{rows}
</View>
}

Even though this.setState() is asynchronous, it's not promisified hence it wont't work using promise's .then() or its syntactic sugar async/await. So there's no use for await in front of it. But I guess it creates a one tick delay.
Also why do you have await in front of fetch() and also .then() after that. Shouldn't either of them do?
The error this.state.matchInfo.map is not a function would occur only when this.state.matchInfo is not an array but you have initialized it to be one, so at any point of time matchInfo gets modified it must be becoming non-array like an object or something which doesn't have a native .map().
Have you checked response coming from API? I hope this helps.
this.setState({ matchInfo: body });

mapData = () => {
this.state.matchInfo.map((item , index)=>{
return(
<View>
<MatchDetailBox />
<View>
{item}
</View>
</View>
)
})
}
render() {
return {this.mapData()}
}

Related

React-admin: useQuery not fetching data on Dashboard after logging in

I have react-admin set up with GraphQL and works fine for the most part... however there seems to be an issue with logging in initially and having the Dashboard make a query; the issue being that it will not resolve upon initial loading. It goes from logging in -> Dashboard and I'm met with a component that loads forever with no data or errors.
My Admin is set up like this:
const routes = [
<Route exact path="/support-chat-list" component={SupportChatPage} noLayout />,
<Route exact path="/group-chat" component={GroupChatPage} noLayout />,
<Route exact path="/" component={Dashboard} />,
];
const App = () => (
<Admin dashboard={Dashboard} layout={MyLayout} customRoutes={routes} dataProvider={dataProvider} authProvider={authProvider} i18nProvider={i18nProvider}>
{/* my resources... */}
</Admin>
);
and my Dashboard looks like this:
const requestOptions = (startingAfter, startingBefore) => {
return {
type : 'getList',
resource: 'appointments',
payload : {
pagination: {
perPage: 100,
page : 0,
},
sort : {
field: 'start_at',
order: 'asc'
},
filter : {
startingAfter: startingAfter.toString(),
startingBefore: startingBefore.toString(),
}
}
};
};
const handleSelectEvent = (event, e, redirectTo) => {
redirectTo('show', ROUTES.APPOINTMENTS, event.resource.id);
};
const initialState = {
middleDate: DateTime.local().startOf('day'),
startDate: DateTime.local().minus({days: 3}).startOf('day'),
endDate: DateTime.local().plus({days: 3}).endOf('day'),
};
export function Dashboard() {
useAuthenticated();
const redirectTo = useRedirect();
const [dateRanges, setDateRanges] = useState(initialState);
const {data, loading, error} = useQueryWithStore(requestOptions(dateRanges.startDate, dateRanges.endDate));
if (error) return <Error />;
return (
<Card>
<Title title="Admin" />
<CardContent>
{loading && <Loading/>}
{!loading &&
<Calendar
events={translateAppointmentData(data)}
//.......
I've tried playing around with useQuery and useQueryWithStore but nothing seems to work. It will only resolve when I refresh the page, and only then it will load everything.
I have a suspicion that it has to do with my authProvider, which is a class that handles authentication. It's a bit bloated so I'll try my best to show the things related to login:
async login({username, password}) {
const response = await authClient.passwordGrant(username, password, ACCESS_TOKEN_SCOPES);
await this.processTokenResponse(response);
return Promise.resolve();
}
authClient.passwordGrant looks like this:
passwordGrant(username, password, scopes) {
const request = new Request(`${this.url}/oauth/access_token`, {
method: 'POST',
body: JSON.stringify({
username: username,
password: password,
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'password',
scope: scopes.join(' '),
}),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
});
}
while processTokenResponse looks like this:
processTokenResponse(response) {
const now = DateTime.local();
localStorage.setItem(TOKEN_STORAGE_KEY, response.access_token);
localStorage.setItem(TOKEN_EXPIRY_STORAGE_KEY, now.plus({seconds: response.expires_in - EXPIRY_BUFFER}).toString());
localStorage.setItem(SCOPES_STORAGE_KEY, JSON.stringify({scopes: response.scopes}));
localStorage.setItem(REFRESH_TOKEN_STORAGE_KEY, response.refresh_token);
localStorage.setItem(REFRESH_TOKEN_EXPIRY_STORAGE_KEY, now.plus({seconds: response.refresh_token_expires_in - EXPIRY_BUFFER}).toString());
return this.fetchActiveUserData();
}
and fetchActiveUserData looks like this:
async fetchActiveUserData() {
let dataProvider = new ActiveUserDataProvider();
return await dataProvider.getOne({}).then((response) => {
return this.processLoadUserResponse(response.data);
})
}
in which my dataProvider looks like this:
import apolloClient from '../apolloClient';
import { BaseResourceDataProvider } from './baseResourceDataProvider';
import {FetchMeQuery} from "../graphql/users";
const FetchMeQuery = gql`
query FetchMe {
me {
id
firstName
lastName
name
username
timezone
}
}
`;
class ActiveUserDataProvider extends BaseResourceDataProvider {
getOne({ id }: { id?: string }) { // maybe something wrong here?
return apolloClient.query({
query: FetchMeQuery,
fetchPolicy: 'cache-first'
}).then((result) => {
return {
data: result.data.me,
};
});
}
}
I apologize for the long post but I'm completely lost on why my Dashboard isn't querying data once initially logged in, and needing a refresh for it to work. I'm hoping someone can point where something's wrong. Thank you.
After much digging, it turned out to be how I set up my ApolloClient. Previously I was using Apollo Link to set up my auth middleware, and discovered that it wasn't calling the middleware at all.
I discovered this question and replaced ApolloLink with setContext and it worked perfectly. This is the code now:
const authMiddleware = setContext(async (_, { headers }) => {
const token = await authProvider.checkAuth();
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
}
}
});
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: from([
authMiddleware,
httpLink
]),
});

Unable to set state from the response of the api

I have a following function in Api.js
const _getCategories = async () => {
var categories = [];
let token = await getJwt();
var config = {
method: 'get',
url: 'myurl',
headers: {
'Authorization': 'Bearer' + ' ' + token
},
data : ''
};
axios(config)
.then(function (response) {
if(response.status == 200)
{
let res = response.data;
// Create a variable having name in the list.
categories = res.data.apps.map(function(item) {
return {
title: item.name,
color: AppStyles.colorSet.analyticsColor,
lightColor: AppStyles.colorSet.analyticsLightColor,
icon: AppStyles.iconSet.analytics
};
});
// console.log('Returning Categories');
console.log(categories);
return categories;
//console.log(data1)
// Handle and fetch the list of apps
}
else
{
// API didn't go through, generate the error functions
}
})
.catch(function (error) {
console.log(error);
});
};
and I am loading it in homscreen.js
class DashboardScreen extends React.Component {
constructor(props) {
super(props);
const { navigation } = props;
navigation.setOptions({
title: 'Dashboard',
headerLeft: () => (
<TouchableOpacity
onPress={() => {
navigation.openDrawer();
}}
>
<Icon
style={AppStyles.styleSet.menuButton}
name="ios-menu"
size={AppStyles.iconSizeSet.normal}
color={AppStyles.colorSet.mainThemeForegroundColor}
/>
</TouchableOpacity>
),
});
this.state = {
categories: [],
};
}
componentDidMount() {
if (!this.state.data) {
Api.getCategories().then(data => console.log("The data is "+data))
.catch(err => { /*...handle the error...*/});
}
}
onPressCategory = item => {
// Handle onpress for the items
};
render() {
//console.log(this.state.categories);
categoryButtonsRow1 = this.state.categories.map((item, index) => {
if (index < 3) {
return (
<CategoryButton
onPress={() => this.onPressCategory(item)}
color={item.color}
lightColor={item.lightColor}
icon={item.icon}
title={item.title}
/>
);
}
});
return (
<ScrollView style={styles.container}>
<View style={styles.row}>{categoryButtonsRow1}</View>
</ScrollView>
);
}
}
But I am getting category as undefined while printing in render().
I even tried to create an async function in the homescreen.js and call the api with await and set the state after the same but still it is coming as undefined.
Any guesses to what I am doing wrong here. Can anyone help with the same. My best guess is that I am not handling the api request properly.
EDIT
I tried Use Async/Await with Axios in React.js but it is still printing undefined to the same.
The reason for getting undefined is the _getCategories is that its not returning anything and you are chaining using .then to get data so the caller has no way to get this data as a callback is not passed.
You can change the to await like this
const _getCategories = async () => {
var categories = [];
let token = await getJwt();
var config = {
method: 'get',
url: 'myurl',
headers: {
Authorization: 'Bearer' + ' ' + token,
},
data: '',
};
const response = await axios(config);
if (response.status == 200) {
let res = response.data;
// Create a variable having name in the list.
categories = res.data.apps.map(function (item) {
return {
title: item.name,
color: AppStyles.colorSet.analyticsColor,
lightColor: AppStyles.colorSet.analyticsLightColor,
icon: AppStyles.iconSet.analytics,
};
});
// console.log('Returning Categories');
console.log(categories);
return categories;
//console.log(data1)
// Handle and fetch the list of apps
} else {
// API didn't go through, generate the error functions
return null;
}
};
And you can set the state in componentDidMount (should be async)
this.setState({categories:await api._getCategories()});

React Native Infinite Scroll - Fetch More

I am trying to do an infinite scroll with flatlist. However, every time I make a function to fetch more data, I have a loop with the same data (3 times). Here is my code without fetch more function. How can I fetch more data by changing the page number and making it an infinite scroll until there is no more data left.
P.S. I have setted up Page state and making it +1, but because state updates >3times, I get unlimited loop again with pages that don't even exist.
export default class List extends React.PureComponent {
constructor(props) {
super(props);
this.fetchData = this._fetchData.bind(this);
this.state = {
isLoading: true,
isLoadingMore: false,
_data: null,
accessToken: ""
};
}
async componentWillMount() {
try {
let accessToken = await AsyncStorage.getItem(ACCESS_TOKEN).then(
JSON.parse
);
if (!accessToken) {
this.redirect("login");
} else {
this.setState({ accessToken: accessToken });
}
} catch (error) {
console.log("Something went wrong");
this.redirect("login");
}
this.fetchData(responseJson => {
const data = responseJson;
this.setState({
isLoading: false,
_data: data
});
});
}
_fetchData(callback) {
fetch(`https://website.com/posts?page=${page}&per_page=10`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer " + this.state.accessToken.token
}
})
.then(response => response.json())
.then(callback)
.catch(error => {
console.error(error);
});
}

Storing a value in Redux

I am building a React Native app, mainly for verifying tickets, to be used by event administrators. The back-end is served by a Laravel app with a working OAuth2-server. I have a working login against that server but now I need to store the access token, to request data, such as events, and to verify if a ticket is matched for a given event.
I'm trying to implement Redux to store the access token etc. The login form I have updates the store via actions correctly, but I can't get it to work with the access token.
Here is the login screen:
import React, { Component } from 'react';
import { Text, View, TextInput, Button } from 'react-native';
import { connect } from 'react-redux'
import StringifyBody from './../lib/oauth2/StringifyBody'
import { login, storeTokens } from '../redux/actions/auth.js'
class Login extends Component {
constructor (props) {
super(props);
this.state = {
route: 'Login',
loading: false,
email: '',
password: '',
accessToken: '',
};
}
handleClick (e) {
e.preventDefault();
return new Promise(function(resolve, reject) {
var data = StringifyBody(this.state.password, this.state.email)
// XHR settings
var xhr = new XMLHttpRequest()
xhr.withCredentials = true
xhr.onerror = function() {
reject(Error('There was a network error.'))
}
xhr.open("POST", "http://192.168.0.141/oauth/access_token")
xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded")
xhr.send(data)
xhr.onloadend = function() {
if (xhr.status === 200) {
var parsedJson = JSON.parse(xhr.response)
responseArray = []
for(var i in parsedJson) {
responseArray.push([parsedJson [i]])
}
// assign values to appropriate variables
let accessToken = responseArray[0];
console.log('access token is: ' + accessToken)
accessToken => this.setState({ access_token: accessToken })
this.props.tokenStore(this.state.accessToken) // This doesn't work: "cannot read property 'tokenStore' of undefined"
resolve(xhr.response)
} else {
reject(Error('Whoops! something went wrong. Error: ' + xhr.statusText))
}
}
})
.done(() => {
this.props.onLogin(this.state.email, this.state.password); // This works
})
}
render() {
return (
<View style={{padding: 20}}>
<Text style={{fontSize: 27}}>{this.state.route}</Text>
<TextInput
placeholder='Email'
autoCapitalize='none'
autoCorrect={false}
keyboardType='email-address'
value={this.state.email}
onChangeText={(value) => this.setState({ email: value })} />
<TextInput
placeholder='Password'
autoCapitalize='none'
autoCorrect={false}
secureTextEntry={true}
value={this.state.password}
onChangeText={(value) => this.setState({ password: value })} />
<View style={{margin: 7}}/>
<Button onPress={(e) => this.handleClick(e)} title={this.state.route}/>
</View>
);
}
}
const mapStateToProps = state => {
return {
isLoggedIn: state.auth.isLoggedIn,
access_token: state.auth.access_token,
}
}
const mapDispatchToProps = (dispatch) => {
return {
onLogin: (email, password) => { dispatch(login(email, password)); },
tokenStore: (accessToken) => { dispatch(storeTokens(accessToken)) },
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);
Redux actions:
export const login = (email, password) => {
return {
type: 'LOGIN',
email: email,
password: password
};
};
export const logout = () => {
return {
type: 'LOGOUT'
};
};
export const storeTokens = () => {
return {
type: 'STORE_TOKENS',
access_token: accessToken,
}
}
And finally the reducers:
const defaultState = {
isLoggedIn: false,
email: '',
password: '',
access_token: '',
};
export default function reducer(state = defaultState, action) {
switch (action.type) {
case 'LOGIN':
return Object.assign({}, state, {
isLoggedIn: true,
email: action.email,
password: action.password
});
case 'LOGOUT':
return Object.assign({}, state, {
isLoggedIn: false,
email: '',
password: ''
});
case 'STORE_TOKENS':
return Object.assign({}, state, {
access_token: action.accessToken,
})
default:
return state;
}
}
I've also tried passing the data to this.props.storeTokens (the actual action) in a componentDidMount() which gives me the error undefined is not a function (evaluating 'this.props.storeTokens()') componentDidMount Login.js:57:8
My question then is: How do I store the variable I get from my XHR POST in the redux store? Why is this.props.tokenStore and this.props.storeToken not defined?
Hey thats a mistake owing to javascript concept. You are calling
this.props.tokenStore(this..state.accessToken) // This doesn't work: "cannot read property 'tokenStore' of undefined"
inside a function defined using ES5 syntax. either you store the reference of this outside the function in some variable and then use that variable instead of this. The other option is define arrow function instead. So change your function keyword into
() =>
and this should work. this as of now in your implementation doesn't point to component that you are thinking

React Native setState(…) warning with both componentWillMount and componentDidMount

I'm starting with react-native and in my project I got to a point where everything works but there's this warning:
Warning: setState(...): Can only update a mounted or mounting component.
So, I've looked several QA, tried a few solutions(changing the setState() call from componentWillMount and componentDidMount) but... the warning is always there.
Here is part of the code:
REQUEST_URL = 'http://url/users.php';
(...)
module.exports = React.createClass({
getInitialState: function() {
return {
uid: null,
bid: null,
username: null,
}
},
componentDidMount: function() {
this.fetchData();
},
fetchData: function() {
fetch(REQUEST_URL)
.then( (response) => response.json() )
.then( (json) => {
console.log('setState called');
this.setState({
uid: json.users[0].user_id,
bid: json.users[0].building_id,
username: json.users[0].username
});
})
.done();
},
render: function() {
if (!this.state.uid) { //user is not defined
console.log('not rendered');
return <Text>chargement...</Text>
}
// else
console.log('rendered');
var userId = this.state.uid;
var buildingId = this.state.bid;
var username = this.state.username;
return (
<View style={styles.content}>
<Text style={styles.label}>User Id</Text>
<Text>{userId}</Text>
<Text style={styles.label}>Building Id</Text>
<Text>{buildingId}</Text>
<Text style={styles.label}>Username</Text>
<Text>{username}</Text>
</View>
)
},
});
The users.php returns a json content-type.
Any clues?
Thanx.
The problem may be that react re-mounts certain components multiple times in one render (think that has something to do with the representation of initial values, could not find the question here), therefore your state would be set to a component that is not mounted.
If you set your state in a decoupled timeout that can be cleared when the component unmounts, you avoid setting state on a unmounted component.
componentDidMount() {
this.mounted = true;
// this.fetchTimeout = setTimeout(()=>{
this.fetchData();
// });
},
componentWillUnmount() {
// clearTimeouts(this.fetchTimeout);
this.mounted = false;
},
fetchData() {
fetch(REQUEST_URL)
.then( (response) => response.json() )
.then( (json) => {
console.log('setState called');
if (this.mounted === true){
this.setState({
uid: json.users[0].user_id,
bid: json.users[0].building_id,
username: json.users[0].username
});
}
})
.done();
},
I still don't know if we are supposed to use TimerMixins but this way works without those.
(TimerMixins take care of clearing any timeout or interval set in the component)
EDIT: update sample to only call setState of the component is still mounted.
I do not know if there is a better way, but as far as I know until now you can not cancel a fetch request.