Setting state with React Hooks + Fetch API does not trigger re-render - reactjs

I'm trying to learn React Hooks by building a simple Domain Availability checker. Specifically, I'm playing with useState()
The aim is just to have an input field where the user types a keyword, hits Enter, and then the app will run a fetch request for that keyword with a number of different domain endings.
Here is my App component (or check codesandbox here: https://codesandbox.io/s/5410rkrq1p)
const App = () => {
const domainEndings = [
".ws",
".ga",
".cf",
".tk",
".ml",
".gq",
".kz",
".st",
".fm",
".je"
];
const [domainString, setDomainString] = useState("");
const [domainsArray, setDomainsArray] = useState([]);
const [lookedUpDomainsArray, setLookedUpDomainsArray] = useState([]);
const handleDomainChange = event => {
setDomainString(event.target.value);
setDomainsArray(
domainEndings.map(ending => event.target.value.trim() + ending)
);
};
let testArray = [];
const runDomainLookup = url => {
return fetch(
`https://domainr.p.rapidapi.com/v2/status?domain=${url}&mashape-key=${myAPIKEY}`
)
.then(res => res.json())
.then(data => {
testArray.push({
url: data.status[0].domain,
status: data.status[0].status
});
setLookedUpDomainsArray(testArray);
});
};
const handleSubmit = e => {
e.preventDefault();
setLookedUpDomainsArray([]);
testArray = [];
domainsArray.map(eachDomain => runDomainLookup(eachDomain));
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={domainString}
placeholder="type keyword then hit enter"
onChange={e => handleDomainChange(e)}
/>
</form>
{lookedUpDomainsArray &&
lookedUpDomainsArray.map((domain, index) => {
return (
<div key={index}>
{domain.url} is {domain.status}
</div>
);
})}
</div>
);
};
The bug that I'm experiencing is:
the state seems to be being set correctly (checked in React Dev Tools).
the first response from the mapped fetch requests is rendered correctly to the DOM
a re-render for the newly added state (from fetch request) is not triggered until the user presses a key in the input field
Here is a video demonstrating it : https://streamable.com/klshu
You will notice that the rest of the results don't appear until I start typing other characters into the input
Thanks in advance ๐Ÿ™

Thanks! Yes I had tried Promise.all() however I wanted each fetch request to run separately (for example, if one fetch request times out, it would hold up ALL of the others).
Found a solution on Reddit
Essentially, when setting the state via Hooks, I should have passed in the previous state as so:
const runDomainLookup = url => {
return fetch(
`https://domainr.p.rapidapi.com/v2/status?domain=${url}&mashape-key=${myAPIKEY}`
)
.then(res => res.json())
.then(data => {
setLookedUpDomainsArray(prevArray => [
...prevArray,
{
url: data.status[0].domain,
status: data.status[0].status
}
]);
});
};
Working solution on CodeSandbox: https://codesandbox.io/s/51ky3r63x

Related

React setState gives emty array in an axiosRequest

Hi I do have to following simplyfied code. I use Formik as a Validation. Also Material Ui and Reactjs. The Form, Row and Col Tags come from Material. The FastField is same as InputField.
What I want is onClick in the Inputfield a dropdown appears and shows an array which I fetched with the axios-Request.
ยดยดยด
const url = 'http://localhost:3000';
const [searchValues, setSearchValues] = useState([]);
const getDropdownItems = (event) => {
console.log('event', event.target.getAttribute('id'));
axios
.get(`${url}/${event.target.getAttribute('id')}`)
.then(
(res) => setSearchValues(res),
console.log('restl', searchValues)
);
};
render(
<Form
onFocus={getDropdownItems}
onSubmit={formik.handleSubmit}
>
<Row>
<Col xs="auto" style={minWidth}>
<FastField
id="DatumEingabe"
name="DatumEingabe"
component={Autocomplete}
label="Datum-Eingabe"
type="text"
options={searchValues}
/>
</Col>
</Row>
</Form>
)
When I check my console I get from the first console.log the name of
the Inputfield. The second console.log says the array is empty,
despite the res is available and should be set. Why does it not work
this way.
setSearchValues(res) will not update searchValues until the next render. If you want to log it each time it changes, you should instead do
const [searchValues, setSearchValues] = useState([]);
useEffect(() => {
console.log(searchValues);
}, [searchValues]);
const getDropdownItems = (event) => {
console.log('event', event.target.getAttribute('id'));
axios
.get(`${url}/${event.target.getAttribute('id')}`)
.then(
(res) => setSearchValues(res)
);
};
I don't think the change is made inmediatly. Try logging searchValues after a second or something like that to see if that is the problem.
const getDropdownItems = (event) => {
console.log('event', event.target.getAttribute('id'));
axios
.get(`${url}/${event.target.getAttribute('id')}`)
.then(
(res) => {
setSearchValues(res);
setTimeout(() => {
console.log('restl', searchValues);
}, 1000)
}
);
};
Also, you have the useEffect hook, which fires an event when a variable is change, so if you want to log it the second it changes you should use:
useEffect(() => {
console.log(searchValues);
}, [searchValues])
To acomplish that, remember to import:
import { useEffect } from "react";
or use
React.useEffect(...)

Mapping into firestore from React

I am new to react and firebase/firestore.
I am trying to map into what I believe to be a nested firestore value. I am able to pull each value individually
function Pull() {
const [blogs,setBlogs]=useState([])
const fetchBlogs=async()=>{
const response=firestore.collection('customer');
const data= await response.get();
data.docs.forEach(item=>{
setBlogs(data.docs.map(d => d.data()))
console.log(data)
})
}
useEffect(() => {
fetchBlogs();
}, [])
return (
<div className="App">
{
blogs.map((items)=>(
<div>
<p>{items[1].name}</p>
</div>
))
}
</div>
);
}
I have been trying to map twice to get into the string inside the collection, yet I have had no luck.
My FireStore collection
https://drive.google.com/file/d/1Erfi2CVrBSbWocQXGR5PB_ozgg9KEu12/view?usp=sharing
Thank you for your time!
If you are iterating a data.docs array and enqueueing multiple state updates then you will want to use a functional state update to correctly enqueue, and update from the previous state.
const fetchBlogs = async ( )=> {
const response = firestore.collection('customer');
const data = await response.get();
data.docs.forEach(item => {
setBlogs(blogs => blogs.concat(item.data()))
});
}
or you can map the data.docs to an array of items and update state once.
const fetchBlogs = async ( )=> {
const response = firestore.collection('customer');
const data = await response.get();
setBlogs(blogs => blogs.concat(data.docs.map(item => item.data())));
}
try changing the foreach to a snapshot like this:
data.docs.onSnapshot(snapshot=>{
setBlogs(snapshot.docs.map(d => d.data()))
console.log(data)
})
ive used it like this in the past multiple times
and it has worked. If it doesnt work, the instagram clone tutorial on youtube by Clever Programmer goes over this well.

Enter input data to api's endpoint query in react hooks

I am new to React. I want to integrate weatherstack API with my project. Problem is I do not understand how can I input country's capital's name into API's endpoint.
const App = () => {
const [ countries, setCountries ] = useState([])
const [ searchingItem, setSearchingItem ] = useState('')
const [ showAll, setShowAll ] = useState(true)
const [ weatherData, setWeatherData ] = useState([])
const [ capital, setCapital ] = useState('')
const api_key = process.env.REACT_APP_WEATHER_API
useEffect(() => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
setCountries(countries.concat(response.data))
})
}, [])
useEffect(() => {
console.log(capital, searchingItem)
axios
.get(`http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`)
.then( response => {
console.log(searchingItem)
setWeatherData(weatherData.concat(response.data))
})
.catch((err) => console.log(err))
}, [])
const handleChangeSearch = (event) => {
setSearchingItem(event.target.value)
setShowAll(false)
}
let countriesToShow = showAll
? countries
: countries.filter(country => country.name.indexOf(searchingItem) !== -1);
const handleClick = (country) => {
setCapital(country.capital)
setSearchingItem(country.name)
}
return (
<div id='App'>
<h2>Phonebook</h2>
<div>
Search a name <Filter value={searchingItem} onChange={handleChangeSearch} />
<div>
{ countriesToShow.length === 250
? ''
: countriesToShow.length >= 10
? <div><p>Too many matches, specify another filter</p></div>
: countriesToShow.length > 1
? countriesToShow.map(country => <div>
<li key={nanoid()}>{country.name}
<button type='button' onClick={() => handleClick(country)}>Show</button></li></div>)
: countriesToShow.length === 1
? <div>
<View country={countriesToShow[0]} key={nanoid()}/>
</div>
: 'No results occured'}
</div>
<div>
<div>{console.log(weatherData)}</div>
</div>
</div>
</div>
)
I need to display weather data below the View component if I pressed button 'Show'.
I have tried to create state variable capital and set it to country's name whenever onClick event will happen:
const handleChangeSearch = (event) => {
setSearchingItem(event.target.value)
setShowAll(false)
}
But it seems like I can't access to capital variable in the useState hook:
useEffect(() => {
console.log(capital, searchingItem)
axios
.get(`http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`)
.then( response => {
console.log(capital)
setWeatherData(weatherData.concat(response.data))
})
.catch((err) => console.log(err))
}, [])
because this is the only thing that I am getting when hook makes a call:
error:
code: 601
info: "Please specify a valid location identifier using the query parameter."
type: "missing_query"
__proto__: Object
success: false
Any hints about how to display weather info whenever user clicks Show button?
Here is repo of the project if it is needed: https://github.com/rd9911/UniHelsinkiFullStack/tree/main/part2/part2c
If more info needed please ask.
When the hook runs initially, capital is empty ''. You want to call the API when capital changes using dep array ([capital]) and avoid calling the API when it is empty. Try this:
useEffect(() => {
if(! capital) { return;}
console.log(capital, searchingItem)
axios
.get(`http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`)
.then( response => {
console.log(searchingItem)
setWeatherData(weatherData.concat(response.data))
})
.catch((err) => console.log(err))
}, [capital])
When you provide no values inside the second argument(which is an array) of useEffect hook, it will run only once in the beginning. So in your case the call to the API goes even before the capital variable is set. To solve this you can just add capital to the array [capital] as a second argument to useEffect.

