I am trying to set a React state to an array of objects via an Axios get request. However, when I try to update the state, it shows up as an empty array.
I know for a fact that I am receiving the correct response from the API, so I think I am missing somethig when it comes to updating the state.
Here's my code:
const Home = () => {
const [movieTitle, setMovieTitle] = useState('');
const [searchResults, setSearchResults] = useState([]);
const handleChange = (e) => {
setMovieTitle(e.target.value);
};
const getMovieData = () => {
const apiKey = 'didntleakit';
const apiUrl = 'http://www.omdbapi.com/?apikey=' + apiKey + '&s=' + movieTitle;
Axios.get(apiUrl)
.then((res) => {
setSearchResults(res.data.Search);
});
console.log(searchResults);
};
return(
<div>
<p>Home page</p>
<TextField defaultValue="Movie" onChange={(e) => handleChange(e)}/>
<button onClick={getMovieData}/>
</div>
);
};
You cannot log the state variable right after setting it as setState is async. You can log the updated state inside a useEffect:
import {useEffect} from 'react'
useEffect(() => {
console.log(searchResults);
}, [searchResults])
That's because you are logging searchResults BEFORE data was retrieved. Axios.get() is an async function, to fix it, just put console.log right before returning JSX code:
console.log(searchResults);
return (<div>....</div>);
So when the data is retrieved from the endpoint, you will update searchResults which will re-render the component with updated state and log correct searchResults to the console.
You can not log state just after setting it as it is async so log it in useEffect and pass state as dependency array.
Always try to use spread operator while setting state if it is object or array.
const Home = () => {
const [movieTitle, setMovieTitle] = useState('');
const [searchResults, setSearchResults] = useState([]);
const handleChange = (e) => {
setMovieTitle(e.target.value);
};
const getMovieData = () => {
const apiKey = 'didntleakit';
const apiUrl = 'http://www.omdbapi.com/?apikey=' + apiKey + '&s=' +
movieTitle;
Axios.get(apiUrl)
.then((res) => {
setSearchResults([...res.data.Search]);//use spread operator for optimised code
});
};
useEffect(()=>{
console.log(searchResults);
},[searchResults])
return(
<div>
<p>Home page</p>
<TextField defaultValue="Movie" onChange={(e) => handleChange(e)}/>
<button onClick={getMovieData}/>
</div>
);
};
Related
i want to fetch data from an input so i create a controlled component like this
import React, { useState } from "react";
import axios from 'axios'
type Props = {data:any};
function Locality({data}: Props) {
const [city, setCity] = useState("13000");
return(
<input
list="city"
type="text"
placeholder={`Code postale, France`}
onChange={(e) => {
setCity(e.target.value) ;
}}
/>
)
}
i want to fetch an url according to the city set in the state but i don't know how to give this state to my fetch below:
export async function getStaticProps(){
const getDataUrl:string = `https://geo.api.gouv.fr/communes?codePostal=${city}&format=geojson`
const result = await axios.get(getDataUrl)
const data = result.data
return {
props:{
data : data.data[0].attributes
}
}
}
any idea ?
nextjs getStaticProps is for getting build-time data on the server. React.useState is for managing run-time state, on the client
If fetching your data relies on some user interaction, try doing this on the client with useEffect and useState
function Locality({ data }) {
const [city, setCity] = useState("13007");
const [features, setFeatures] = useState({});
const fetchData = async () => {
const url = `https://geo.api.gouv.fr/communes?codePostal=${city}&format=geojson`;
const result = await axios.get(url);
const data = result.data.features;
setFeatures(data);
};
useEffect(() => {
fetchData();
}, [city]);
return (
<>
<input
value={city}
type="text"
placeholder={`Code postale, France`}
onChange={(e) => {
setCity(e.target.value);
}}
/>
<pre>{JSON.stringify(features, null, 2)}</pre>
</>
);
}
You might also want to investigate nextjs getServerSideProps
As pointed out by #ksav, you would need to use some local state, but also, an effect to fetch the data.
Here is an example how to do that: (untested, for the idea)
import React, { useState } from "react";
import axios from 'axios'
type Props = {data:any};
function Locality({data}: Props) {
const [city, setCity] = useState("13000");
// That will contain the result of the fetch
const [isFetching, setIsFetching] = useState(false)
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
useEffect(function fetchResults() {
const getDataUrl:string = `https://geo.api.gouv.fr/communes?codePostal=${city}&format=geojson`
setIsFetching(true)
const result = axios.get(getDataUrl).then(res => {
const data = result.data
setResult(data.data[0].attributes)
}).catch(setError).finally(() => {
setIsFetching(false)
})
}
return(
<input
list="city"
type="text"
placeholder={`Code postale, France`}
onChange={(e) => {
setCity(e.target.value) ;
}}
/>{isFetching && 'Loading...'}
{!error && !isFetching &&
<div>Result for {city}: {result}</div>}
)
}
If you use the above effect that I or #ksav suggested, I suggest you look for XHR cancelation and effect debouncing, to avoid bugs that will occur as the user types in the box and many requests are sent at the same time. For example if I type '75000', it will send 5 requests, and if the 3rd request is the slowest, result can finally contain the result for 750, not 75000.
I'm trying to load a component using useEffect hook and axios. However, when loading the page the components are not render, but when inspecting the parent component, his children are undefined (I'm very new to react)
Component
export const BonusRows = () => {
const [state, setState] = useState([]);
const [order, setOrder] = useState("asc");
const bonusRequest = () => {
axios
.get(`http://localhost:4000/api/v1/process/creator/gutierad5`)
.then((res) => {
const bonus = res.data;
const processes = bonus.processes;
setState(processes);
console.log(processes);
});
};
useEffect(() => {
bonusRequest();
}, []);
return (
<ProcessTable
funcionality={() => {
sortByDate(state, setState, order, setOrder);
}}
>
<Accordion allowToggle allowMultiple>
{state.map((element, index) => {
<AccordionItemSlot
key={index}
proccessID={element.id}
title={element.name}
targeruser='gutierad5'
createDate={FormatDateInYYMMDD(element.createdAt)}
status={element.status}
creator={element.creator}
links={element.links}
amount={element.processBonus.amount}
updatedAt={element.updatedAt}
password={element.processBonus.password}
franchise={element.processBonus.franchise}
/>;
})}
</Accordion>
</ProcessTable>
);
};
I don't know what I'm doing wrong. My best guess is that the state is not present when the component is loaded, so thet's the reasong is undefined. However when console log the state, the data is there.
I'm using react-router-dom v5, and react 16
Whenever I navigate like this:
from "/products/:someId" to "/products/:someOtherId"
the url changes but the components do not update accordingly
it's the same with queries.
"/products?search=something" or "/products?search=someOtherThing"
it does work when I'm in a different url, like "/" or "/users",
I'm using Link for navigation, I also tried the useHistory hook: history.push, history.replace and withRouter(myComponent);
This is how I'm getting data from queries/params
async function searchProducts(searchValue) {
const response = await axios.post(
"http://localhost:8000/api/products/search",
{ search: searchValue });
return response.data.body;
}
const useFetchData = (query) => {
const [products, setProducts] = useState([]);
useEffect(() => {
if (products.length === 0) {
// Use searchProducts for the request
searchProducts(query).then((foundProducts) => {
setProducts(foundProducts);
});
}
}, [products, query]);
return products;
};
then I useFetchData in my component which goes:
const ProductList = () => {
const history = useHistory();
// parsing query to be -> { search: "value" }
const urlQuery = queryString.parse(history.location.search);
const products = useFetchData(urlQuery.search);
const getList = () => {
return products.map((product) => {
return (
<li key={product._id}>
<ProductItem product={product} />
</li>
);
});
};
return <div className="container">{getList()}</div>;
};
The search button is in a different component for the header, it's always there since it's in the layout
<button className="header-search-button" onClick={handleClick}>
Search
</button>
and the handleClick:
// searchvalue has it's own onChange handler
const [searchValue, setSearchValue] = useState("");
// code...
const handleClick = () => {
// .... some code
// I also tried with push and Link
history.replace(`/products?search=${searchValue}`);
};
It's really difficult to tell without the code.
But my guess is you are able to change the /products?search=bag to /products?search=watch.
But after that it's not able to update the state and hence no re render.When you reload then the render happens.
It would be easier if we could see the code.
Currently, my code re-renders every time the query parameter is updated. Once I remove the query parameter; however, I get a warning stating "React Hook useCallback has a missing dependency: 'query'. Either include it or remove the dependency array react-hooks/exhaustive-deps". I have tried just defining my getData function within the useEffect, but I am using getData as on onclick function outside of the useEffect. What I am trying to accomplish is to initially fetch articles on react hooks and then only fetch new data on submit as opposed to when the query is updated and not have any warnings about query being a missing dependency as well. Any suggestions would help immensely. the code is as follows:
import React, { useState, useEffect, useCallback } from "react"
import axios from "axios"
const Home = () => {
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
const getData = useCallback(async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
}, [query])
useEffect(() => {
getData()
}, [getData])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
return (
<div>
<input type='text' onChange={handleChange} value={query} />
<button type='button' onClick={getData}>
Submit
</button>
{data &&
data.hits.map(item => (
<div key={item.objectID}>
{item.url && (
<>
<a href={item.url}>{item.title}</a>
<div>{item.author}</div>
</>
)}
</div>
))}
</div>
)
}
export default Home
Add a submitting state as a condition for triggering your axios request
const [submitting, setSubmitting] = useState(true)
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
useEffect(() => {
const getData = async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
setSubmitting(false) // call is finished, set to false
}
// query can change, but don't actually trigger
// request unless submitting is true
if (submitting) { // is true initially, and again when button is clicked
getData()
}
}, [submitting, query])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
const getData = () => setSubmitting(true)
If you wanted to useCallback, it could be refactored as such:
const [submitting, setSubmitting] = useState(true)
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
const getData = useCallback(async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
}, [query])
useEffect(() => {
if (submitting) { // is true initially, and again when button is clicked
getData().then(() => setSubmitting(false))
}
}, [submitting, getData])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
and in render
<button type='button' onClick={() => setSubmitting(true)}>
How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?
What i see so far is to have something "indirect" like:
export default = () => {
const [sendRequest, setSendRequest] = useState(false);
useEffect(() => {
if(sendRequest){
//send the request
setSendRequest(false);
}
},
[sendRequest]);
return (
<input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
);
}
Is that the proper way or is there some other pattern?
export default () => {
const [isSending, setIsSending] = useState(false)
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
this is what it would boil down to when you want to send a request on click and disabling the button while it is sending
update:
#tkd_aj pointed out that this might give a warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.
export default () => {
const [isSending, setIsSending] = useState(false)
const isMounted = useRef(true)
// set isMounted to false when we unmount the component
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
if (isMounted.current) // only update if we are still mounted
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method
const App = (props) => {
//define you app state here
const fetchRequest = useCallback(() => {
// Api request here
}, [add dependent variables here]);
return (
<input type="button" disabled={sendRequest} onClick={fetchRequest}
);
}
Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop
In functional programming, any async function should be considered as a side effect.
When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).
Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.
Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.
to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted
/**
* check if the component still mounted
*/
export const useIsMounted = () => {
const mountedRef = useRef(false);
const isMounted = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return isMounted;
};
Then your code should look like this
export const MyComponent = ()=> {
const isMounted = useIsMounted();
const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);
// do my async thing
const doMyAsyncThing = useCallback(async () => {
// do my stuff
},[])
/**
* do my async thing effect
*/
useEffect(() => {
if (isDoMyAsyncThing) {
const effect = async () => {
await doMyAsyncThing();
if (!isMounted()) return;
setIsDoMyAsyncThing(false);
};
effect();
}
}, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);
return (
<div>
<button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
Do My Thing {isDoMyAsyncThing && "Loading..."}
</button>;
</div>
)
}
Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)
UPDATE:
Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.
Example:
import {useAsyncFn} from 'react-use';
const Demo = ({url}) => {
const [state, doFetch] = useAsyncFn(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
<button onClick={() => doFetch()}>Start loading</button>
</div>
);
};
You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.
Example
const { useState } = React;
function getData() {
return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}
function App() {
const [data, setData] = useState(0)
function onClick() {
getData().then(setData)
}
return (
<div>
<button onClick={onClick}>Get data</button>
<div>{data}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:
const [requestSent, setRequestSent] = useState(false);
const sendRequest = () => {
setRequestSent(true);
fetch().then(() => setRequestSent(false));
};
Working example
You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).
useApi hook:
export type ApiMethod = "GET" | "POST";
export type ApiState = "idle" | "loading" | "done";
const fetcher = async (
url: string,
method: ApiMethod,
payload?: string
): Promise<any> => {
const requestHeaders = new Headers();
requestHeaders.set("Content-Type", "application/json");
console.log("fetching data...");
const res = await fetch(url, {
body: payload ? JSON.stringify(payload) : undefined,
headers: requestHeaders,
method,
});
const resobj = await res.json();
return resobj;
};
export function useApi(
url: string,
method: ApiMethod,
payload?: any
): {
apiState: ApiState;
data: unknown;
execute: () => void;
} {
const [apiState, setApiState] = useState<ApiState>("idle");
const [data, setData] = useState<unknown>(null);
const [toCallApi, setApiExecution] = useState(false);
const execute = () => {
console.log("executing now");
setApiExecution(true);
};
const fetchApi = useCallback(() => {
console.log("fetchApi called");
fetcher(url, method, payload)
.then((res) => {
const data = res.data;
setData({ ...data });
return;
})
.catch((e: Error) => {
setData(null);
console.log(e.message);
})
.finally(() => {
setApiState("done");
});
}, [method, payload, url]);
// call api
useEffect(() => {
if (toCallApi && apiState === "idle") {
console.log("calling api");
setApiState("loading");
fetchApi();
}
}, [apiState, fetchApi, toCallApi]);
return {
apiState,
data,
execute,
};
}
using useApi in some component:
const SomeComponent = () =>{
const { apiState, data, execute } = useApi(
"api/url",
"POST",
{
foo: "bar",
}
);
}
if (apiState == "done") {
console.log("execution complete",data);
}
return (
<button
onClick={() => {
execute();
}}>
Click me
</button>
);
For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.
<const Component= (props) => {
//define you app state here
const getRequest = useCallback(() => {
// Api request here
}, [dependency]);
return (
<input type="button" disabled={sendRequest} onClick={getRequest}
);
}
My answer is simple, while using the useState hook the javascript doesn't enable you to pass the value if you set the state as false. It accepts the value when it is set to true. So you have to define a function with if condition if you use false in the usestate