React setState callback won't update state - javascript

So I have 3 functions below. One containing calls to the two (getBooks), which are getting requests. I set my state (isLoading) to true before the calls and then to true after the calls. This is to also make sure that the data is properly loaded. However the state is not updating so, therefore, my data from the get request is invalid. The callbacks in my setstate work in my other components, so I am confused. Below are my 3 functions.
import React from 'react';
import ReactDOM from 'react-dom';
import SidePane from './SidePane.js';
import HomeNavBar from './HomeNavBar.js';
import axios from 'axios';
import qs from 'qs';
import Loading from './Loading.js';
class HomePage extends React.Component {
constructor(props) {
super(props);
this.state = {
bookSearch: "",
bookSearchResults: [],
bookSearchFound: false,
isLoading: false
};
this.handleSearch = this.handleSearch.bind(this);
this.alertBookName = this.alertBookName.bind(this);
this.getBooksFromIsbn = this.getBooksFromIsbn.bind(this);
this.getBooks = this.getBooks.bind(this);
axios.defaults.withCredentials = true;
}
changeBookName = (e) => {
var bookName = e.target.value;
bookName = bookName.split(' ').join('+');
this.setState({bookSearch: bookName})
}
getBooksFromIsbn(isbns){
var books = [];
axios
.get('http://localhost:9000/api/getBooksFromIsbn',
{
params: {
books: JSON.stringify(isbns)
}
})
.then(res =>
{
console.log(res.data);
books = res.data;
})
.catch(error => {
console.log(error.response);
});
}
getBooks(){
this.setState({
isLoading: true
},
function(){console.log("setState completed", this.state)}
);
var bookResults = this.handleSearch();
var books = this.getBooksFromIsbn(bookResults);
this.setState({
isLoading: false
},
function(){console.log("setState completed", this.state)}
);
this.props.setBookSearchResults(books);
}
handleSearch(){
var bookResults = [];
var url = 'http://localhost:9000/api/getOpenLibrarySearch';
axios
.get(url,
{
params: {
bookSearch: this.state.bookSearch
}
})
.then(res =>
{
//this.setState({bookSearchResults: res.data});
for(var i=0; i < res.data.docs[i].isbn.length; i++){
bookResults = bookResults.concat(res.data.docs[i].isbn);
}
console.log(bookResults);
})
.catch(error => {
console.log(error.response);
});
return bookResults;
}
render(){
if(this.state.isLoading == false){
return(
<div>
<HomeNavBar authToken = {this.props.authToken} email = {this.props.email} />
<SidePane changeBookName = {this.changeBookName} handleSearch = {this.getBooks} />
</div>
)
}
else
{
return <Loading />;
}
}
}

It looks like you need to actually return the books value from getBooksFromIsbn. Await the axios call resolution and return books.
async getBooksFromIsbn (isbns) {
const books = [];
try {
const res = await axios.get(
'http://localhost:9000/api/getBooksFromIsbn',
{
params: {
books: JSON.stringify(isbns)
}
});
books = res.data;
} catch(error) {
console.log(error.response);
}
return books;
}
Same for handleSearch, the code needs to wait and return the resolution of the GET request.

Related

Why history listen is not updating my component's state?