Setting and updating form inputs with firebase database and react hooks

In my app I have profile section with a form. When the component mounts I want to fetch user data from firebase, and display it in the form, with the current values of the user profile. Either using the "value" prop or the "placeholder" prop.
When the user makes changes in the form inputs and submit the changes, I want the database to update and the form to update with the new data.
Currently I can make the database value appear in the form input field, or I can make the form input field empty, but update the database. But not both.
The following code makes the database data render in the form input, but it cant be changed.
I know it could be something with the second useEffect() and the getUserData() function, that I cant seem to figure out.
const UserEdit = (props) => {
const [currentUser, setCurrentUser] = useState('');
const [forening, setForening] = useState('');
useEffect(() => {
firebase_app.auth().onAuthStateChanged(setCurrentUser);
}, [])
const getUserData = async () => {
await dbRef.ref('/' + currentUser.uid + '/profil/' ).once('value', snapshot => {
const value = snapshot.val();
setForening(value)
})
}
useEffect(() => {
getUserData()
},[] )
const handleInput = (event) => {
setForening(event.target.value)
}
const updateUserData = () => {
dbRef.ref('/' + currentUser.uid + '/profil/' ).set({foreningsnavn: forening}, function(error) {
if(error) {
console.log("update failed")
} else {
alert(forening)
}
})
}
const handleClick = () => {
updateUserData()
}
return (
<>
<div className="card-body">
<div className="row">
<div className="col-md-5">
<div className="form-group">
<label className="form-label">{Forening}</label>
<input className="form-control" type="text" value={forening} onChange={handleInput}/>
</div>
</div>
</div>
</div>
</>
)
}
Your second useEffect will run only one time because the second argument array [] of dependencies is empty:
useEffect(() => {
getUserData()
},[] )
You can add foreign dependency to make useEffect run with input change
useEffect(() => {
getUserData()
},[foreign] )
or you can use polling to sync database state

Mocking the fetching state of URQL in Jest

I'm trying to mock the fetching state in my tests. The state with data and state with an error were successfully mocked. Below you can find an example:
const createMenuWithGraphQL = (graphqlData: MockGraphQLResponse): [JSX.Element, MockGraphQLClient] => {
const mockGraphQLClient = {
executeQuery: jest.fn(() => fromValue(graphqlData)),
executeMutation: jest.fn(() => never),
executeSubscription: jest.fn(() => never),
};
return [
<GraphQLProvider key="provider" value={mockGraphQLClient}>
<Menu {...menuConfig} />
</GraphQLProvider>,
mockGraphQLClient,
];
};
it('displays submenu with section in loading state after clicking on an item with children', async () => {
const [Component, client] = createMenuWithGraphQL({
fetching: true,
error: false,
});
const { container } = render(Component);
const itemConfig = menuConfig.links[0];
fireEvent.click(screen.getByText(label));
await waitFor(() => {
const alertItem = screen.getByRole('alert');
expect(client.executeQuery).toBeCalledTimes(1);
expect(container).toContainElement(alertItem);
expect(alertItem).toHaveAttribute('aria-busy', 'false');
});
});
The component looks like the following:
export const Component: FC<Props> = ({ label, identifier }) => {
const [result] = useQuery({ query });
return (
<div role="group">
{result.fetching && (
<p role="alert" aria-busy={true}>
Loading
</p>
)}
{result.error && (
<p role="alert" aria-busy={false}>
Error
</p>
)}
{!result.fetching && !result.error && <p>Has data</p>}
</div>
);
};
I have no idea what am I doing wrong. When I run the test it says the fetching is false. Any help would be appreciated.
maintainer here,
I think the issue here is that you are synchronously returning in executeQuery. Let's look at what happens.
Your component mounts and checks whether or not there is synchronous state in cache.
This is the case since we only do executeQuery: jest.fn(() => fromValue(graphqlData)),
We can see that this actually comes down to the same result as if the result would just come out of cache (we never need to hit the API so we never hit fetching).
We could solve this by adding a setTimeout, ... but Wonka (the streaming library used by urql) has built-in solutions for this.
We can utilise the delay operator to simulate a fetching state:
const mockGraphQLClient = {
executeQuery: jest.fn(() => pipe(fromValue(data), delay(100)),
};
Now we can check the fetching state correctly in the first 100ms and after that we are able to utilise waitFor to check the subsequent state.

Resources