简体   繁体   中英

What is the proper way to set state with React Hooks?

I recently asked a Stack Overflow post about useState() taking 2 clicks. I was told I could immediately update a boolean state by doing setState(state => !state) . However, this only works for booleans. Take this example:

  let [popularQuotes, setPopularQuotes] = useState([])
  let [newQuotes, setNewQuotes] = useState([])

  let [selected, select] = useState("popular")

  const getQuotes = async() => {
    Post('https://example.com/api/quotes', {
      sort: selected
    }).then((r) => selected == "popular" ? setPopularQuotes(r) : setNewQuotes(r))
  }

When I want to toggle from popular quotes to new quotes, such as like this onClick={() => {select("popular"); getQuotes()}} onClick={() => {select("popular"); getQuotes()}} , it takes 2 clicks, as the state newQuotes remains initially unchanged for the first click (just being an empty array). How can I counteract this and update the state on the first click?

Add an effect hook like this to your function component body for triggering the Api whenever selected is changed:

useEffect(() => {
    getQuotes(selected);
},[selected]);

Change getQuotes to get selected value as a parameter:

const getQuotes = async(selectedItem) => {
  Post('https://example.com/api/quotes', {
    sort: selectedItem
  }).then((r) => selectedItem == "popular" ? setPopularQuotes(r) : setNewQuotes(r))
}

Finally modify onClick callback like this:

onClick={() => select("popular")}

You are running into the fact that setting state is async, therefore -

onClick={() => {select("popular"); getQuotes()}}

will not be calling getQuotes with the updated value of 'selected' being 'popular'

I suggest you either handle the currently required quotes in state, and let an effect take care of the API call, however be careful if your components aren't well structure and pure or you could end up with more API calls than want you wish for -

const [popularQuotes, setPopularQuotes] = useState([])
const [newQuotes, setNewQuotes] = useState([])

const [selected, select] = useState("popular")

const getQuotes = async(selected) => {
   Post('https://example.com/api/quotes', {
      sort: selected
   }).then((r) =>
      selected ===  "popular" ?
         setPopularQuotes(r) : setNewQuotes(r))
   )
}

useEffect(() => {   
   getQuotes(selected);
}, [getQuotes, selected])

...

or pass the quote type to getQuotes as an argument, and keep track of the previously requested quote type in state -

let [selected, select] = useState("popular")

...

onClick={() => {
   getQuotes(selected === 'popular' ? 'new' : 'popular');
   select("popular");
}}

though this gets hard to maintain as your state needs to clearly define if its storing the state of the next to call, or the current thats been displayed, and therefore the getQuotes needs to act on the opposite

As you mentioned in your question, you will need to update the state using a callback.

In the below example, i'm using the previous state value but it's not mandatory.

 const [someState, setSomeState] = useState({id: 'idA', name: 'someNameA'}) setSomeState(prevState => ({id: prevState.id + 'B', name: prevState.name + 'B'}));

More info can be found here: The useState set method is not reflecting a change immediately

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM