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.
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?
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
:
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:
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:
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
andbackground
, with a temporary state ofinactive
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
:
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:
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
withuseRef
, and use that inside event listeners instead of usingcounter
directly.Defining a custom
setCounter
method that will update the ref’scurrent
value as well as thecounter
state value. To do this, we can change the name of useState’ssetCounter
to_setCounter
, and use this inside our customsetCounter
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 theref
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:
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:
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 />
:
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