Skip to main content

React Native Context & Hooks

Victoria Harbour, Hongkong

Github Repository

Setup

npm install -g expo-cli
expo init react-native-context-hooks

Choose the blank template and run the client:

cd react-native-context-hooks
npm install styled-components
npm start

I can test code by scanning the QR Code using the Expo Go app under Android or by pressing the w key to activate a web preview.

error:0308010C:digital envelope routines::unsupported: Newer versions of Node.js use OpenSSLv3 that brought some breaking changes. Can be fixed (>=Nodejs v17) by adding the following option to the start script inside your package.json -> "start": "export NODE_OPTIONS=--openssl-legacy-provider && expo start"

Context Consumer vs useContext Hooks

In a previous step I created an React Native application that used the React Context API using Context Consumers in Class components. Thanks to the useContext hook we can now simplify those components.

Before (Class Component)

import React from 'react'
import {Text} from 'react-native'

import { NavBackground, NavHeader, NavTabs, NavTabsHeader, ThemeToggle } from './_styles'
import { ThemeContext } from '../context/ThemeContext'

class Navbar extends React.Component {
render() {

return (
<ThemeContext.Consumer >
{(context) => {
const { isDarkTheme, darkTheme, lightTheme, changeTheme } = context
// if isDarkTheme is true return dark state / else light
const theme = isDarkTheme ? darkTheme : lightTheme
return (
<NavBackground style={theme}>
<NavHeader style={theme}>Company Inc.</NavHeader>
<NavTabs>
<NavTabsHeader>Home</NavTabsHeader>
<NavTabsHeader>Contact</NavTabsHeader>
<ThemeToggle onPress={changeTheme}>
<Text>{theme.text}</Text>
</ThemeToggle>
</NavTabs>
</NavBackground>
)
}}
</ThemeContext.Consumer>
)
}
}

export default Navbar

After (Functional Component)

import React, {useContext} from 'react'
import {Text} from 'react-native'

import { NavBackground, NavHeader, NavTabs, NavTabsHeader, ThemeToggle } from './_styles'
import { ThemeContext } from '../context/ThemeContext'

const Navbar = () => {
const { isDarkTheme, darkTheme, lightTheme, changeTheme } = useContext(ThemeContext)
// if isDarkTheme is true return dark state / else light
const theme = isDarkTheme ? darkTheme : lightTheme
return (
<NavBackground style={theme}>
<NavHeader style={theme}>Company Inc.</NavHeader>
<NavTabs>
<NavTabsHeader>Home</NavTabsHeader>
<NavTabsHeader>Contact</NavTabsHeader>
<ThemeToggle onPress={changeTheme}>
<Text>{theme.text}</Text>
</ThemeToggle>
</NavTabs>
</NavBackground>
)

}

export default Navbar

ToDo List

Before (Class Component)

import React from 'react'
import {Text} from 'react-native'

import { TodoBackground, TodoItem, LoginView } from './_styles'
import { ThemeContext } from '../context/ThemeContext'
import { AuthContext } from '../context/AuthContext'

class ToDoList extends React.Component {
render() {
return (
<AuthContext.Consumer >
{(authContext) => {
return (
<ThemeContext.Consumer >
{(themeContext) => {
const { isLoggedIn, changeAuthStatus } = authContext
const { isDarkTheme, darkTheme, lightTheme } = themeContext
// if isDarkTheme is true return dark state / else light
const theme = isDarkTheme ? darkTheme : lightTheme
if (isLoggedIn) {
return (
<TodoBackground style={theme}>
<TodoItem>One thing to do</TodoItem>
<TodoItem>Another thing to do</TodoItem>
<TodoItem>And one more thing</TodoItem>
<TodoItem onPress={changeAuthStatus}>
<Text>Logout</Text>
</TodoItem>
</TodoBackground>
)}
return (
<LoginView>
<TodoItem onPress={changeAuthStatus}>
<Text>Login</Text>
</TodoItem>
</LoginView>
)
}}
</ThemeContext.Consumer>
)
}}
</AuthContext.Consumer>
)
}
}

