2

In my test app, every time a certain function is called, a call to my API is made (axios.get) and then two state variables are updated with the data received from the database. These two state variables both change a part of what is shown on the screen.

The things is, I added a useEffect hook to "debug" the amount of re-renders and I noticed that the component is re-rendered twice, I guess because it is once for one state variable and once for the other one. I thought using useReducer would change this, but it doesn't.

Is this a normal React behaviour or is there something I should be doing differently in order for the component is re-rendered only once?

Edit: I am editing to add the code: (It's a trivia kind of test app, I'm new to React, so I am practicing)

import React, { useEffect, useReducer } from 'react'
import axios from 'axios'
import './App.css';
import reducer from './Reducer.js'

const initialState = {
    question: '',
    id: 1,
    choices: []
}

const Questions = () => {

    const [state, dispatch] = useReducer(reducer, initialState)

    useEffect(() => {
        console.log('executed');
    })


    const getQuestion = async (e) => {

        try {
            e.preventDefault()
            
            const res = await axios.get(`/questions/${state.id}`)
            
            dispatch({
                type: 'set_question',
                payload:
                    res.data.question
            })
            dispatch({
                type: 'set_choices',
                payload:
                    [res.data.correct_answer,
                    res.data.incorrect_answer1,
                    res.data.incorrect_answer2]
            })

        } catch (err) {
            console.log(err);
        }
    }


    return (
        <div>

            <form onSubmit={getQuestion}>
                <button>Get next question</button>
            </form>

            <h1> {state.question ? state.question : null}</h1>
            <button> {state.choices ? `${state.choices[0]}` : null}</button>
            <button> {state.choices ? ` ${state.choices[1]}` : null}</button>
            <button> {state.choices ? ` ${state.choices[2]}` : null}</button>
        </div>
    )
}

export default Questions

Reducer:

const reducer = (state, action) => {

    switch (action.type) {

        case 'set_question':
            return {
                ...state,
                question: action.payload
            }

        case 'set_choices':
            return {
                ...state,
                choices: action.payload
            }

        default:
            return state

    }
}

export default reducer
Paulprg19
  • 573
  • 1
  • 5
  • 14

1 Answers1

2

React only batches state updates in event handlers and lifecycle methods. If the state updates happen in an async function e.g. in response of a successful call to fetch or a setTimeout they will not be batched. This is announced to change in a future version of react.

Also see this answer from Dan Abramov about this: https://stackoverflow.com/a/48610973/5005177

However, both in React 16 and earlier versions, there is yet no batching by default outside of React event handlers. So if in your example we had an AJAX response handler instead of handleClick, each setState() would be processed immediately as it happens. In this case, yes, you would see an intermediate state.

promise.then(() => {
    // We're not in an event handler, so these are flushed separately.
    this.setState({a: true}); // Re-renders with {a: true, b: false }
    this.setState({b: true}); // Re-renders with {a: true, b: true }
    this.props.setParentState(); // Re-renders the parent
});

If you want your component to re-render only once you have to keep all the data in a single state object so that you only have to call setState once (or dispatch if you want to use useReducer) with the new data.

Another workaround is to wrap your block of state updates in ReactDOM.unstable_batchedUpdates(() => {...}), which will most likely not be required anymore in a future version of react. Also see the answer of Dan Abramov from above for details.

trixn
  • 15,761
  • 2
  • 38
  • 55
  • Oh, maybe I should have mentioned that the function I am talking about is triggered by a button click... – Paulprg19 Jun 22 '20 at 13:38
  • @Paulprg19 You wrote: "In my test app, every time a certain function is called, **a call to my API is made** (axios.get) and then two state variables are **updated with the data received from the database**." So if your state updates happen in the `.then()` handler of a promise or are in an async function they are **not** called in context of the button click anymore and it applies what I described in my answer. – trixn Jun 22 '20 at 15:48
  • @Paulprg19 And if you say that your state gets updated with the result of your API call it must be inside of the promise `.then()` handler as API calls are async. It doesn't matter that you trigger the API call itself in the click handler because the response is being handled async. – trixn Jun 22 '20 at 16:00
  • I added the code to make it clearer. Actually, it's not like I WANT the component to re-render only once. The point of this post is more like "is it ok that the component is re-rendering more than once or am I doing something wrong?" – Paulprg19 Jun 22 '20 at 18:38
  • @Paulprg19 You aren't doing something wrong and this is the expected behaviour. If you don't do any heavy lifting during rendering the affected components it should be fine. Also in future versions of react batching state updates might also cover async use cases which will resolve this issue entirely. – trixn Jun 23 '20 at 06:36