Reputation: 93
I am learning react and I am trying to use a text input in a dynamic fetch request
My component is defined as ...
export default testpage = () => {
const [state, setState] = React.useState({})
let handleChange = (event) => {
setState({input: event.target.value})
}
async function buttonClick (input) {
console.log(state.input)
await fetch(`http://localhost:8080/api/${input}`)
.then(response => response.json())
.then(data => setState({...state, data}))
render(
<input type={'text'} onChange={handleChange.bind(this)} />
<Button onClick={() => buttonClick(state.input)}>test</Button>
)
}
My problem relates to useState updating asynchronously. If I enter a number ie. 4 into the input box and then click the button. The first time I click the button the fetch fails because undefined is passed to the fetch statement because the state hasn't been updated. If I click the button a second time the fetch succeeds. I have read into the useEffect hook but I am unable to figure out how to apply it to my situation.
Upvotes: 1
Views: 1398
Reputation: 1534
Change the code to keep input's value directly in the state. The state value not need to be an object - it can be a string, number or null
if that’s all you need.
const TestPage = () => {
const [postId, setPostId] = useState(null);
async function buttonClick() {
await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`)
.then(response => response.json())
.then(data => console.log(data));
}
return (
<div>
<input onChange={e => setPostId(e.target.value)} />
<button onClick={buttonClick}>test</button>
</div>
);
};
The comonent already works as expected - it downloads data on every button click. It requires a display logic and a proper error handling, but I leave it for clarity.
You mentioned useEffect
and here is the example of how you can use it:
function Test() {
const [postId, setPostId] = useState(null);
const [data, setData] = useState([]);
useEffect(() => {
async function getComments() {
if (Number(postId)) {
await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}/comments`
)
.then(response => response.json())
.then(data => setData(data));
} else { setData([]); }
}
getComments();
}, [postId]);
const comments = data
? data.map(comment => <li key={comment.id}>{comment.body}</li>)
: [];
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<input type={"text"} onChange={e => setPostId(e.target.value)} />
{comments.length > 0 ? <ul>{comments}</ul> : <span>Write correct post ID (number 1-100)</span>}
</div>
);
}
But useEffect
changes how you interact with your component. It runs an effect after rendering new state, meaning it runs right after changing input's value. Meaning, you don't need the <button>
at all.
Because you begin request on button click it is better to use useCallback
hook. It returns the same function on every button click as long as postId
(input's value) doesn't change. You can use this function the same as you used buttonClick
in first example:
const TestPage = () => {
const [postId, setPostId] = useState(null);
const handleClick = useCallback(() => {
async function getData() {
await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}/comments`
)
.then(response => response.json())
.then(data => console.log(data));
}
getData();
}, [postId]);
return (
<div>
<input onChange={e => setPostId(e.target.value)} />
<button onClick={handleClick}>test</button>
</div>
);
};
Upvotes: 1