TLDR: I am building a React router app, I trying to update the state of my component through a history listener, this listener works fine I put a console.log and I can see it, but the state of my component is not changing, I can see this with the React chrome extension and my component is not updating.
`
import React from "react";
import { withRouter } from "react-router-dom";
import { styles } from './Styles';
import { url } from './App';
class Searchresults extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults : []
}
}
async fetchResults(endpoint) {
try {
const response = await fetch(endpoint);
if (response.ok) {
const rJson = await response.json();
return rJson;
}
} catch (err) {
console.log(err);
}
}
componentDidMount() {
this.searchUpdate();
this.unlisten = this.props.history.listen((location, action) => {
console.log("it works!");
this.searchUpdate();
})
}
searchUpdate = () => {
const { location } = this.props;
const params = new URLSearchParams(location);
const query = params.get("search");
const name = query.replace("?name", "s");
const endpoint = url + "&" + name;
this.fetchResults(endpoint).then(response => {
return response['Search'].map(item => {
return { title: item['Title'], poster: item['Poster'], id: item['imdbID'] }
})
}).then(response => {
this.setState({
searchResults : response
})
});
}
render() {
return (
<div style={styles.movieList}>
<ul>
{
!this.state.searchResults? 'Loading' : this.state.searchResults.map((item, index) => {
return (<li key={index}>
<a href={'/moviepage?id=' + item.id}>{item.title}</a><br />
<img src={item.poster} alt="Movie poster"
style={{ width: "6rem", height: "auto" }} />
</li>)
})
}
</ul>
</div>
);
}
}
export default withRouter(Searchresults);
`
I am trying to update the state with a method searchUpdate, then this method is called in componentDidMount, here works fine, then when the URL changes, the history.listen triggers and searchUpdate is fired again, and everything seems to work except the change of the state of my component.
The first .then function in your searchResult function doesn't return a promise, so there is no need to use another .then. Just put the setState call in the same block:
this.fetchResults(endpoint).then(response => {
const searchResults = response['Search'].map(item => {
return { title: item['Title'], poster: item['Poster'], id: item['imdbID'] }
});
this.setState({searchResults})
});

How to change the internal state property of a component in an action creator function

