State Management in React Native — SitePoint

September 03, 2019 0 Comments

State Management in React Native — SitePoint

 

 

Managing state is one of the most difficult concepts to grasp while learning React Native, as there are so many ways to do it. There are countless state management libraries on the npm registry — such as Redux — and there are endless libraries built on top of other state management libraries to simplify the original library itself — like Redux Easy. Every week, a new state management library is introduced in React, but the base concepts of maintaining the application state has remained the same since the introduction of React.

The most common way to set state in React Native is by using React’s setState() method. We also have the Context API to avoid prop drilling and pass the state down many levels without passing it to individual children in the tree.

Recently, Hooks have emerged into React at v16.8.0, which is a new pattern to simplify use of state in React. React Native got it in v0.59.

In this tutorial, we’ll learn about what state actually is, and about the setState() method, the Context API and React Hooks. This is the foundation of setting state in React Native. All the libraries are made on top of the above base concepts. So once you know these concepts, understanding a library or creating your own state management library will be easy.

Want to learn React Native from the ground up? This article is an extract from our Premium library. Get an entire collection of React Native books covering fundamentals, projects, tips and tools & more with SitePoint Premium. Join now for just $9/month.

What Is a State?

Anything that changes over time is known as state. If we had a Counter app, the state would be the counter itself. If we had a to-do app, the list of to-dos would change over time, so this list would be the state. Even an input element is in a sense a state, as it over time as the user types into it.

Intro to setState

Now that we know what state is, let’s understand how React stores it.

Consider a simple counter app:

import React from 'react' import { Text, Button } from 'react-native' class Counter extends React.Component { state = { counter: 0 } render() { const { counter } = this.state return ( <> <Text>{counter}</Text> <Button onPress={() => {}} title="Increment" /> <Button onPress={() => {}} title="Decrement" /> </> ) } } 

In this app, we store our state inside the constructor in an object and assign it to this.state.

Remember, state can only be an object. You can’t directly store a number. That’s why we created a counter variable inside an object.

In the render method, we destructure the counter property from this.state and render it inside an h1. Note that currently it will only show a static value (0).

You can also write your state outside of the constructor as follows:

import React from 'react' import { Text, Button } from 'react-native' class Counter extends React.Component { state = { counter: 0 } render() { const { counter } = this.state return ( <> <Text>{counter}</Text> <Button onPress={() => {}} title="Increment" /> <Button onPress={() => {}} title="Decrement" /> </> ) } } 

Now let’s suppose we want the + and - button to work. We must write some code inside their respective onPress handlers:

import React from 'react' import { Text, Button } from 'react-native' class Counter extends React.Component { state = { counter: 0 } render() { const { counter } = this.state return ( <> <Text>{counter}</Text> <Button onPress={() => { this.setState({ counter: counter + 1 }) }} title="Increment" /> <Button onPress={() => { this.setState({ counter: counter - 1 }) }} title="Decrement" /> </> ) } } 

Now when we click the + and - buttons, React re-renders the component. This is because the setState() method was used.

The setState() method re-renders the part of the tree that has changed. In this case, it re-renders the h1.

So if we click on +, it increments the counter by 1. If we click on -, it decrements the counter by 1.

Remember that you can’t change the state directly by changing this.state; doing this.state = counter + 1 won’t work.

Also, state changes are asynchronous operations, which means if you read this.state immediately after calling this.setState, it won’t reflect recent changes.

This is where we use “function as a callback” syntax for setState(), as follows:

import React from 'react' import { Text, Button } from 'react-native' class Counter extends React.Component { state = { counter: 0 } render() { const { counter } = this.state return ( <> <Text>{counter}</Text> <Button onPress={() => { this.setState(prevState => ({ counter: prevState.counter + 1 })) }} title="Increment" /> <Button onPress={() => { this.setState(prevState => ({ counter: prevState.counter - 1 })) }} title="Decrement" /> </> ) } } 

The “function as a callback” syntax provides the recent state — in this case prevState — as a parameter to setState() method.

This way we get the recent changes to state.

What are Hooks?

Hooks are a new addition to React v16.8. Earlier, you could only use state by making a class component. You couldn’t use state in a functional component itself.

With the addition of Hooks, you can use state in functional component itself.

Let’s convert our above Counter class component to a Counter functional component and use React Hooks:

import React from 'react' import { Text, Button } from 'react-native' const Counter = () => { const [ counter, setCounter ] = React.useState(0) return ( <> <Text>{counter}</Text> <Button onPress={() => { setCounter(counter + 1 ) }} title="Increment" /> <Button onPress={() => { setCounter(counter - 1 ) }} title="Decrement" /> </> ) } 

Notice that we’ve reduced our Class component from 18 to just 12 lines of code. Also, the code is much easier to read.

Let’s review the above code. Firstly, we use React’s built-in useState method. useState can be of any type — like a number, a string, an array, a boolean, an object, or any type of data — unlike setState(), which can only have an object.

In our counter example, it takes a number and returns an array with two values.

The first value in the array is the current state value. So counter is 0 currently.

The second value in the array is the function that lets you update the state value.

In our onPress, we can then update counter using setCounter directly.

Thus our increment function becomes setCounter(counter + 1 ) and our decrement function becomes setCounter(counter - 1).

React has many built-in Hooks, like useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect and useDebugValue — which you can find more info about in the React Hooks docs.

Additionally, we can build our own Custom Hooks.

There are two rules to follow when building or using Hooks:

  1. Only Call Hooks at the Top Level. Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.


  2. Only Call Hooks from React Functions. Don’t call Hooks from regular JavaScript functions. Instead, you can either call Hooks from React functional components or call Hooks from custom Hooks.


