Getting Started with React Native Hybrid Apps

Getting Started with React Native Hybrid Apps

1. Introduction

a. Why Hybrid Structure?

The first concern that a React web developer faces when encountering React Native is, "To what extent can I reuse existing web code?" Just because you are creating a mobile app does not mean you have to rewrite every screen and business logic in React Native. If your existing React web application already has routing, permission handling, state management, and API integration flows established, a hybrid structure that executes this within a WebView of a React Native app becomes a realistic option.

This structure divides the roles of web and app. The web application maintains the existing screens and domain logic, while the React Native app takes on the role of a native shell that wraps the WebView. The native shell is responsible for mobile environment-related features such as storing authentication information, splash screens, network status detection, handling the Android hardware back button, and safe area processing.

However, simply displaying a web address within the app using WebView is not enough. Web and native function in different execution environments. The web runs on a browser runtime, while React Native runs on a native runtime closer to the mobile OS. Therefore, state synchronization, message passing methods, initialization order, and error fallback between the two areas must be separately designed.

This article is not about the detailed implementation of features in a React Native app; rather, it summarizes design points to consider when a React web developer first establishes a WebView-based hybrid app structure.

b. Scope of this Article

The scope covered in this article is the initial skeleton design of a hybrid app. Specifically, it focuses on the following items.

  • Separation of roles between React web applications and React Native apps

  • WebView shell structure

  • Bridge communication contract between web and native

  • Initial authentication information synchronization

  • Connection of splash screens and web readiness states

  • Handling Android back button

  • Network status communication

  • Security and validation points to complement during the operational phase

Conversely, the implementation of all detailed screens of the native app, store distribution, push notifications, album access, and camera permission handling are not the focus of this article. These features are seen as areas that can be incrementally expanded once the bridge structure is securely established.

2. Basic Structure

a. WebView Shell

The starting point of a WebView-based hybrid app is a single WebView shell. The React Native app acts as the shell for the app the user installs, while the actual service screens are handled by the React web application running inside the WebView.

In this structure, the React Native app has the following responsibilities.

  • Loading the WebView

  • Handling safe areas at the top/bottom of the app

  • Controlling the native splash

  • Accessing native storage like SecureStore

  • Handling Android hardware back button

  • Checking network status based on NetInfo

  • Message relaying between web and native

The web application uses React, React Router, React Query, state management tools, etc., in the same way as before to handle screens and business logic. Users feel like they are using a single app, but internally, the native shell and web application operate with divided roles.

An important point when establishing the initial structure is to view React Native not as a 'new frontend that replaces the web', but rather as a 'shell for running the web in a mobile app environment'. This approach allows connecting the minimum necessary native features needed for mobile apps while preserving existing web logic.

b. Role Separation

The most important thing to avoid in a hybrid structure is the mixing of responsibilities between web and native. When React Native dependencies are directly included in a web project, it becomes difficult to run and test standalone web applications. Conversely, if excessive web domain logic is included in a native app, the advantages of the existing web architecture will be lost.

Therefore, it is advisable to set the basic direction as follows.

  • The web owns the web technology stack and domain logic.

  • The native app owns Expo, React Native, WebView, SecureStore, and native settings.

  • The communication standards that both sides need to know are separated into a common bridge package.

This separation is especially important in the initial design phase. Even if all the detailed functionalities of the app are not yet completed, setting the boundary of responsibilities in advance makes it easier to predict the scope of impact when adding later functionalities.

For example, if the web needs to change the native header title, the web does not need to know about React Native components directly. The web just sends a message like UPDATE_HEADER_TITLE through the bridge. The native receives that message and changes its header state. This way, the web only knows the contract of 'requesting a title change to native' and does not depend on the actual native UI implementation.

c. Monorepo and Common Packages

If the web and native apps are in the same repository, it's convenient to have common packages. In particular, the bridge package is suitable for managing action names, payload types, and repository keys that need to be referenced together by web and native.

The example structure is as follows.