I am working on a React application and I am using Redux to store the state. I have the following code:
category-arrows.component.jsx:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increaseCategoryRank, decreaseCategoryRank, fetchCategoryRanks } from '../../redux/menu/menu.actions';
import './category-arrows.styles.scss';
class CategoryArrows extends Component {
state = {
isSending: false
}
render() {
const { category } = this.props;
const categoryClicked = true;
return (
<div className="arrows-container">
<div className="up-arrow" onClick={
() => {
if(this.state.isSending === false) {
this.props.increaseCategoryRank(category, categoryClicked)
}
this.props.fetchCategoryRanks(this.props.menu);
}}></div>
<div className="category-rank">
<p>{category.rank}</p>
</div>
<div className="down-arrow" onClick={
() => {
if(this.state.isSending === false) {
this.props.decreaseCategoryRank(category, categoryClicked)
}
this.props.fetchCategoryRanks(this.props.menu);
}}></div>
</div>
)
}
}
const mapStateToProps = state => ({
menu: state.menu
})
export default connect(mapStateToProps, { increaseCategoryRank, decreaseCategoryRank, fetchCategoryRanks } )(CategoryArrows);
menu.actions.js:
import { apiUrl, apiConfig } from '../../util/api';
import { INCREASE_CATEGORY_RANK, DECREASE_CATEGORY_RANK, FETCH_CATEGORY_RANKS } from './menu.types';
export const decreaseCategoryRank = (category, categoryClicked) => dispatch => {
dispatch({ type: DECREASE_CATEGORY_RANK, category, categoryClicked })
}
export const increaseCategoryRank = (category, categoryClicked) => dispatch => {
dispatch({ type: INCREASE_CATEGORY_RANK, category, categoryClicked })
}
export const fetchCategoryRanks = menu => async dispatch => {
console.log("Printing menu (fetch category ranks)");
console.log(menu);
var sentRequests = 0;
menu.map(async (category) => {
const menuLength = menu.length;
const options = {
...apiConfig(),
method: 'PUT',
body: JSON.stringify(category)
}
const response = await fetch(`${apiUrl}/category/${category._id}`, options)
let data = await response.json()
if (response.ok) {
console.log("It got sent")
sentRequests++;
console.log("Printing sentRequests");
console.log(sentRequests);
if(sentRequests === menuLength) {
console.log("All the requests have been sent");
}
} else {
alert(data.error)
}
});
dispatch({ type: FETCH_CATEGORY_RANKS, menu });
}
menu.types.js:
export const INCREASE_CATEGORY_RANK = "INCREASE_CATEGORY_RANK";
export const DECREASE_CATEGORY_RANK = "DECREASE_CATEGORY_RANK";
export const FETCH_CATEGORY_RANKS = "FETCH_CATEGORY_RANKS";
menu.reducer.js:
// import INITIAL_STATE from './menu.data';
import { INCREASE_CATEGORY_RANK, DECREASE_CATEGORY_RANK, FETCH_CATEGORY_RANKS } from './menu.types';
const INITIAL_STATE = []
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case INCREASE_CATEGORY_RANK: {
console.log("Went into increase category rank");
if(action.categoryClicked === false) {
return state;
}
const menuArray = [...state];
var index = menuArray.map(category => category._id).indexOf(action.category._id);
//if it's the first element in array it won't move up
if(index === 0) {
return state;
} else {
const temp = menuArray[index];
menuArray[index] = menuArray[index - 1];
menuArray[index - 1] = temp;
var newrank = 0;
menuArray.forEach(category => {
category.rank = newrank++;
});
return menuArray;
}
}
case DECREASE_CATEGORY_RANK: {
console.log("Went into decrease category rank");
if(action.categoryClicked === false) {
return state;
}
const menuArray = [...state];
console.log(menuArray);
var index = menuArray.map(category => category._id).indexOf(action.category._id);
//if it's the last element in the array, it won't move down
if(index === menuArray.length - 1) {
return state;
} else {
const temp = menuArray[index];
menuArray[index] = menuArray[index + 1];
menuArray[index + 1] = temp;
var newrank = 0;
menuArray.forEach(category => {
category.rank = newrank++;
});
return menuArray;
}
}
case FETCH_CATEGORY_RANKS:
return state;
default:
return state;
}
}
In my CategoryArrows component I have a state property called isSending which is set to false. In my fetchCategoryRanks action creator, I am sending information about categories from the menu array in the state to a server using fetch.
I would like to be able to set the isSending property from the CategoryArrows component to true or false, depending on certain conditions in the function fetchCategoryRanks.
However, I am not sure what the best way to do this is. Any insights are appreciated.
First way
You can change internal state by sending a callback function to your axios api call. Before the axios request starts you can call that callback function from axios api function to set isSending=true and after request completed again call callback function to set isSending=false. Callback function implementation must be in component from where you are calling axios api.
Api call
this.props.fetchCategoryRanks(this.props.menu, (response) => {
if (isRequestStart) {
this.setState({
isSending: true
});
}
if (!isRequestStart) {
this.setState({
isSending: false
});
}
});
Below is your fetch request
export const fetchCategoryRanks = (menu, callback) => async dispatch => {
var sentRequests = 0;
menu.map(async (category) => {
const menuLength = menu.length;
callback({
isRequestStart: true
});
const options = {
...apiConfig(),
method: 'PUT',
body: JSON.stringify(category)
}
const response = await
fetch(`${apiUrl}/category/${category._id}`, options)
let data = await response.json()
if (response.ok) {
callback({
isRequestStart: false
});
console.log("It got sent")
sentRequests++;
console.log("Printing sentRequests");
console.log(sentRequests);
if (sentRequests === menuLength) {
console.log("All the requests have been sent");
}
} else {
alert(data.error);
callback({
isRequestStart: false
});
}
});
dispatch({
type: FETCH_CATEGORY_RANKS,
menu
});
}
Second way
You can use a reducer where you can set initial state of isSending by dispatching a function from the axios api, calling the dispatch function same as above. And you can use that reducer state into your component.

How to an update an api call after an action is dispatched with a dependant value which is updated

