Axios + React (Next.js), post request loses track of the context / state - reactjs

Been scratching my head for hours, this bug doesn't make any sense to me.
Similarly to this solved problem, the react context and state is lost after the request is complete. The only problem is that all my functions are already bound.
The code below should return an image url and add it to the "image" array state every time the button is clicked.
What's not working is that the contents of the "image" array state is magically forgotten inside the axios promise, meaning that all previous images are lost, and only the image returned by the request will end up in the state.
I never had something like this happen before, feel like i must be missing some incredibly stupid mistake i've made. Anyone have any ideas what might be going on here?
Thank you,
// Added
const RANDOM_PROMPTS = [
'a whale flying in the sky',
'a skeleton dancing',
'a sunflower growing in a field'
]
export default function Home() {
const [prompt, setPrompt] = useState('')
const [images, setImages] = useState([])
// Added
useEffect(() => {
randomPrompt()
}, [])
const handleSubmit = event => {
event.preventDefault()
event.stopPropagation()
axios.post('http://localhost:3000/v1/generate', {prompt})
.then(res => {
res.data && add(res.data.url)
})
}
const add = (url) => {
// images state is always [] here
console.log(images)
setImages([...images, url])
}
// Added
const randomPrompt = () => {
const prompt = RANDOM_PROMPTS.shift()
setPrompt(`a painting of ${prompt}, by Paul Delvaux`)
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" value={prompt} onChange={event => setPrompt(event.target.value)}/>
<button type="submit">Draw</button>
</form>
</div>
)
}

Did you try to make youre function handleSubmit async function ?
const handleSubmit = async (event) => {
event.preventDefault()
event.stopPropagation()
const {data} = await axios.post('http://localhost:3000/v1/generate',
{prompt},{
validateStatus: (status) => status === 201,
})
if(!!data && !!data.url) {
add(data.url)
}
}
Je rajouterais que tu peux modifier ta fonction add comme suit:
const add = (url) => {
console.log(images)
setImages((prevState) => [...prevState, url])
}
My opinion is during the .then you may lose the contexte of youre react state.

Random Prompts should not have any effect on the Axios request you are making on a different useEffect() dependency. Please try my code below which I tried to do what I suggested in my comment.
const RANDOM_PROMPTS = [
'a whale flying in the sky',
'a skeleton dancing',
'a sunflower growing in a field'
]
export default function Home() {
const [prompt, setPrompt] = useState('')
const [images, setImages] = useState([])
const [initiateRequest, setInitiateRequest] = useState(false)
// Added
useEffect(() => {
randomPrompt()
}, [])
async function fetchData() {
let response = axios.post('http://localhost:3000/v1/generate', {prompt})
.then(res => {
res.data && add(res.data.url)
})
}
useEffect(() => {
fetchData()
}, [initiateRequest])
const handleSubmit = event => {
event.preventDefault()
event.stopPropagation()
setInitiateRequest(true)
}
const add = (url) => {
// images state is always [] here
console.log(images)
setImages([...images, url])
}
// Added
const randomPrompt = () => {
const prompt = RANDOM_PROMPTS.shift()
setPrompt(`a painting of ${prompt}, by Paul Delvaux`)
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" value={prompt} onChange={event => setPrompt(event.target.value)}/>
<button type="submit">Draw</button>
</form>
</div>
)
}

Related

Get input value with react hooks to search on oMdb api

I want to do a movie search with the oMdb api using React Hooks.
The result is not as expected. I seem to break some React Hooks rule that I don't understand.
Here is the code.
HOOK TO SEARCH
The Hook inside of a store.
(If I use searchMovies('star wars') in a console.log I can see the result of star wars movies and series.)
import React, { useState, useEffect } from "react";
const useSearchMovies = (searchValue) => {
const API_KEY = "731e41f";
const URL = `http://www.omdbapi.com/?&apikey=${API_KEY}&s=${searchValue}`
// Manejador del estado
const [searchMovies, setSearchMovies] = useState([])
//Llamar y escuchar a la api
useEffect(() => {
fetch(URL)
.then(response => response.json())
.then(data => setSearchMovies(data.Search))
.catch((error) => {
console.Console.toString('Error', error)
})
}, []);
return searchMovies;
};
THE INPUT ON A SANDBOX
Here i have the input to search with a console log to see the result.
import React, { useState } from "react";
import searchMovies from "../store/hooks/useSearchMovies";
const Sandbox = () => {
const [search, setSearch] = useState('')
const onChangeHandler = e =>{
setSearch(e.target.value)
console.log('Search result', searchMovies(search))
}
const handleInput =()=> {
console.log('valor del input', search)
}
return (
<div>
<h1>Sandbox</h1>
<div>
<input type="text" value={search} onChange={onChangeHandler}/>
<button onClick={handleInput()}>search</button>
</div>
</div>
)
}
export default Sandbox;
Issue
You are breaking the rules of hooks by conditionally calling your hook in a nested function, i.e. a callback handler.
import searchMovies from "../store/hooks/useSearchMovies";
...
const onChangeHandler = e => {
setSearch(e.target.value);
console.log('Search result', searchMovies(search)); // <-- calling hook in callback
}
Rules of Hooks
Only call hooks at the top level - Don’t call Hooks inside loops,
conditions, or nested functions.
Solution
If I understand your code and your use case you want to fetch/search only when the search button is clicked. For this I suggest a refactor of your useSearchMovies hook to instead return a search function with the appropriate parameters enclosed.
Example:
const useSearchMovies = () => {
const API_KEY = "XXXXXXX";
const searchMovies = (searchValue) => {
const URL = `https://www.omdbapi.com/?apikey=${API_KEY}&s=${searchValue}`;
return fetch(URL)
.then((response) => response.json())
.then((data) => data.Search)
.catch((error) => {
console.error("Error", error);
throw error;
});
};
return { searchMovies };
};
Usage:
import React, { useState } from "react";
import useSearchMovies from "../store/hooks/useSearchMovies";
const Sandbox = () => {
const [search, setSearch] = useState('');
const [movies, setMovies] = useState([]);
const { searchMovies } = useSearchMovies();
const onChangeHandler = e => {
setSearch(e.target.value)
};
const handleInput = async () => {
console.log('valor del input', search);
try {
const movies = await searchMovies(search);
setMovies(movies);
} catch (error) {
// handle error/set any error state/etc...
}
}
return (
<div>
<h1>Sandbox</h1>
<div>
<input type="text" value={search} onChange={onChangeHandler}/>
<button onClick={handleInput}>search</button>
</div>
<ul>
{movies.map(({ Title }) => (
<li key={Title}>{Title}</li>
))}
</ul>
</div>
);
};
export default Sandbox;