packages/
브리지/

apps(episodes)/
web-app/
mobile-shell/

It is advisable to keep the bridge package as a pure communication layer that does not know specific screens or domain logic. If this package becomes aware of business logic, the common package will become heavier, and the degree of coupling between web and native will increase.

Good items to include in a common package are as follows.

  • Bridge action names

  • Payload types for each action

  • Message creation function

  • Message parsing function

  • Function to send natively from the web

  • Native to web message generator

  • Shared storage key between web and native

Just by commonizing to this extent, we can reduce string mismatches, payload omissions, and storage key typos between web and native.

3. Bridge Design

a. Communication methods between web and native

The web inside WebView and the React Native app cannot directly reference each other's states. An explicit message channel is needed between them. This channel is the bridge.

When sending a message from the web to native, use window.ReactNativeWebView.postMessage() injected in the WebView environment. The native receives this message through the onMessage of the WebView.

Conversely, when sending a message from native to web, use WebView.injectJavaScript(). The native injects JavaScript code into the WebView, and the web receives this value through the message event listener.

Simplifying the flow looks like this.

Web → Native
window.ReactNativeWebView.postMessage(JSON.stringify(message))

Native → Web
webView.injectJavaScript(...)
window.dispatchEvent(new MessageEvent('message', { data }))

This communication is basically JSON string-based. Therefore, both the sender and receiver must share the same action name and payload structure.

b. Type-based message contract

Since bridge messages are string-based, there is a possibility of errors. For example, if the web sends REQUEST_USER_CONTEXT but the native processes it as REQUEST_CONTEXT, the message may be delivered but the function will not work. There may also be issues where the payload structure changes but only one side is modified.

To reduce this, the bridge should be managed as a communication contract rather than a simple utility function. You can define an event map by direction and configure it so that the payload type is automatically determined once an action name is selected.

An example is as follows.

interface NativeToWebEventMap {
  SYNC_USER_CONTEXT: SyncUserContext페이로드;
  NETWORK_STATUS_CHANGED: {
    isConnected: boolean;
    isInternetReachable?: boolean | null;
    type?: string;
  };
}

interface WebToNativeEventMap {
  REQUEST_USER_CONTEXT: void;
  SYNC_USER_CONTEXT: SyncUserContext페이로드;
  CLEAR_USER_CONTEXT: void;
  WEB_APP_READY: void;
  UPDATE_HEADER_TITLE: { title: string };
  REQUEST_NETWORK_STATUS: void;
}

In this structure, actions that require a payload can be distinguished from those that do not by type. Actions without a payload are defined as void, and by using conditional types, incorrect argument passing can be prevented at the compile stage.

Additionally, by creating a message type in the form of a discriminated union based on the event map, the receiver can safely handle the payload types per action.

However, TypeScript types are a compile-time safety feature. In actual runtime, since the structure parses a JSON string coming from outside, it can be considered to add schema validation tools like Zod during the operational phase.

4. Initialization flow

a. Authentication handshake

One of the most important aspects to be careful about in a WebView app is the initial transmission of authentication information. The native app has a token and role stored in SecureStore, and the React web application within the WebView must receive this information to determine the initial screen.

The simplest way is for the native app to send the token to the web at the onLoadEnd point of the WebView. However, this method is not reliable. onLoadEnd is closer to indicating that the WebView has finished loading the HTML. It does not mean that the React application has been mounted and that a listener for bridge messages has been registered.

In other words, the native app determines that 'the web has been loaded' and sends the message, but the web may not yet be ready to receive the message. In this case, the token message may be lost, and the user could see a screen appearing to be logged out despite having a token.

It is safer to design this problem as a handshake approach rather than a push approach. The key is to ensure that the native app does not push the authentication information first and that the web directly requests it after registering the listener.

The flow is as follows.

  1. The top-level component of the web registers the SYNC_USER_CONTEXT listener.

  2. After registering the listener, the web sends REQUEST_USER_CONTEXT.

  3. The native app retrieves the token and role from SecureStore.

  4. The native responds with SYNC_USER_CONTEXT.

  5. The web reflects the token and role in the store and state.

  6. It renders the actual screen after the synchronization is complete.