export default ToDoList

After (Functional Component)

import React, { useContext } from 'react'
import {Text} from 'react-native'

import { TodoBackground, TodoItem, LoginView } from './_styles'
import { ThemeContext } from '../context/ThemeContext'
import { AuthContext } from '../context/AuthContext'

const ToDoList = () => {

const { isDarkTheme, darkTheme, lightTheme } = useContext(ThemeContext)
const { isLoggedIn, changeAuthStatus } = useContext(AuthContext)
// if isDarkTheme is true return dark state / else light
const theme = isDarkTheme ? darkTheme : lightTheme

if (isLoggedIn) {
return (
<TodoBackground style={theme}>
<TodoItem>One thing to do</TodoItem>
<TodoItem>Another thing to do</TodoItem>
<TodoItem>And one more thing</TodoItem>
<TodoItem onPress={changeAuthStatus}>
<Text>Logout</Text>
</TodoItem>
</TodoBackground>
)}
return (
<LoginView>
<TodoItem onPress={changeAuthStatus}>
<Text>Login</Text>
</TodoItem>
</LoginView>
)
}

export default ToDoList

Creating Context with Functional Components

Authentication Context

Before (Class Component)

import React, {createContext, Component} from 'react'

export const AuthContext = createContext()

class AuthContextProvider extends Component {
state = {
isLoggedIn: false
}

changeAuthStatus = () => {
this.setState({isLoggedIn: !this.state.isLoggedIn})
}

render() {
return (
<AuthContext.Provider value={{...this.state, changeAuthStatus: this.changeAuthStatus}}>
{this.props.children}
</AuthContext.Provider>
)
}
}

export default AuthContextProvider

After (Functional Component)

import React, { createContext, useState } from 'react'

export const AuthContext = createContext()

const AuthContextProvider = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false)

const changeAuthStatus = () => {
setIsLoggedIn(!isLoggedIn)
}

return (
<AuthContext.Provider value={{ isLoggedIn, changeAuthStatus}} >
{ children }
</AuthContext.Provider>
)
}

export default AuthContextProvider

Theme Context

In case of the theme context this makes a little less sense - it actually is more readable as a class component.

Before (Class Component)

import React, {createContext, Component} from 'react'

export const ThemeContext = createContext()

class ThemeContextProvider extends Component {
state = {
isDarkTheme: true,
lightTheme: {
color: 'purple',
backgroundColor: 'snow',
text: 'Dark'
},
darkTheme: {
color: 'plum',
backgroundColor: 'purple',
text: 'Light'
}
}

changeTheme = () => {
// toggle theme to opposite when called
this.setState({isDarkTheme: !this.state.isDarkTheme})
}

render() {
return (
<ThemeContext.Provider value={{ ...this.state, changeTheme: this.changeTheme }}>
{this.props.children}
</ThemeContext.Provider>
)
}
}


export default ThemeContextProvider

After (Functional Component)

import React, {createContext, useState } from 'react'

export const ThemeContext = createContext()

const ThemeContextProvider = ({ children }) => {

const [isDarkTheme, setIsDarkTheme] = useState(true)

const [lightTheme, setLightTheme] = useState({
color: 'purple',
backgroundColor: 'snow',
text: 'Dark'
})

const [darkTheme, setdarkTheme] = useState({
color: 'plum',
backgroundColor: 'purple',
text: 'Light'
})

const changeTheme = () => {
// toggle theme to opposite when called
setIsDarkTheme(!isDarkTheme)
}

return (
<ThemeContext.Provider value={{ isDarkTheme, changeTheme, lightTheme, darkTheme }}>
{ children }
</ThemeContext.Provider>
)
}

export default ThemeContextProvider

Handling To-Do Items using Context