Cannot set React state to array of objects

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>
);
};

Refresh tag info

have some problem, do little pokedex, have after chose the cound of cards on the page i need to reload a container with cards, can u help me?
To get selected item use onSelect,
handleSelect = (e) => {
this.setState({value:e})
}
<DropdownButton
variant="danger"
alignRight
id="dropdown-menu-align-right"
onSelect={this.handleSelect}>
and get it to link in component Pokemon list
<div className="col">
<PokemonList pages={this.value} />
</div>
PokemonList working like this
function PokemonList({ pages }) {
const [pokemonList, setPokemonList] = useState([]);
const [currPage, setCurrPage] = useState(
`https://pokeapi.co/api/v2/pokemon?offset=0&limit=${pages}`
);
const [nextPage, setNextPage] = useState();
const [prevPage, setPrevPage] = useState();
const [pageNum, setPageNum] = useState(0);
useEffect(() => {
let cancel;
axios
.get(currPage, {
cancelToken: new axios.CancelToken((c) => (cancel = c)),
})
.then((res) => {
setPokemonList(res.data.results);
setPrevPage(res.data.previous);
setNextPage(res.data.next);
})
.catch((error) => {
console.log(error);
});
return () => {
cancel();
};
}, [currPage, pageNum]);
i don't know but after select item at deop down, nothing changes, can u please help me
Find the problem, i tried to change only value, but i should to change the link, so answer was:
const handleChange = (e) => {
setCurrPage(`https://pokeapi.co/api/v2/pokemon?offset=${pageNum}&limit=${e}`);
};
The problem is that you try to store props inside state and with useEffect listen on state change. Because of this React can't properly update components. The currPage state doesn't change when pages change. You should avoid this because it's anti-pattern.
Working short example:
const Component = ({pages}) => {
const [pokemonList, setPokemonList] = useState([]);
useEffect( () => {
fetch("/api/pokemons/" + pages )
.then( res => res.json() )
.then( data => setPokemonList(data.pokemonList ))
.catch( err => console.log("handle errors") )
})
return <div>{ pokemonList.map( pokemon => <div>{ pokemon.name }</div>) }</div>
}

async custom hooks does't provide updated data

I have a simple hook to help me handle a POST request. With the following code, I expect unsub will be true after the POST is done. Can anyone point out anything I could have done wrong?
Custom Hook
const useUnsubscribeEmail = () => {
const [userId, setUserId] = useState(null);
const [unsub, setUnSub] = useState();
const UNSUB_URL = '/web-registry-api/v1/reviews/unsubscription';
useEffect(() => {
if (userId) {
// async POST call
(async () => {
try {
await ApiService.post(`${UNSUB_URL}/${userId}`);
// update unsub value
setUnSub(true);
} catch (error) {
console.error(error)
}
})();
}
}, [userId]);
return [unsub, setUserId];
};
export default useUnsubscribeEmail;
Component
const ReviewUnsubscription = () => {
const { userId } = useParams();
const [unsub, unsubscribeEmail] = useUnsubscribeEmail();
return (
<MinimumLayout>
<div className={styles.content}>
<h1>Unsubscribe from email reminders to review products you’ve received from Zola?{unsub}</h1>
{/* unsub here is still undefined */}
<Button disabled={unsub} onClick={() => { unsubscribeEmail(userId); }} variant="primary" className={styles.button}>Unsubscribe</Button>
</div>
</MinimumLayout>
);
};
unsub is still going to be undefined until you click the button as you have not set a default state for it in your hook.
change : const [unsub, setUnSub] = useState(); to const [unsub, setUnSub] = useState(false); is what I would recommend
I tested on my side and works just fine; However, I cannot test the APIService.post.

How to send request on click React Hooks way?

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

Resources