The key to this approach is not treating the WebView load completion time and the React listener ready time as the same. By requesting when the web is ready to receive, the structure where the native responds reduces the likelihood of initial message loss.

b. Initial screen defense

Even if the authentication information is received via a handshake method, there exists a short asynchronous interval between the request and response. If the web application renders first during this time, it may show an unlogged-in screen while the token has not yet been reflected. Once the token arrives, it switches back to the main screen, causing a flash.

In mobile apps, such flashing feels particularly awkward. This is because users do not know the internal structure of the WebView and perceive it as a single app. Therefore, it is necessary to defend against rendering the actual routing screen until the initial authentication synchronization is complete.

Placing a status like isSynced in the top-level area of the web and only showing a white background or loading area before synchronization is complete is appropriate. In this state, neither the login page, role selection page, nor main page is drawn first. The screen is determined after receiving authentication information from the native or after responding that there is no authentication information.

However, it is also necessary to prepare for situations where the bridge response does not come. If the response is lost due to native errors, message parsing errors, or differences in the WebView environment, the app may remain indefinitely on a blank screen. To prevent this, a fallback timer can be set to forcibly transition to a ready state after a certain period of time.

The normal path is a bridge response. The abnormal path is a timeout. It is safe to keep both of these paths in the initial design.

c. Splash integration

In React Native/Expo apps, a splash screen can be set. However, in a WebView app, the more important question is when to close the splash screen.

Closing the splash screen immediately at WebView's onLoadEnd may create the perception that the screen appears quickly. But just because the HTML load has finished does not mean that the initialization of the React app, synchronization of authentication information, and routing decisions are also finished. If the splash screen is closed at this point, users may see an unprepared blank screen or a flickering login screen.

Therefore, it is advisable to link the timing of the splash screen's closure with the actual readiness state of the web. After the web application completes authentication synchronization and initial screen judgment, it sends the WEB_APP_READY message to the native, and the native closes the splash screen at the moment it receives this message.

The flow is as follows.

  1. App Launch

  2. Expo Static Splash Display

  3. React Native Shell Loads WebView

  4. Web Registers Bridge Listener

  5. Web Requests Authentication Credentials

  6. Native Responds with Authentication Credentials

  7. Web Determines Initial Screen

  8. Web Sends WEB_APP_READY

  9. Native Completes Splash

Fallback is also needed here. If an error occurs on the web or the WEB_APP_READY message does not arrive, the splash may continue to be displayed. Therefore, after loading the WebView, it is safe to set a timer to forcibly close the splash after a certain period of time and to cancel the timer upon receiving a normal signal.

d. Storage Synchronization

In WebView apps, storage is divided into multiple layers. The web can use sessionStorage, localStorage, cookies, IndexedDB, Cache Storage, etc., and native can use separate storage like SecureStore. When dealing with authentication credentials, these storages should be designed to not conflict with each other.

Upon initial entry, the token and role are read from the native SecureStore and passed to the web. The web reflects this in the storages and states that can be used in the existing authentication flow. Conversely, if the role is changed on the web, the selected role must also be communicated to the native. This way, the last selected state can be restored when the app is restarted.

Logout requires particular attention. If only the token is cleared from the web storage, the authentication credentials may still remain in the native SecureStore. This can lead to a situation where the logout state is not restored when the app is restarted, as the native will send the token back to the web.

Therefore, the flow for logout should be designed to clear both web and native storages together. On the web, sessionStorage, localStorage, cookies, Cache Storage, IndexedDB, etc., should be cleared, and to native a CLEAR_USER_CONTEXT message can be sent to delete the SecureStore.

Additionally, it is better to manage storage keys as common constants. If the web and native each write their own string keys, it is easy to make typos or miss changes. By managing the token key and role key together in the bridge package, you can maintain the consistency of the storage contract on both sides.