So far the to-do list simply rendered a few hard coded items. Let's start by copying them into context like:

import React, { createContext, useState } from 'react'
import {v4 as uuidv4} from 'uuid'

export const TodoItemContext = createContext()

const TodoItemContextProvider = ( {children} ) => {
const [items, setItems] = useState([
{ text: 'One thing to do', id: `${uuidv4()}`},
{ text: 'Another thing to do', id: `${uuidv4()}`},
{ text: 'And one more thing', id: `${uuidv4()}`},
])

return (
<TodoItemContext.Provider value={{items}}>
{children}
</TodoItemContext.Provider>
)
}

export default TodoItemContextProvider

To provide this context we have to wrap the list inside the provider tags:

import React from 'react'

import { RootView } from './src/components/_styles'
import ThemeContextProvider from './src/context/ThemeContext'
import AuthContextProvider from './src/context/AuthContext'
import TodoItemContextProvider from './src/context/TodoItemContext'
import Navbar from './src/components/Navbar'
import TodoList from './src/components/ToDo'

const App = () => {
return (
<RootView>
<ThemeContextProvider>
<Navbar />
<TodoItemContextProvider>
<AuthContextProvider>
<TodoList />
</AuthContextProvider>
</TodoItemContextProvider>
</ThemeContextProvider>
</RootView>
);
}

export default App

And then access the context to fill a React Native FlatList using the useContext function like:

import React, { useContext } from 'react'
import { Text } from 'react-native'

import { TodoBackground, TodoList, TodoItem, LoginView } from './_styles'
import { ThemeContext } from '../context/ThemeContext'
import { AuthContext } from '../context/AuthContext'
import { TodoItemContext } from '../context/TodoItemContext'

const ToDoList = () => {

const { isDarkTheme, darkTheme, lightTheme } = useContext(ThemeContext)
const { isLoggedIn, changeAuthStatus } = useContext(AuthContext)
const { items } = useContext(TodoItemContext)
// if isDarkTheme is true return dark state / else light
const theme = isDarkTheme ? darkTheme : lightTheme

if (isLoggedIn) {
return (
<TodoBackground style={theme}>
{
items.length ? (
<TodoList
data={items}
keyExtractor={(item) => item.id}
renderItem={({item}) => {
return <TodoItem>{item.text}</TodoItem>
}}
/>
) : (
<TodoItem>Nothing to do...</TodoItem>
)
}
<TodoItem onPress={changeAuthStatus}>
<Text>Logout</Text>
</TodoItem>
</TodoBackground>
)}
return (
<LoginView>
<TodoItem onPress={changeAuthStatus}>
<Text>Login</Text>
</TodoItem>
</LoginView>
)
}

export default ToDoList

Adding Items using Context

Start by creating an addItem function inside the To-Do List component:

import React, { createContext, useState } from 'react'
import {v4 as uuidv4} from 'uuid'

export const TodoItemContext = createContext()

const TodoItemContextProvider = ( {children} ) => {
const [items, setItems] = useState([
{ text: 'Have a great day!', id: `${uuidv4()}`}
])

const addItem = (item) => {
setItems([...items, {text: item, id: `${uuidv4()}`}])
}

return (
<TodoItemContext.Provider value={{items, addItem}}>
{children}
</TodoItemContext.Provider>
)
}

export default TodoItemContextProvider

And add an InputText field + a submit button that can take the text input and forward it to the addItem function for us:

import React, { useContext, useState, useEffect } from 'react'
import { Text } from 'react-native'

import { TodoBackground, TodoList, TodoItem, LoginView, InputItem } from './_styles'
import { ThemeContext } from '../context/ThemeContext'
import { AuthContext } from '../context/AuthContext'
import { TodoItemContext } from '../context/TodoItemContext'

