Manging Component State and Props

Props

Props are short for Properties. The simple rule of thumb is props should not be changed. In the programming world we call it “Immutable” or in simple english “Unchangeable”.

Props are Unchangeable — Immutable

Components receive props from their parent. These props should not be modified inside the component. In React and React Native the data flows in one direction -> From the parent to the child.

You can write your own components that use props. The idea behind props is that you can make a single component that is used in many different places in your app. The parent that is calling the component can set the properties, which could be different in each place.

Props essentially help you write reusable code.

export default class ScreenOne extends React.Component {
  render () {
    return (
     <View>
     	 <Heading message={'Custom Heading for Screen One'}/>
     </View>
    )
  }
}

// Child component
export default class Heading extends React.Component {
  render () {
    return (
      <View>
        <Text>{this.props.message}</Text>
      </View>
    )
  }
}
Heading.propTypes = {
  message: PropTypes.string
}
Heading.defaultProps = {
  message: 'Heading One'
}

This simple example shows how props are used.

In the example above we have a Heading component, with a message prop. The parent class ScreenOne sends the prop to the child component Heading.

Notice that the same component Heading can be reused several times with different message prop values passed to it from different parents components. The key here is to remember that the prop should not be modified inside the Heading component.

You can create as many screens as you would like to include the same Heading component with different message props.

State

State works differently when compared to props. State is internal to a component, while props are passed to a component.

State can Change — Mutable

In english the ‘state of a being’ refers to the physical condition of a person, and it is a mere state, which changes over time. Well, similarly state in React/React Native is used within components to keep track of information.

Keep in mind not to update state directly using this.state. Always use setState to update the state objects. Using setState re-renders the component and all the child components. This is great, because you don’t have to worry about writing event handlers like other languages.

So when can state be used?

class Form extends React.Component {

  constructor (props) {
     super(props)
     this.state = {
       input: ''
     }
  }

handleChangeInput = (text) => {
    this.setState({ input: text })
  }
  
  render () {
    const { input } = this.state

    return (
       <View>
          <TextInput style={{height: 40, borderColor: 'gray', borderWidth: 1}}
            onChangeText={this.handleChangeInput}
            value={input}
          />
        </View>
      )
    }
 }

Anytime there is data that is going to change within a component, state can be used.

User interaction with components are good examples of how state works. Clicking buttons, checkboxes, filling forms, etc. are examples of user interaction where state can be used within the component.

If you had to fill a form with text inputs, each field in the form will retain it’s state based on the user input. If the user input changes, the state of the text inputs will change, causing a re-rendering of the component and all of it’s child components.

Take a look at the code snippet below to better understand how states works within a form.

In the above code snippet you can see a Form class with an input state. It renders a text input which accepts the user’s input. Once the user inputs the text, the onChangeText is triggered which in turn calls setState on input.

The setState triggers a re-rendering of the component again, and the UI is now updated with the user’s latest input. This simple example illustrates how state within a component can be updated and its usage.

How to Use App State -- (from React Native)

AppState is a simple API supplied by the react-native framework, so is most likely readily available in your React Native projects now. In it’s most basic usage, we can simply refer to the current App State using its currentState property, that will either be active, inactive or background:

// get current app state from `AppState`import React, { useState } from 'react'
import { AppState } from 'react-native'const App = (props) => {
  
  const [appState, setAppState] = useState(AppState.currentState);
  ...
}

In the above example, the App component will store the current state of the app as it is rendered — which will almost certainly be active.

This alone is not too useful — the app needs to know when this state changes, which in turn needs to be reflected in the above useState hook. To tackle this, event listeners can be attached to AppState, that gives the component the opportunity to update with the underlying value:

// `AppState` event listeners within `useEffect`const handleAppStateChange = (state: any) => {
  console.log(state);
}useEffect(() => {
  AppState.addEventListener('change', handleAppStateChange);
  return (() => {
    AppState.removeEventListener('change', handleAppStateChange);
  })
}, []);