5. Considerations for Mobile Environment

a. Android Back Button

The Android hardware back button in a WebView app is an item that must be considered. In web browsers, going back is naturally tied to history. However, in React Native apps, the Android back button event is first delivered to the native layer.

Native apps do not automatically know the React Router state inside the WebView. If the user presses back to return to the list from the detail screen and the app closes immediately, it creates a very awkward experience.

To prevent this, a structure that tracks canGoBack in the navigation state of the WebView is necessary. When the Android hardware back button event occurs and canGoBack is true, call webView.goBack() to prevent the app from closing. Conversely, if there is no longer WebView internal history, allow the Android default action.

This process is simple but greatly impacts the completeness of the app. Users expect that regardless of whether the app is WebView based or not, pressing the back button will take them back to the previous screen. It is important to meet this expectation even in a hybrid structure.

b. Network Status

In mobile environments, the network status changes frequently. Requests can fail or be delayed in areas with temporary communication shadows, such as subways, elevators, or while switching Wi-Fi while moving. In the web, navigator.onLine or browser's online and offline events are typically used, but this alone may not be sufficient for WebView apps.

In React Native, the NetInfo library allows you to check the device's network connection status more directly. By passing this through the bridge to the web, the web application can also utilize the native-based network status.

For example, if the native sends a NETWORK_STATUS_CHANGED message, the web can link this value with React Query's onlineManager. When disconnected, it ensures queries or mutations do not fail unnecessarily, and when the connection is restored, it can resume paused mutations or invalidate queries to fetch the latest data again.

This approach is a more stable structure than simply displaying an offline notification screen. The app can return to normal flow without the user having to refresh manually after network recovery.

However, the browser's online/offline events should not be completely disregarded. It's advisable to have a fallback together for environments where the native bridge does not operate or standalone web execution environments.

c. Operational Security

While WebView bridging is convenient, security aspects should also be considered in the production environment. During development, origins may be broadly allowed for quick verification or postMessage('*') may be used in iframe tests. However, in the production environment, it is better to clearly restrict the origins that send and receive messages.

Additionally, the bridge message is a JSON string, so it may fail to parse. TypeScript types provide safety during development, but there is no guarantee that the values coming in at runtime actually satisfy that type. Therefore, it is safer to add runtime schema validation for critical actions.

Messages that directly affect app behavior, such as authentication information, user context, network status, and requests for native features, should consider the following items.

  • Check if the message is from an allowed origin.

  • Check if the action is in the defined list.

  • Verify that the payload structure matches the expected format.

  • Log or track parsing errors instead of simply ignoring them.

  • Only pass sensitive information within the necessary scope.

When initially structuring a WebView app, it can be easy to focus on connecting functionalities, but the bridge is the conduit between the web and the app. Thus, considering validation and limitations from the early design stage helps with operational stability later on.

Conclusion

When React developers first attempt a hybrid app based on React Native WebView, what matters is not how many native screens are implemented. What is more important is defining how the web and native share responsibilities, how they communicate, and at what point they will align the initial state.

The WebView shell structure is a practical way to provide service in mobile app form while retaining the advantages of existing React web applications. However, the key to this structure is not just putting a web address in the WebView, but designing the boundaries between the web and native.

In the initial structural design phase, it is advisable to establish the following items first.

  • Separation of responsibilities between web and native

  • Bridge action and payload type contract

  • Authentication handshake flow

  • Initial screen rendering delay and fallback

  • Splash termination point

  • Synchronizing SecureStore and web storage

  • Android back button handling

  • Network status transmission

  • Origin restrictions and runtime validation in production environments

If this framework is established first, the structure will not be greatly shaken when extending detailed features later. For React developers encountering React Native for the first time, a WebView hybrid app can be an unfamiliar territory, but by separating the roles of web and app and managing the bridge with a clear contract, it is a structure that can be sufficiently accessed based on existing web experience.

Hazel

Site footer