const ToDoList = () => {

const [todo, setTodo] = useState('')

const { isDarkTheme, darkTheme, lightTheme } = useContext(ThemeContext)
// if isDarkTheme is true return dark state / else light
const theme = isDarkTheme ? darkTheme : lightTheme

const { isLoggedIn, changeAuthStatus } = useContext(AuthContext)
const { items, addItem } = useContext(TodoItemContext)

const handleChange = (text) => {
setTodo(text)
}

const handleAddItem = () => {
if(todo.length > 0)
addItem(todo)
setTodo('')
}

if (isLoggedIn) {
return (
<TodoBackground style={theme}>
{
items.length ? (
<TodoList
data={items}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
renderItem={({item}) => {
return <TodoItem>{item.text}</TodoItem>
}}
/>
) : (
<TodoItem>Nothing to do...</TodoItem>
)
}
<InputItem
value={todo}
onChangeText={(text) => handleChange(text)}
/>
<TodoItem onPress={handleAddItem}>
<Text>Add Item</Text>
</TodoItem>
<TodoItem onPress={changeAuthStatus}>
<Text>Logout</Text>
</TodoItem>
</TodoBackground>
)}
return (
<LoginView>
<TodoItem onPress={changeAuthStatus}>
<Text>Login</Text>
</TodoItem>
</LoginView>
)
}

export default ToDoList

Removing Items using Context

Now lets add another function to our context that allows us to remove items from the list:

import React, { createContext, useState } from 'react'
import {v4 as uuidv4} from 'uuid'

export const TodoItemContext = createContext()

const TodoItemContextProvider = ( {children} ) => {
const [items, setItems] = useState([
{ text: 'Have a great day!', id: `${uuidv4()}`}
])

const addItem = (item) => {
setItems([...items, {text: item, id: `${uuidv4()}`}])
}

const removeItem = (id) => {
setItems(items.filter((item) => {
return item.id !== id
}))
}

return (
<TodoItemContext.Provider value={{items, addItem, removeItem}}>
{children}
</TodoItemContext.Provider>
)
}

export default TodoItemContextProvider

We can call this function by pressing an item and providing the item id for the removal in the process:

import React, { useContext, useState } from 'react'
import { Text, TouchableOpacity } from 'react-native'

import { TodoBackground, TodoList, TodoItem, LoginView, InputItem } from './_styles'
import { ThemeContext } from '../context/ThemeContext'
import { AuthContext } from '../context/AuthContext'
import { TodoItemContext } from '../context/TodoItemContext'

const ToDoList = () => {

const [todo, setTodo] = useState('')

const { isDarkTheme, darkTheme, lightTheme } = useContext(ThemeContext)
// if isDarkTheme is true return dark state / else light
const theme = isDarkTheme ? darkTheme : lightTheme

const { isLoggedIn, changeAuthStatus } = useContext(AuthContext)
const { items, addItem, removeItem } = useContext(TodoItemContext)

const handleChange = (text) => {
setTodo(text)
}

const handleAddItem = () => {
if(todo.length > 0)
addItem(todo)
setTodo('')
}

const handleRemoveItem = (id) => {
removeItem(id)
}

if (isLoggedIn) {
return (
<TodoBackground style={theme}>
{
items.length ? (
<TodoList
data={items}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
renderItem={({item}) => {
return <TouchableOpacity onPress={() => handleRemoveItem(item.id)}><TodoItem>{item.text}</TodoItem></TouchableOpacity>
}}
/>
) : (
<TodoItem>Nothing to do...</TodoItem>
)
}
<InputItem
value={todo}
onChangeText={(text) => handleChange(text)}
/>
<TodoItem onPress={handleAddItem}>
<Text>Add Item</Text>
</TodoItem>
<TodoItem onPress={changeAuthStatus}>
<Text>Logout</Text>
</TodoItem>
</TodoBackground>
)}
return (
<LoginView>
<TodoItem onPress={changeAuthStatus}>
<Text>Login</Text>
</TodoItem>
</LoginView>
)
}

export default ToDoList