Server Repository : https://github.com/FastheDeveloper/react-native-rise-sdui-server
Mobile Repository : https://github.com/FastheDeveloper/react-native-rise-sdui-mobile
This article contains the implementation for Server-Driven UI (SDUI) development using Rise Tools. It's designed to seamlessly update React Native applications, providing dynamic UI components that can be updated in production without rebuilding or redeploying your app.
- Core principles of Server Defined Rendering for React Native
- Server Defined Rendering VS OTA updates Vs Update Prompts
- Rise Tools Architecture
- Creating Server side
- Creating Client side app
- Building mobile app for installation
- Deploying backend server and using it to update the mobile app
Before diving in, make sure you have:
- A basic understanding of JavaScript, Typescript and React Native
- Node.js and npm installed on your development machine
- Make sure you have completed the React Native - Environment Setup instructions
- A basic knowlwdge of Tamagui
-
Introduction to Server-Driven Development
- Definition and core concepts of Server-Driven UI (SDUI)
- Benefits and use cases for SDUI
- Real-world examples of apps utilizing SDUI and their success stories
-
Setting Up Rise Server
- Project setup & folder structure
- Server configuration and initialization
- Creating Base HomeScreen Model
-
Setting Up Your Expo Project
- Cloning existing expo template
- Creating a new Expo project
- Essential configurations and dependencies
- Integrating the HomeScreen model from the server
- Updating React Native Application with Server update
- Deploying the Server to Production
- Dynamic React Native App Updates from Server
- Building and Testing the Mobile App for Production
Definition and core concepts of Server-Driven UI (SDUI): SDUI is an architectural pattern where the server defines and controls the user interface of a client application. Instead of hardcoding UI elements in the client, the server sends a UI specification (often in JSON format) that the client interprets and renders. This approach allows for dynamic UI updates without requiring app updates.
- UI specification: A structured description of the interface layout and components
- Client-side renderer: A component that interprets the specification and creates native UI elements
- Dynamic updates: The ability to change the UI by modifying the server-side specification
- Flexibility: Easily update UI without app store releases
- Consistency: Maintain a uniform experience across platforms
- A/B testing: Quickly test different layouts or features
- Personalization: Tailor UI based on user segments or behaviors
- Rapid iteration: Implement UI changes faster
- Feature flags: Enable or disable features remotely
- Reduced app size: Some UI logic moves to the server
- Instant Updates: Unlike OTA updates or update prompts, SDR ensures all users are always on the latest version, eliminating fragmentation and version compatibility issues.
- Improved User Experience: SDR removes the need for intrusive update prompts or download waiting times, providing a seamless experience.
- Enhanced Security and Control: With logic on the server, SDR offers better protection against reverse engineering and unauthorized modifications.
- Flexibility and Agility: SDR allows for rapid iterations and A/B testing without the need for app store approvals or user actions.
- Reduced Client-Side Complexity: By moving logic to the server, SDR simplifies client-side code, potentially improving app performance and reducing bugs.
- Data-Driven Improvements: With all users on the same version, it's easier to gather consistent analytics and make data-driven decisions.
We will work with this UI focusing on creating a server-side UI and implementing it in a React Native application. Follow the tutorial sections to build your SDUI-powered app step by step.
To create a new Rise server project, follow these steps:
- Open your terminal.
- Run the following command:
npm create rise@latest
This is the resulting folder structure
my-rise-project/
├── node_modules
├── src/
│ ├── models.tsx
│ ├── server.ts
├── package.json
├── tsconfig.json
└── README.md
Create a HomeScreen.tsx
file in your preferred directory, and let's get started.
import {
H1,
H3,
H6,
Text,
XStack,
YStack,
} from "@rise-tools/kitchen-sink/server";
export const models = {
HeaderUI,
SearchUI,
NearbyUi: NearbyUi,
BannerUi,
ProductUi,
HomeScreen,
};
function HeaderUI() {
return (
<YStack>
<Text>Header Ui</Text>
</YStack>
);
}
function SearchUI() {
return (
<YStack>
<H1>Search Ui</H1>
</YStack>
);
}
function NearbyUi() {
return (
<YStack>
<H3>Nearby resturants</H3>
<XStack gap="$12">
<H6>Nearest resturants</H6>
</XStack>
</YStack>
);
}
function BannerUi() {
return (
<YStack>
<H1>Banner</H1>
</YStack>
);
}
function ProductUi() {
return (
<YStack>
<Text>Our Top Product</Text>
<XStack gap="$8">
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
</XStack>
</YStack>
);
}
function TripUi() {
return (
<YStack gap="$8">
<Text>Your Trips</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
</YStack>
);
}
function HomeScreen() {
return (
<YStack>
<HeaderUI />
<SearchUI />
<NearbyUi />
<BannerUi />
<ProductUi />
<TripUi />
</YStack>
);
}
An object named models is exported, containing references to various UI components. This object is used for dynamic rendering and to organize components for easier access.
This component breaks down the HomeScreen
into several distinct components: HeaderUi
, SearchUi
, NearbyUi
, BannerUi
, ProductUi
, and TripUi
. These components can be used individually on the mobile/client side or combined to form the complete HomeScreen
. By using the main HomeScreen
in the client app, we can dynamically rearrange its structure from the server.
import { setupRiseTools } from "@rise-tools/cli";
import { createWSServer } from "@rise-tools/server";
import { models as mainModal } from "~src/home/home"; // import models from `Homescreen.tsx`
const port = Number(process.env.PORT || "3015");
const models = { ...mainModal };
const server = createWSServer(models, port);
if (process.env.NODE_ENV === "development") {
setupRiseTools({ server });
}
To quickly get started with your application, you can clone this base repository, which is set up with Expo
, expo-router
, Tamagui
, and Rise Tools
.
Run the following command to clone the repository:
git clone [email protected]:rise-tools/rise-mobile-quickstart.git
To set up your project with Tamagui and Expo Router, run the following command:
npx create-expo-stack@latest react-native-rise-sdui-mobile --expo-router --tamagui
Then, install the necessary dependencies:
@rise-tools/kit-expo-router
@rise-tools/kitchen-sink
@rise-tools/react
@rise-tools/ws-client
expo-haptics
Next, create a modelSource.ts
file to define the connection to the WebSocket server provided by the server.
import { createWSModelSource } from "@rise-tools/ws-client";
export const modelSource = createWSModelSource("ws://192.168.0.213:3015"); //switch 192.168.0.213 with your expo localhost url
Create a riseActions.ts
file to define the local action library for the app.
import { useExpoRouterActions } from "@rise-tools/kit-expo-router";
import {
useHapticsActions,
useLinkingActions,
useToastActions,
} from "@rise-tools/kitchen-sink";
export function useRiseActions() {
return {
...useHapticsActions(),
...useLinkingActions(),
...useToastActions(),
...useExpoRouterActions(),
};
}
Create a riseComponents.ts
file to define the local component library for the app.
import {
FormComponents,
LucideIconsComponents,
QRCodeComponents,
RiseComponents,
SVGComponents,
TamaguiComponents,
} from '@rise-tools/kitchen-sink';
export const components = {
...FormComponents,
...LucideIconsComponents,
...QRCodeComponents,
...RiseComponents,
...SVGComponents,
...TamaguiComponents,
};
-
In the server terminal, run Server with
npm run dev
to start the server -
In the react native app terminal, run the app using the following commands:
yarn ios
to open the app on an iOS simulatoryarn android
to open the app on an Android emulatoryarn start
to open the app with Expo on your physical device -
In the appropriate Expo file, set up your HomeScreen to utilize Rise tools by calling the models defined for the HomeScreen:
import { Rise } from '@rise-tools/react';
import { Stack, Link } from 'expo-router';
import { View } from 'react-native';
import { Button } from '~/components/Button';
import { Container } from '~/components/Container';
import { ScreenContent } from '~/components/ScreenContent';
import { modelSource } from '~/src/modelSource';
import { useRiseActions } from '~/src/riseActions';
import { components } from '~/src/riseComponents';
export default function Home() {
return (
<>
<Rise
modelSource={modelSource}
components={components}
path="HomeScreen" //model gotten from the server
actions={useRiseActions()}
/>
</>
);
}
While the server is running, any changes made to the Homescreen model will be automatically reflected in the mobile application.
function HeaderUI() {
return (
<XStack
alignItems={"center"}
padding={"$2"}
justifyContent={"space-between"}>
<XStack>
<UserIcon width={70} height={60} />
<XStack alignItems={"center"}>
<YStack>
<Text color={"#838282"}>Hello,</Text>
<H3 color={"#0F0F0F"} fontWeight={"700"}>
F.A.S
</H3>
</YStack>
</XStack>
</XStack>
<Hamburger width={32} height={32} />
</XStack>
);
}
To finalize the design, we'll update the other components in our homescreen component, which will automatically update the app.
import {
H3,
H6,
H5,
RiseForm,
Text,
XStack,
YStack,
InputField,
} from "@rise-tools/kitchen-sink/server";
import {
UserIcon,
Hamburger,
SearchIcon,
MicIcon,
SmallFork,
RightIcon,
ClockIcon,
StarIcon,
CarIcon,
PackageIcon,
ErrandIcon,
Ridesicon,
ResturantIcon,
} from "~/components/UserIcon";
export const models = {
HeaderUI,
SearchUI,
NearbyUi: NearbyUi,
BannerUi,
ProductUi,
HomeScreen,
};
const myProducts = [
{
name: "Package",
icon: <PackageIcon width={32} height={32} />,
},
{
name: "Rides",
icon: <Ridesicon width={35} height={32} />,
},
{
name: "Errands",
icon: <ErrandIcon width={32} height={32} />,
},
{
name: "Resturant",
icon: <ResturantIcon width={32} height={32} />,
},
];
function HeaderUI() {
return (
<XStack
alignItems={"center"}
padding={"$2"}
justifyContent={"space-between"}>
<XStack>
<UserIcon width={70} height={60} />
<XStack alignItems={"center"}>
<YStack>
<Text color={"#838282"}>Hello,</Text>
<H3 color={"#0F0F0F"} fontWeight={"700"}>
F.A.S
</H3>
</YStack>
</XStack>
</XStack>
<Hamburger width={32} height={32} />
</XStack>
);
}
function SearchUI() {
return (
<XStack alignItems={"center"}>
<SearchIcon width={20} height={20} />
<RiseForm onSubmit={() => {}} flex={1} justifyContent={"center"}>
<InputField
id="searchInput"
backgroundColor={"#fff"}
borderWidth={0}
borderRadius={0}
height={40}
style={{ marginTop: -14 }}
placeholder={"Where are you going?"}
placeholderTextColor={"#838282"}
color={"black"}
/>
</RiseForm>
<MicIcon width={20} height={20} />
</XStack>
);
}
function NearbyUi() {
return (
<YStack marginTop={"$4"} gap={"$2"}>
<XStack alignItems={"flex-end"} justifyContent={"space-between"}>
<H3 color={"black"} fontWeight={"500"}>
Nearby Resturants
</H3>
<H3 color={"#838282"} fontSize={20}>
See all
</H3>
</XStack>
<XStack justifyContent={"space-between"} alignItems={"center"}>
<XStack gap={"$2"} alignItems={"center"}>
<SmallFork width={34} height={34} />
<YStack gap={"$2"}>
<H6 color={"#000"} fontWeight={"500"}>
The Place Restaurant
</H6>
<XStack gap={"$4"} alignItems={"center"}>
<XStack gap={"$2"}>
<ClockIcon width={16} height={16} />
<Text color={"#838282"}>5mins drive</Text>
</XStack>
<XStack gap={"$2"}>
<StarIcon width={16} height={16} />
<Text color={"#838282"}>4.5</Text>
</XStack>
</XStack>
</YStack>
</XStack>
<RightIcon width={24} height={24} />
</XStack>
</YStack>
);
}
function BannerUi() {
return (
<XStack
backgroundColor={"#24B229"}
alignItems={"center"}
justifyContent={"space-between"}
paddingHorizontal={"$4"}
marginVertical={"$4"}
borderRadius={15}>
<YStack>
<Text fontSize={20} color={"white"} fontWeight={"500"}>
Ride with us.
</Text>
<Text fontSize={20} color={"white"} fontWeight={"500"}>
Earn Points.
</Text>
<Text fontWeight={"500"} color={"white"}>
Get Started {"->"}
</Text>
</YStack>
<CarIcon width={187} height={118} />
</XStack>
);
}
function ProductUi() {
return (
<YStack gap={"$2"}>
<XStack alignItems={"flex-end"} justifyContent={"space-between"}>
<H5 color={"black"} fontWeight={"500"}>
Our Products
</H5>
<H3 color={"#838282"} fontSize={20}>
See all
</H3>
</XStack>
<XStack marginBottom={"$4"}>
{myProducts.map((item) => (
<YStack
key={item.name}
justifyContent="space-between"
alignItems="center"
flex={1}>
<YStack alignItems="center">
<YStack
backgroundColor="#ECECEC"
borderRadius={"$4"}
padding={"$4"}
marginVertical={"$2"}>
{item.icon}
</YStack>
<Text fontSize={14} color={"#0F0F0F"}>
{item.name}
</Text>
</YStack>
</YStack>
))}
</XStack>
</YStack>
);
}
function TripUi() {
return (
<YStack gap={"$2"}>
<XStack alignItems={"flex-end"} justifyContent={"space-between"}>
<H5 color={"black"} fontWeight={"500"}>
Your trips
</H5>
<H3 color={"#838282"} fontSize={20}>
See all
</H3>
</XStack>
<XStack justifyContent={"space-between"} alignItems={"center"}>
<XStack gap={"$2"} alignItems={"center"}>
<SmallFork width={34} height={34} />
<YStack gap={"$2"}>
<H6 color={"#000"} fontWeight={"500"}>
So Fresh
</H6>
<XStack gap={"$4"} alignItems={"center"}>
<XStack gap={"$2"}>
<ClockIcon width={16} height={16} />
<Text color={"#838282"}>1hr drive</Text>
</XStack>
<XStack gap={"$2"}>
<StarIcon width={16} height={16} />
<Text color={"#838282"}>4</Text>
</XStack>
</XStack>
</YStack>
</XStack>
<H6 color={"#000"} fontWeight={"500"}>
₦12,000.00
</H6>
</XStack>
<XStack
justifyContent={"space-between"}
alignItems={"center"}
marginTop={"$4"}>
<XStack gap={"$2"} alignItems={"center"}>
<Ridesicon width={34} height={34} />
<YStack gap={"$2"}>
<H6 color={"#000"} fontWeight={"500"}>
Film house cinema
</H6>
<XStack gap={"$4"} alignItems={"center"}>
<XStack gap={"$2"}>
<ClockIcon width={16} height={16} />
<Text color={"#838282"}>15mins drive</Text>
</XStack>
<XStack gap={"$2"}>
<StarIcon width={16} height={16} />
<Text color={"#838282"}>2.5</Text>
</XStack>
</XStack>
</YStack>
</XStack>
<H6 color={"#000"} fontWeight={"500"}>
₦5,000.00
</H6>
</XStack>
<XStack
justifyContent={"space-between"}
alignItems={"center"}
marginTop={"$4"}>
<XStack gap={"$2"} alignItems={"center"}>
<Ridesicon width={34} height={34} />
<YStack gap={"$2"}>
<H6 color={"#000"} fontWeight={"500"}>
iFitness Gym
</H6>
<XStack gap={"$4"} alignItems={"center"}>
<XStack gap={"$2"}>
<ClockIcon width={16} height={16} />
<Text color={"#838282"}>10min drive</Text>
</XStack>
<XStack gap={"$2"}>
<StarIcon width={16} height={16} />
<Text color={"#838282"}>2.5</Text>
</XStack>
</XStack>
</YStack>
</XStack>
<H6 color={"#000"} fontWeight={"500"}>
₦3,000.00
</H6>
</XStack>
</YStack>
);
}
function HomeScreen() {
return (
<YStack paddingHorizontal={"$4"}>
<HeaderUI />
<SearchUI />
<NearbyUi />
<BannerUi />
<ProductUi />
<TripUi />
</YStack>
);
}
A common use case for Server Driven Rendering is managing promotional content. For example, if our `BannerUi` represents promotional content that needs to be hidden, you simply need to delete or comment it out on the server. This change will automatically remove the promotional content from the React Native application.
To deploy the Rise server, we will use Render
for hosting.
Update the modelSource.ts file in the React Native application to use the new render service link.
export const modelSource = createWSModelSource(
"https://my-render-link.com/:3015"
);
With any update to the main branch of the server, the server will automatically redeploy. This will trigger an automatic update of the React Native app as well.
To build and run the iOS app on your physical device in release configuration:
npx expo run:ios --configuration Release --device
This command compiles the app with optimizations enabled and deploys it to your connected iOS device.
To create a release variant of your Android application:
npx expo run:android --variant release
This command generates an optimized APK or App Bundle suitable for distribution and installation
Update your server and monitor the deployment process on Render. Once the deployment is complete, the app will update accordingly.
We welcome contributions to enhance this tutorial. Feel free to submit issues or pull requests.