Here on the Stream Services team, we have the pleasure of working with a variety of clients and get to solve exciting challenges daily. This blog post provides a peek into one such occasion by highlighting the intricacies of building a native app in Javascript, and the creative solutions that inevitably must arise to support multiple programming languages.
For a little bit of context (see what I did there), we recently teamed up with the fine folks at Branch to implement a deep linking and navigation feature for a client. The idea was simple: use the Branch dashboard to create deep links with specific data payloads, and on a deep link-attributed load of our client’s app, access the data for a variety of use cases. Sounds simple enough, until you learn the hard way that the native methods upon which Branch’s code is built don’t always run synchronously with a React Native / Javascript codebase.
Asynchronicity between native code and Javascript in our application created a whole host of unintended consequences in trying to read and use the deep link payloads, from multiple screen re-renders to blank page renders to straight-up error messages. We learned very quickly that Branch’s native methods weren’t going to follow the lifecycle of our React Native components. Since native code runs immediately, no matter how deeply we nest Branch’s deep link listener function, the data payload is only available on initial load. It can’t be manually fetched later by our Javascript. To handle this issue, we had to devise a means of reading and saving the deep link data payload on load time. We would then recall this data when our application needed specific values for navigation and other use cases.
From Redux to prop drilling, there are many ways one can save and share data in this day and age. After much consideration, we settled on React’s Context API as the right fit for our use. Sebastian Markbage at Facebook, essentially the godfather of Hooks and Context, says Context is best suited for “low-frequency unlikely updates” and “static values.” Therefore, the one-time receipt of an object of deep link key/value pairs fits the bill perfectly. The following example details at a high-level how we were able to read, save, and consume data contained within a deep link upon attributed entry into our application.
The first step in our sample application is to create a React context, which we’ll use to store the data payload coming in through our Branch deep link.
import React, { createContext, useState } from "react"; // create a React context for managing global state const BranchContext = createContext(); // access context value through a consumer component export const BranchConsumer = BranchContext.Consumer; // save data and share with children through provider component const BranchProvider = ({ children }) => { const [contextData, setContextData] = useState({}); const contextValue = { contextData, setContextData }; return ( <BranchContext.Provider value={contextValue}> {children} </BranchContext.Provider> ); }; export default BranchProvider;
In this file, we accomplish the following:
- Create a React context to keep track of our deep link data
- Create a consumer component which can pass specific values held in the context state to child components
- Create a provider component which enables its children to be wrapped in the consumer component and receive data as props
You’ll also notice we’re taking advantage of React’s Hooks API for local state, specifically the useState hook. The second value returned from the useState function gives us the ability to reset the first. In our case, any argument passed into the setContextData
function will overwrite the value of contextData
. Mostly we’re running the old class-based, setState
method in far fewer lines of code. In a moment, you’ll see how this setter function comes in handy.
Next up, we add context-specific code to our App.js
file.
import React, { useEffect } from "react"; import { View, Text } from "react-native"; import branch from "react-native-branch"; import BranchProvider, { BranchConsumer } from "./BranchContext"; let App = ({ setContextData }) => { // run code in useEffect callback on App load useEffect(() => { // branch's subscribe method listens for deeplink activity branch.subscribe(({ error, params }) => { if (error) return; // if the App loads via deeplink, save deeplink params in context setContextData(params); }); }, []); return ( <View> <Text>Save Deeplink Content on App Load!</Text> </View> ); }; App = props => ( // provide value stored in context to child components <BranchProvider> {/* destructure context value(s) and pass as props into App */} <BranchConsumer> {({ setContextData }) => ( <App {...props} setContextData={setContextData} /> )} </BranchConsumer> </BranchProvider> ); export default App;
In this file, we accomplish the following:
- Wrap our app in a provider component for context access
- Wrap our app in a consumer component and pass the context setter function via props
- Activate the Branch deep link listener on app load
- Use the context setter function to save any deep-link parameters picked up by the Branch listener
When our app loads, the callback function in the useEffect
hook runs, and the Branch deep link listener activates. Hopefully, now you see the benefit of utilizing the useState hook within our context. If the Branch listener picks up a deep link, the useState setter function we passed into our App component through the context consumer saves the deep link data to the contextData
variable within our BranchContext
. Once we acquire this data, any component we wrap in a BranchConsumer
component can access contextData
via props.
The last file in our app shows how to access data that has been saved in context.
import React from "react"; import { View, Text } from "react-native"; import { BranchConsumer } from "./BranchContext"; let DataConsumer = ({ deeplinkContent }) => { /* at this point, all key/value pairs embedded in the deeplink may be pulled off the `deeplinkContent` object for in-app use */ return ( <View> <Text>Access Deeplink Content in this Component!</Text> </View> ); }; DataConsumer = props => ( // pass deeplink data saved on App load into DataConsumer as props <BranchConsumer> {({ contextData }) => ( <DataConsumer {...props} deeplinkContent={contextData} /> )} </BranchConsumer> ); export default DataConsumer;
In this file, we accomplish the following:
- Wrap our component in a consumer component and pass any saved context data as props
- Allow our component to access any key/value pairs embedded in the Branch deep link
By wrapping our functional, React component in a consumer component, DataConsumer
can now access the same data object the Branch deep link listener picked up on app load. In our case, we primarily use this data for routing purposes, and deep-link navigation to specific content, the profile page of a marketing influencer as one example. Though the use case potential is endless, as any data that can be saved within a Javascript object is accessible.
The Context API saved us with unauthenticated deep link clicks, specifically when logged out or potential users click Branch links. When a user is logged in, immediate read of a deep link and navigation to content makes sense. However, attempting to navigate to a specific app screen before the user has logged in may throw a permissions error. Saving deep link data within the BranchContext
allows us to guide the user through the standard authentication process, and upon successful login, navigate to content specified in the Branch deep link. This proves particularly useful for marketing influencers who direct followers to our app and want them to land on a specific profile page after signup.
Working on a services team is a never-ending flow of critical thinking and problem-solving. At Stream, we strive to learn such lessons internally so we can share best practices with our clients and make product integrations as seamless as possible. Get in touch with our team if you’d like to see how we can support your next big idea!