2

Im trying to simulate breathing in a React Native app, the console log of the breathing in breathing out seems to work okay, however the Text never updates on the screen. it just sits on 'IN'

I'm new to react and struggling to find a solution. I tried using useState in stead of useRef for the current breathing status and it still does not update and the console does not work as expected when using useState.

useRef version

import React from 'react'
import { useState, useRef, useEffect } from 'react'
import { SafeAreaView } from 'react-native'
import { Layout, Text, Button } from '@ui-kitten/components'

const HomeScreen = () => {

    const [sessionStarted, setSessionStarted] = useState(false)
    const breathDoing = useRef('STOP');


    const doNext = () => {
        console.log('doNext')
        switch(breathDoing.current) {
            case 'IN':
                console.log('We are breathing in we should breath out')
                breathOut()
                break
            case 'OUT':
                console.log('We are breathing out we should breath in')
                breathIn()
                break
            case 'STOP':
                console.log('We should stop breathing, yikes')
                break
        }
    }
    
    const breathIn = () => {
        console.log('In breath in')
        breathDoing.current = 'IN'
        setTimeout(() => {
            doNext()
        }, 1000)
    }

    const breathOut = () => {
        console.log('In breath out')
        breathDoing.current = 'OUT'
        setTimeout(() => {
            doNext()
        }, 1000)
    }

    const breathSession = () => {
        console.log('breathSession');
        setSessionStarted(true)
        breathIn()
    }

    const stopBreathSession = () => {
        console.log('stopBreathSession');
        setSessionStarted(false)
        breathDoing.current = 'STOP'
    }

    const BreathOutput = (props) => {
        return <Text>{props.doing}</Text>
    }

    const handleSessionButton = () => {
        if (sessionStarted) {
            stopBreathSession()
        } else {
            breathSession()
        }
    }

    return (
        <>
            <SafeAreaView>
                <Layout level="1">
                    {sessionStarted ? 
                    <>
                       <BreathOutput doing={breathDoing.current} />
                    </>
                    : null}
                    <Button onPress={() => handleSessionButton()}>
                        {sessionStarted ? 'stop' : 'start'}
                    </Button>
                </Layout>
            </SafeAreaView>
        </>
    )
}

export default HomeScreen

the console shows correct output:

We are breathing in we should breath out
In breath out
doNext
We are breathing out we should breath in
In breath in
doNext
We are breathing in we should breath out
In breath out

EDIT

useState version:

    const [sessionStarted, setSessionStarted] = useState(false)
    const [breathDoing, setBreathDoing] = useState('STOP')
    
    const doNext = () => {
        console.log('doNext')
        switch(breathDoing) {
            case 'IN':
                console.log('We are breathing in we should breath out')
                breathOut()
                break
            case 'OUT':
                console.log('We are breathing out we should breath in')
                breathIn()
                break
            case 'STOP':
                console.log('We should stop breathing, yikes')
                break
        }
    }
    
    const breathIn = () => {
        console.log('In breath in')
        setBreathDoing('IN')
        setTimeout(() => {
            doNext()
        }, 1000)
    }

    const breathOut = () => {
        console.log('In breath out')
        setBreathDoing('OUT')
        setTimeout(() => {
            doNext()
        }, 1000)
    }

    const breathSession = () => {
        console.log('breathSession');
        setSessionStarted(true)
        breathIn()
    }

    const stopBreathSession = () => {
        console.log('stopBreathSession');
        setSessionStarted(false)
        setBreathDoing('STOP')
    }

    const BreathOutput = (props) => {
        return <Text>{props.doing}</Text>
    }

    const handleSessionButton = () => {
        if (sessionStarted) {
            stopBreathSession()
        } else {
            breathSession()
        }
    }

    return (
        <>
            <SafeAreaView>
                <Layout level="1">
                    {sessionStarted ? 
                    <>
                       <BreathOutput doing={breathDoing} />
                    </>
                    : null}
                    <Button onPress={() => handleSessionButton()}>
                        {sessionStarted ? 'stop' : 'start'}
                    </Button>
                </Layout>
            </SafeAreaView>
        </>
    ) 

The text stays as 'IN'

and the console outputs

breathSession
In breath in
doNext
We should stop breathing, yikes
squareborg
  • 1,562
  • 14
  • 18
  • whenever value of `breathDoing.current` changes, react does not re-render component so value will remain same. To fix this I would recommend to use `useState` instead of `useRef` – Priyank Kachhela May 02 '21 at 09:14
  • What is the output if you add `console.log(breathDoing);` before your return? Does it correctly pass the value to BreathOutput? – MaartenDev May 02 '21 at 11:57

2 Answers2

1

The setTimeout calls cache the state values, because of this the values never get updated. An alternative approach would be to listen for state changes using the useEffect hook:

export default function App() {
  const [sessionStarted, setSessionStarted] = useState(false);
  const [breathDoing, setBreathDoing] = useState("STOP");

  const getLabel = (status) => {
    switch (status) {
      case "IN":
        return "We are breathing in we should breath out";
      case "OUT":
        return "We are breathing out we should breath in";
      case "STOP":
        return "We should stop breathing, yikes";
    }
  };

  useEffect(() => {
    const stateTransitions = {
      STOP: "IN",
      IN: "OUT",
      OUT: "IN"
    };

    if (sessionStarted) {
      const nextState = stateTransitions[breathDoing];

      setTimeout(() => {
        setBreathDoing(nextState);
      }, 1000);
    }
  }, [sessionStarted, breathDoing]);

  const breathSession = () => {
    console.log("breathSession");
    setBreathDoing("IN");
    setSessionStarted(true);
  };

  const stopBreathSession = () => {
    console.log("stopBreathSession");
    setSessionStarted(false);
    setBreathDoing("STOP");
  };

  const BreathOutput = (props) => {
    return <div>{props.doing}</div>;
  };

  const handleSessionButton = () => {
    if (sessionStarted) {
      stopBreathSession();
    } else {
      breathSession();
    }
  };

  return (
    <>
      <div>
        <div level="1">
          {sessionStarted ? (
            <>
              {getLabel(breathDoing)}
              <BreathOutput doing={breathDoing} />
            </>
          ) : null}
          <button onClick={() => handleSessionButton()}>
            {sessionStarted ? "stop" : "start"}
          </button>
        </div>
      </div>
    </>
  );
}

Background information: React - useState - why setTimeout function does not have latest state value?

MaartenDev
  • 5,631
  • 5
  • 21
  • 33
  • Hey thanks this works. Intrestingly it doesnt work on ios simulator, it pauses and waits for me to click anywhere on the screen to progress the state. however I then tried it on the iphone and it worked. – squareborg May 03 '21 at 12:58
  • Great to hear! You may have to tweak the transition times but I think this should work as a starting point. – MaartenDev May 03 '21 at 13:08
0

You should use useState() instead of useRef(). To init the breathingState you can use a statement like

const [breathingState, setBreathingState] = useState('STOP');

Then, from your functions breathIn() or breathOut(), update the breathingState like this:

setBreathingState('IN');

or

setBreathingState('OUT')

respectively.

In the JSX, just refer to the breathingState as follows:

<BreathOutput doing={breathingState} />

React should then realize that there needs to be performed a re-render.

  • Thanks, this was my original useState try at this and it also didnt work. I think its something to do with the setTimeout closure and it saving the old state instead of accessing new state, I added an edit with the original try – squareborg May 02 '21 at 11:14