By following this rule, you ensure that all stateful logic in a component is clearly visible from its source code.

Hooks are really simple to understand, and they’re helpful when adding state to a functional component.

The Context API

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

In a typical React Native application, data is passed top-down via props. If there are multiple levels of components in the React application, and the last child in the component tree wants to retrieve data from the top-most parent, then you’d have to pass props down individually.

Consider an example below. We want to pass the value of theme from the App component to the Pic component. Typically, without using Context, we’ll pass it through every intermediate level as follows:

const App = () => ( <> <Home theme="dark" /> <Settings /> </> ) const Home = () => ( <> <Profile /> <Timeline /> </> ) const Profile = () => ( <> <Pic theme={theme} /> <ChangePassword /> </> ) 

The value of theme goes from App -> Home -> Profile -> Pic. The above problem is known as prop-drilling.

This is a trivial example, but consider a real-world application where there are tens of different levels.

It becomes hard to pass data through every child just so it can be used in the last child. Therefore, we have Context.

Context allows us to directly pass data from App -> Pic.

Here’s how to do it using the Context API:

import React from 'react' const ThemeContext = React.createContext("light") // set light as default const App = () => ( <ThemeContext.Provider value="dark"> <Home /> <Settings /> </ThemeContext.Provider> ) const Home = () => ( <> <Profile /> <Timeline /> </> ) const Profile = () => ( <ThemeContext.Consumer> {theme => ( <Pic theme={theme} /> <ChangePassword /> )} </ThemeContext.Consumer> ) 

Firstly, we create ThemeContext using the React.createContext API. We set light as the default value.

Then we wrap our App component’s root element with ThemeContext.Provider, while providing theme as a prop.

Lastly, we use ThemeContext.Consumer as a render prop to get the theme value as dark.

The render prop pattern is nice, but if we have multiple contexts then it might result in callback hell. To save ourselves from callback hell, we can use Hooks instead of ThemeContext.Consumer.

The only thing we need to change is the Profile component’s implementation detail:

const Profile = () => { const theme = React.useContext(ThemeContext) return (<> <Pic theme={theme} /> <ChangePassword /> </> ) } 

This way, we don’t have to worry about callback hell.

Sharing State across Components

Until now, we only managed state in the component itself. Now we’ll look at how to manage state across components.

Suppose we’re creating a simple to-do list app as follows:

import { View, Text } from 'react-native' const App = () => ( <> <AddTodo /> <TodoList /> </> ) const TodoList = ({ todos }) => ( <View> {todos.map(todo => ( <Text> {todo} </Text>) )} </View> ) 

Now, if we want to add a to-do from the AddTodo component, how will it show up in the TodoList component’s todos prop? The answer is “lifting state up”.

If two sibling components want to share state, then the state must be lifted up to the parent component. The completed example should look like this:

import { View, Text, TextInput, Button } from 'react-native' const App = () => { const [ todos, setTodos ] = React.useState([]) return ( <> <AddTodo addTodo={todo => setTodos([...todos, todo])} /> <TodoList todos={todos} /> </> ) } const AddTodo = ({ addTodo }) => { const [ todo, setTodo ] = React.useState('') return ( <> <TextInput value={todo} onChangeText={value => setTodo(value)} /> <Button title="Add Todo" onPress={() => { addTodo(todo) setTodo('') }} /> </> ) } const TodoList = ({ todos }) => ( <View> {todos.map(todo => ( <Text> {todo} </Text>) )} </View> ) 

Here, we keep the state in the App component. We use the React Hook useState to store todos as an empty array.

We then pass the addTodo method to the AddTodo component and the todos array to the TodoList component.

The AddTodo component takes in the addTodo method as a prop. This method should be called once the button is pressed.

We also have an TextInput element which also uses the React Hook useState to keep track of the changing value of TextInput.

Once the Button is pressed, we call the addTodo method, which is passed from the parent App. This makes sure the todo is added to the list of todos. And later we empty the TextInput box.

The TodoList component takes in todos and renders a list of todo items given to it.

You can also try deleting a to-do to practice lifting state up yourself. Here’s the solution:

const App = () => { const [ todos, setTodos ] = React.useState([]) return ( <> <AddTodo addTodo={todo => setTodos([...todos, todo])} /> <TodoList todos={todos} deleteTodo={todo => setTodos(todos.filter(t => t !== todo))} /> </> ) } const TodoList = ({ todos, deleteTodo }) => ( <View> {todos.map(todo => ( <Text> {todo} <Button title="x" onPress={() => deleteTodo(todo)} /> </Text>) )} </View> ) 

This is the most common practice in React. Lifting states up is not as simple as it seems. This is an easy example, but in a real-world application we don’t know which state will be needed to lift up to its parent to be used in a sibling component. So at first, keep state in the component itself, and when a situation arises to have to share state between components then only lift state up to the parent.

This way you don’t make your parent component a big state object.

Conclusion

To sum up, we looked at what state is and how to set the value of state using the setState() API provided by React. We also looked at React Hooks, which make it easy to add state to a functional component without having to convert it to a class component.

We learned about the new Context API and its Hooks version useContext, which helps us to stay away from render prop callback hell.

Finally, we learned about lifting state up to share state between sibling components.

React becomes very simple once you understand these core concepts. Remember to keep state as local to the component as possible. Use the Context API only when prop drilling becomes a problem. Lift state up only when you need to.

Finally, check out state management libraries like Redux and MobX once your application gets complex and it’s hard to debug state changes.


Tag cloud