0

I'm very new to react so apologies if this question has already been answered or should be phrased differently. I have a functional component that fetches a .json file from my public folder in an async function (loadData()). Using the developer tools in my chrome window, I can see that the function gets me exactly what I want, yet the state doesn't seem to want to update when I use setData.

Edit: So I think I know what the problem is, which is that the first time the component renders, the variable source needs that JSON object which won't be there until the component re-renders. If that's the case, should all the code starting at let source = pickRandomVerb(data) go somewhere outside useEffect()?

function ComposedTextField() {

    const classes = useStyles();
    const [data, setData] = React.useState([]);
    const [displayVerb, setDisplayVerb] = React.useState('');

    const pickRandomVerb = (list) => {
        var obj_keys = Object.keys(list);
        var ran_key = obj_keys[Math.floor(Math.random() * obj_keys.length)];
        return list[ran_key];
    }

    const loadData = async() => {
        const response = await fetch('verbs.json');
        const json = await response.json();
        setData(json);
        console.log(json); //json is exactly what I want here
        console.log(data); //data is just '[]' here
    }

    useEffect(() => {

        loadData();
        console.log(data) //data is also '[]' here

        let source = pickRandomVerb(data)
        let verbSource = source.infinitive;
        let showVerb = verbSource.toString().replaceAll("\"", "");
        setDisplayVerb(showVerb)
    
    }, [])


    return(
        <div>
            <Typography className = {classes.form}>
                <p>{displayVerb}</p>
            </Typography>
        </div>
    )
}

Can anyone let me know what I'm missing? Thanks in advance.

  • Could it be the fact that you set an empty array as the 2nd arg of useEffect? That means it will only run once, if you remove that does it update? You can add `data` as an arg inside the array if it does so it runs if that state var updates. – Jon B Jan 04 '21 at 05:50
  • Does this answer your question? [useState set method not reflecting change immediately](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately) – Sinan Yaman Jan 04 '21 at 06:13
  • @SinanYaman If I understand that link correctly, if I get the component to re-render somehow the data should show up and be available for use? – annoyed_avocado Jan 04 '21 at 06:41
  • Your loadData is an async function, so the only change you need to add, is to await for it's execution in useEffect – Ivan Satsiuk Jan 04 '21 at 09:03

4 Answers4

0

setState is async also then you can't see the data changed in

    const loadData = async() => {
        const response = await fetch('verbs.json');
        const json = await response.json();
        setData(json);
        console.log(json); //json is exactly what I want here
        console.log(data); //data is just '[]' here
    }

Your useEffect doesn't define the dependency then it just run once. If you want to see your data changed just add dependency for data to track when data changed.

   /// Just load at the first time you render you component
   useEffect(()=>{
       loadData();
    },[])

    useEffect(() => {

        console.log(data)

        let source = pickRandomVerb(data)
        let verbSource = source.infinitive;
        let showVerb = verbSource.toString().replaceAll("\"", "");
        setDisplayVerb(showVerb)
    
    }, [data]) //Just add dependency here for useEffect
Nghi Nguyen
  • 910
  • 6
  • 10
  • If I do that, then the component will keep on fetching and re-rendering in an infinite loop. – annoyed_avocado Jan 04 '21 at 08:41
  • this is incorrect answer, you cannot add the `data` object into dependency list, because that is the function that's changing the value of the `data`. this is why it goes to an infinite loop – Ivan Satsiuk Jan 04 '21 at 09:04
  • Opps sorry I didn't recognize you put the loadData inside that. I just updated it hope it's good for you – Nghi Nguyen Jan 04 '21 at 13:32
0

useState hook is also asynchronous, and will not be reflected immediately and this question already addressed link-1, link-2

changes required (you have to listen the changes of the data in useEffect),

useEffect(() => {
   loadData();
    console.log(data) //data is also '[]' here
}, []);

useEffect(() => {
        let source = pickRandomVerb(data)
        let verbSource = source.infinitive;
        let showVerb = verbSource.toString().replaceAll("\"", "");
        setDisplayVerb(showVerb)
}, [data]);
Sunil Kumar
  • 420
  • 4
  • 13
0

Maybe you can try something like this

function ComposedTextField() {

    const classes = useStyles();
    const [data, setData] = React.useState([]);
    const [displayVerb, setDisplayVerb] = React.useState('');

    const pickRandomVerb = (list) => {
        var obj_keys = Object.keys(list);
        var ran_key = obj_keys[Math.floor(Math.random() * obj_keys.length)];
        return list[ran_key];
    }

    const loadData = async() => {
        const response = await fetch('verbs.json');
        const json = await response.json();
        setData(json);
        console.log(json); //json is exactly what I want here
        console.log(data); //data is just '[]' here
    }

    useEffect(() => {

        loadData();
        console.log(data) //data is also '[]' here
    
    }, [])

    // Move that code outside useEffect and inside condition
    if(data!==[]){
        let source = pickRandomVerb(data)
        let verbSource = source.infinitive;
        let showVerb = verbSource.toString().replaceAll("\"", "");
        setDisplayVerb(showVerb)
    }

    return(
        <div>
            <Typography className = {classes.form}>
                <p>{displayVerb}</p>
            </Typography>
        </div>
    )
}

In this code setDisplayVerb() will be called when the data has some value. The flow of the control would be something like this.

  1. Component is mounted with data=[]
  2. usEffect is called which in turns calls loadData()
  3. loadData will set the value of data using setData(json)
  4. When data is set it will cause re-render, since data has value this time, the if condition is satisfied and the statements inside it will be executed.
  5. setDisplayVerb() will cause re-render of the application.
  6. Since the dependency array of useEffect is empty it will not be called again.

This way you should be able to see the data on your screen.

Hardik3296
  • 336
  • 2
  • 14
0

so to expand on my comment, try this:

    const loadData = async() => {
        const response = await fetch('verbs.json');
        const json = await response.json();
        return json; /// <<< note: here you return data
    }

    useEffect(async () => {
        const data = await loadData(); // <<<< note: here you wait for data
        console.log(data);

        let source = pickRandomVerb(data)
        let verbSource = source.infinitive;
        let showVerb = verbSource.toString().replaceAll("\"", "");
        setDisplayVerb(showVerb)
    
    }, [])

this will only get you the data on initial load.

Just as a side note: to improve a bit on this component, you can move your loadData function outside of the component.

Ivan Satsiuk
  • 333
  • 2
  • 9
  • 1
    Thanks this actually works, although eslint throws a warning about how 'Effect callbacks are synchronous to prevent race conditions.' I found a workaround called useAsyncEffect here: https://www.npmjs.com/package/use-async-effect – annoyed_avocado Jan 04 '21 at 19:51