lane
lane

Reputation: 679

React: how can I force state to update in a functional component?

This function component has a template method that calls onChangeHandler, which accepts a select value and updates state. The problem is, state does not update until after the render method is called a second time, which means the value of selected option is one step ahead of the state value of selectedRouteName.

I know there are lifecycle methods in class components that I could use to force a state update, but I would like to keep this a function component, if possible.

As noted in the code, the logged state of selectedRouteDirection is one value behind the selected option. How can I force the state to update to the correct value in a functional component?

This question is not the same as similarly named question because my question asks about the actual implementation in my use case, not whether it is possible.

import React, { useState, Fragment, useEffect } from 'react';

const parser = require('xml-js');

const RouteSelect = props => {
    const { routes } = props;

    const [selectedRouteName, setRouteName] = useState('');
    const [selectedRouteDirection, setRouteDirection] = useState('');

    //console.log(routes);

    const onChangeHandler = event => {

        setRouteName({ name: event.target.value });

        if(selectedRouteName.name) {
            getRouteDirection();
        }
    }
/*
    useEffect(() => {
        if(selectedRouteName) {
            getRouteDirection();
        }
    }); */

    const getRouteDirection = () => {

        const filteredRoute = routes.filter(route => route.Description._text === selectedRouteName.name);
        const num = filteredRoute[0].Route._text;

        let directions = [];
        fetch(`https://svc.metrotransit.org/NexTrip/Directions/${num}`)
            .then(response => { 
                return response.text();
            }).then(response => {
                return JSON.parse(parser.xml2json(response, {compact: true, spaces: 4}));
            }).then(response => {
                directions = response.ArrayOfTextValuePair.TextValuePair;
                // console.log(directions);
                setRouteDirection(directions);    
            })
            .catch(error => {
                console.log(error);
            });

            console.log(selectedRouteDirection); // This logged state is one value behind the selected option
    }

    const routeOptions = routes.map(route => <option key={route.Route._text}>{route.Description._text}</option>);

    return (
        <Fragment>
            <select onChange={onChangeHandler}>
                {routeOptions}
            </select>
        </Fragment>
    );
};

export default RouteSelect;

Upvotes: 3

Views: 4315

Answers (2)

jcal
jcal

Reputation: 875

Well, actually.. even though I still think effects are the right way to go.. your console.log is in the wrong place.. fetch is asynchronous and your console.log is right after the fetch instruction.

As @Bernardo states.. setState is also asynchronous so at the time when your calling getRouteDirection();, selectedRouteName might still have the previous state.

So to make getRouteDirection(); trigger after the state was set. You can use the effect and pass selectedRouteName as second parameter (Which is actually an optimization, so the effect only triggers if selectedRouteName has changed)

So this should do the trick:

useEffect(() => {
  getRouteDirection();
}, [selectedRouteName]);

But tbh.. if you can provide a Stackblitz or similar, where you can reproduce the problem. We can definitely help you better.

Upvotes: 1

Bernardo
Bernardo

Reputation: 71

setState is asynchronous! Many times React will look like it changes the state of your component in a synchronous way, but is not that way.

Upvotes: 0

Related Questions