I have the following App.js:
constructor(props) {
super(props);
this.props.setLoading();
}
componentDidMount(){
this.convert();
}
changeFromCurr = (event) => {
this.props.setFromCurrency(event.target.value);
this.convert();
}
changeToCurr = (event) => {
this.props.setToCurrency(event.target.value);
this.convert();
}
changeAmount = (event) => {
this.props.setValue(event.target.value);
}
convert = () => {
return this.props.convertCurr(this.props.fromCurrency,this.props.toCurrency,this.props.value);
}
render() {
const {fromCurrency, toCurrency, value, result} = this.props;
return (
<div>
<CurrencyForm
fromCurrency={fromCurrency}
toCurrency={toCurrency}
amount={value}
changeFromCurr={this.changeFromCurr}
changeToCurr={this.changeToCurr}
changeAmount={this.changeAmount}
result={result}
/>
My Request is for convertCurr is:
mport convertCurrency from '../service';
import {requestApi, receiveRes, accessDenied, apiError} from './currencyActions';
function convertCurr(from,to,amt) {
return dispatch => {
dispatch(requestApi());
convertCurrency(from,to,amt)
.then(res => res)
.then(res => {
if(res.error) {
throw(res.error);
}else if(res.accessDenied){
dispatch(accessDenied(res.accessDenied));
}
dispatch(receiveRes(res.data.rates[to]));
return res.result;
})
.catch(error => {
dispatch(apiError(error));
})
}
}
Which calls this service :
import axios from 'axios';
let convertCurrency = async (from,to,amt) => {
const API_KEY= `b688884ff57c3e17139e632b5f852755`;
const convertUrl = `http://data.fixer.io/api/latest?
access_key=${API_KEY}&base=${from}&symbols=${to}`
try {
const response = await axios.get(convertUrl);
console.log(response);
return response;
}
catch (err) {
console.log('fetch failed', err);
}
}
export default convertCurrency;
Now What I want to do is fire this service call once my Redux store gets updated with new fromCurrency property after the:
this.props.setFromCurrency(event.target.value);
updates the state, I want the service to be called with new values.
Any help would be much appreciated.
Use react life-cycle componentDidUpdate
componentDidUpdate(prevProps, prevState) {
if (yourMethodToCheckIfNotEqual(prevProps.fromCurrency, this.props.fromCurrency)) {
// Fire your service call
// Notice that if your service call changes fromCurrency above may cause infinite loop
}
}

Reactjs get data with async on componentDidMount

What I trying to do is, get user data before render, I made a object call API, it get data from api with axios (it get successfull) now I want to get it on info.js compontent, so I call it on componentDidMount:
API.js
const API = {
async getUser() {
const cookies = new Cookies();
const token = cookies.get('token');
let result = null;
if (token) {
await axios.get('http://localhost:8000/api/user').then(res => {
if (res.data.success) {
result = res.data.success;
}
});
return await result;
} else {
return false;
}
}
};
Info.js
import API from 'API'
export default class UserInfo extends React.Component {
constructor() {
super();
this.state = {result: null};
}
componentDidMount() {
let result = API.getUser();
this.setState({
result: result
});
console.log(result)
}
render() {
return(
<div className="UserInfo">
{this.state.result}
</div>
)
}
}
But it give me this:
PromiseĀ {<pending>}...
So I confused, what I have done wrong?
async componentDidMount() {
let result = await API.getUser();
this.setState({
result: result
});
console.log(result)
}
Probably this should fix it for you!
import API from 'API'
export default class UserInfo extends React.Component {
constructor() {
super();
this.state = { result: undefined };
}
async componentDidMount() {
let result = await API.getUser();
this.setState({ result });
}
render() {
return(
<div className="UserInfo">
{this.state.result && this.state.result.name}
</div>
)
}
}
It's because setState is asynchronous. Try it like this and you will get the result you expect:
this.setState({
result: result
}, () => console.log(result);
If you use this reactjs.org page there is a bit about it further down (search for callback).

Fetch function in React Native with mobx-react-lite and React hooks

I'm newbie in mobx-react and i need to write fetch function that get data from API and after renders it to FlatList. I have already created fetch function, set initial state with useContext hook and wrapped my app by observer mobx class. But now i need to implement of getting the data from the server. Can you tell me please which will be the best way to do it?
import { createContext } from 'react'
import { action, decorate, observable, computed, runInAction } from 'mobx'
import fetchData from '../utils/fetchData'
import mapObjects from '../utils/mapObjects'
class DataStore {
data = null
error = false
loading = true
get getData(){
return this.data
}
get getError(){
return this.error
}
get getLoading(){
return this.loading
}
async fetchData(url) {
this.data = null
this.error = false
this.loading = true
try {
console.log('TRY')
const response = await fetch(url)
const jsonResponse = await response.json()
const obj = await mapObjects(jsonResponse)
runInAction(() => {
console.log('WRITE!!!')
this.loading = false
this.data = obj
})
} catch (err) {
runInAction(() => {
console.log(err)
this.loading = false
this.error = err
})
}
}
}
decorate(DataStore, {
data: observable,
error: observable,
loading: observable,
fetchData: action
})
export default createContext(new DataStore())
My component:
import React, { useContext, useEffect, useState } from 'react'
import { ActivityIndicator, FlatList, Platform, StyleSheet, View } from 'react-native'
import DataStore from '../mobx/DataStore'
import { autorun } from 'mobx'
import { ChartsHeader, CryptoItem, IconsHeader, ProjectStatusBar } from '../components'
import { useFetch } from '../hooks/useFetch'
import { WP, HP } from '../constants'
const styles = StyleSheet.create({
container: {
flex: 1
}
})
const ChartsScreen = ({ navigation }) => {
const { container } = styles
const store = useContext(DataStore)
const url = 'https://poloniex.com/public?command=returnTicker'
console.log('store', store)
useEffect(() => {
store.fetchData(url)
}, [])
//*Call custom hook and data distruction
//const { data, error, loading } = useFetch(url)
//*Change percent amount color depends on the amount
const percentColorHandler = number => {
return number >= 0 ? true : false
}
return (
<View style={container}>
{Platform.OS === 'ios' && <ProjectStatusBar />}
<IconsHeader
dataError={store.error}
header="Charts"
leftIconName="ios-arrow-back"
leftIconPress={() => navigation.navigate('Welcome')}
/>
<ChartsHeader />
<ActivityIndicator animating={store.loading} color="#068485" style={{ top: HP('30%') }} size="small" />
<FlatList
data={store.data}
keyExtractor={item => item.key}
renderItem={({ item }) => (
<CryptoItem
name={item.key}
highBid={item.highestBid}
lastBid={item.last}
percent={item.percentChange}
percentColor={percentColorHandler(item.percentChange)}
/>
)}
/>
</View>
)
}
export { ChartsScreen }
For everybody who is still looking for the approach for the fetch function in React with MobX. I checked a lot of information, but could not find a good decision. But in the end I created mine. Maybe it will help for somebody:
MobX store:
import { action, observable, runInAction } from 'mobx'
class DataStore {
#observable data = null
#observable error = false
#observable fetchInterval = null
#observable loading = false
//*Make request to API
#action.bound
fetchInitData() {
const response = fetch('https://poloniex.com/public?command=returnTicker')
return response
}
//*Parse data from API
#action.bound
jsonData(data) {
const res = data.json()
return res
}
//*Get objects key and push it to every object
#action.bound
mapObjects(obj) {
const res = Object.keys(obj).map(key => {
let newData = obj[key]
newData.key = key
return newData
})
return res
}
//*Main bound function that wrap all fetch flow function
#action.bound
async fetchData() {
try {
runInAction(() => {
this.error = false
this.loading = true
})
const response = await this.fetchInitData()
const json = await this.jsonData(response)
const map = await this.mapObjects(json)
const run = await runInAction(() => {
this.loading = false
this.data = map
})
} catch (err) {
console.log(err)
runInAction(() => {
this.loading = false
this.error = err
})
}
}
//*Call reset of MobX state
#action.bound
resetState() {
runInAction(() => {
this.data = null
this.fetchInterval = null
this.error = false
this.loading = true
})
}
//*Call main fetch function with repeat every 5 seconds
//*when the component is mounting
#action.bound
initInterval() {
if (!this.fetchInterval) {
this.fetchData()
this.fetchInterval = setInterval(() => this.fetchData(), 5000)
}
}
//*Call reset time interval & state
//*when the component is unmounting
#action.bound
resetInterval() {
if (this.fetchInterval) {
clearTimeout(this.fetchInterval)
this.resetState()
}
}
}
const store = new DataStore()
export default store

Categories

Resources