Aaron Etheridge
Aaron Etheridge

Reputation: 93

SetState Not Updating to fetch dynmically

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

Answers (1)

macborowy
macborowy

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

Related Questions