A useEffect hook has been introduced here with an empty dependency array, ensuring the event listeners will only mount upon the component’s initial render. useEffect’s return function is executed when the component unmounts, giving the component an opportunity to remove the event listener.

Let’s make this slightly more intelligent by updating the AppState within handleAppStateChange, and use another useEffect hook to console.log that value upon the subsequent re-render:

// listening to `AppState` changesconst [appState, setAppState] = useState(AppState.currentState);const handleAppStateChange = (state: any) => {
  setAppState(state);
}useEffect(() => {
  AppState.addEventListener('change', handleAppStateChange);
  return (() => {
    AppState.removeEventListener('change', handleAppStateChange);
  })
}, []);useEffect(() => {
  console.log(appState);
});

With this simple setup, we already have the means to handle updates to the App State from within a component. However, there are indeed some limitations to our event listeners in this setup, as they will only ever be aware of the component state at the initial render. We will explore why this is the case further down the article.

But when do these “changes” actually occur? Let’s examine this next in order to understand exactly when these events are firing.

When AppState changes happen

The three values of AppState (active, inactive and background) are toggled between two key events:

  • Minimising and opening the app, to and from the Home screen. Upon doing so, the app switches between active and background, with a temporary state of inactive as the app is being minimised.

  • Entering the app switcher from the app itself. If this is done from within the app, the app state will persistently change to inactive until the user leaves app switcher.

To demonstrate this, I have copied the example code from above into the Dashboard screen of an app I personally develop. Notice the changes between App State as I change from foreground to background, and as I enter the app switcher: Demonstrating AppState changes as the app changes from foreground, background and app-switcher

What you may have noticed is that we always have a period of inactive when minimising the app to the device’s Home screen, before changing again to background. In addition, this inactive state is only triggered from within the app. If you attempt to go into the app switcher while on the device’s Home screen, the app will remain in the background state, until it is opened to the foreground again.

Notice how some apps blur their screens in app switcher?

It is in the temporary inactive period that you can make some interesting changes to the app, such as overriding the current screen with a placeholder screen, in the event that the current screen contains confidential information — such as a banking app or FinTech app.

This can be handled simply be rendering a different screen if AppState.currentState is inactive. If you want this behaviour globally, you could wrap your entire app around a component, say, an <AppStateManager> component, that will re-render the app from the top level when the state changes to inactive:

// render `inactive` screen via top-level `AppStateManager` componentexport const AppStateManager = (props: any) => {  const [appState, setAppState] = useState(AppState.currentState);  const handleAppStateChange = (state: any) => {
    setAppState(state);
  }  useEffect(() => {
    AppState.addEventListener('change', handleAppStateChange);
    return (() => {
      AppState.removeEventListener('change', handleAppStateChange);
    })
  }, []);  return (
    {appState === 'inactive'
     ? <View><Text>Inactive Screen!</Text><View>
     : <>{props.children}</>
     }
  )
}

This adds a certain level of security within React Native apps — an arguably compulsory feature for more sensitive applications.

With a high level understanding of AppState, let’s now examine how to overcome the limitation of event listeners, that only read component state from the render the event listeners are initialised. This can be solved with Refs.

Using Refs with Event Listeners to Access True State Values

As mentioned above, event listeners will only be aware of the state of the component at the initial render. Because the event listeners are not updated upon subsequent re-renders (when state changes), they will not be aware of those changes taking place.

To see this problem in action, we can increment a counter that will exist in useState, and log that counter within an event listener as it is being incremented. As the event listener is not aware of state updates after it is initialised, the counter will always log zero.

The following snippet sets this demo up with an event listener added to react-navigation’s didFocus event.

For testing purposes, React Navigation’s didFocus and didBlur events are really useful for testing component logic, that are triggered as screens are visited and left.

This event is initialised as the screen in question is visited for the first time — it is the state at this point that will be logged:

const [counter, setCounter] = useState(0);// update state every 2 secondssetInterval(() => {
  setCounter(counter + 1);
}, 2000);// console.log `counter` within event listener every 2 secondsuseEffect(() => {
  this.focusListener = props.navigation.addListener('didFocus', async () => {
    setInterval(() => {
      // this will always be 0
      console.log(counter);
    }, 2000);
  });
  
  return (() => {
    this.focusListener.remove();
  })
}, []);

Concretely, the event listener will not have access to updated state values. This is an inherent issue to React’s relationship to event listeners in general, and is not just related to AppState.

To overcome this, we can use the useRef hook, as well as React.createRef(), to access real-time state values (from the most recent update), from useState or from DOM elements.

Firstly visiting useRef, we can give event listeners a true state value by making a couple of small changes from the above code:

  • Creating a reference to counter with useRef, and use that inside event listeners instead of using counter directly.

  • Defining a custom setCounter method that will update the ref’s current value as well as the counter state value. To do this, we can change the name of useState’s setCounter to _setCounter, and use this inside our custom setCounter method.

That may be hard to visualise — here is the updated counter example with useRef integrated:

With these changes made, the current state values can now be accessed from within event listeners — event listeners that were initialised on the initial component render.

This is a necessary workaround when it comes to AppState, allowing you to refer to current state values when determining your app state switching logic, where you may need to access local state or updated global state from a Redux store or similar.

What about getting current HTML / JSX element state within event listeners?

In the above example, useState values were referenced with useRef. But what if we wanted to fetch attributes of rendered elements, such as form elements, or even state from React Native components likeScrollView, where the event listener may need to know the current scroll position. A slightly different approach is needed here.

Let’s take this Scroll View scenario. We can take the following steps to solve this:

  • Create a ref to the Scroll View element with React.createRef. This will act as a pointer to the element.

  • Wrap the above ref with a useRef hook, and use this reference within event listeners, and within the ref prop of <ScrollView />.

In this scenario we are wrapping a ref with a useRef hook — that may appear confusing, but highlights that the two implementations act differently. Let’s drill down why both APIs are being used.

The first difference is the syntax itself:

const scrollview = React.createRef();
const scrollviewRef = React.useRef(scrollview);

Now, if we ignored scrollviewRef and simply assigned scrollview to the <ScrollView /> ref prop, and tried to refer to this value within event listeners, we would get a value of null.

Try this yourself with the following snippet:

// INCORRECT: attempting to assign `scrollview` as ref and use within event listenerconst scrollview = React.createRef();useEffect(() => {
  this.focusListener = props.navigation.addListener('didFocus', async () => {
    // this will be `null`
    console.log(scrollview.current);
  });
  return (() => {
    this.focusListener.remove();
  })
}, []);
return (
 <ScrollView
    ref={scrollview}
    ...
  />
);

The issue here is due to the same reason as the previous counter example — at the time the event listeners are being initialised, scrollview.current is still null, and is yet to be linked to the <ScrollView /> component.

What we can apply here is the same useRef solution, and use that reference as the ref prop of <ScrollView />:

// CORRECT: accessing `ScrollView` ref within event listenerconst scrollview = React.createRef();
const scrollviewRef: any = React.useRef(scrollview);useEffect(() => {
  this.focusListener = props.navigation.addListener('didFocus', async () => {
    // this will now successfully reference <ScrollView />
    console.log(scrollviewRef.current);
  });
  return (() => {
    this.focusListener.remove();
  })
}, []);return (
 <ScrollView
    ref={scrollviewRef}
    ...
  />
);

Now our event listeners will successfully reference JSX elements, using useRefs ability to persist its reference object for the majority of the component’s lifetime.

Perhaps a clearer way to think about this solution is that scrollview acts as the pointer to <ScrollView /> on the DOM level, whereas scrollviewRef acts as a pointer on the component level.

Last updated