Part 1: a star rating component
React Native core has no rating control. The options in 2026:
| Option | Half stars | Gesture | Pick it for | The cost |
|---|---|---|---|---|
react-native-star-rating-widget | Yes | Tap + swipe | TypeScript, maintained, animated, works in Expo Go | Needs react-native-svg |
react-native-ratings | Fractional display, tap in steps | Tap + swipe (SwipeRating) | Long-standing, custom images via AirbnbRating | Irregular releases; styling API shows its age |
DIY: 5 Pressables + icons | Yes, via locationX | Tap (swipe is extra) | Zero dependencies, exactly your icon set and colours | You write the half-star math and accessibility |
Read-only: 5 icons, no touch | Yes | None | Displaying an average (4.3) in a list; cheapest option | Not an input |
If you want a library, react-native-star-rating-widget is the one to install. It is controlled, so it fits a form library, and the API is the whole component:
import StarRating from 'react-native-star-rating-widget';
function RecipeRating({ value, onChange }) {
return (
<StarRating
rating={value}
onChange={onChange}
maxStars={5}
starSize={32}
enableHalfStar
// Users swipe across stars on mobile more than they tap; keep it on.
enableSwiping
color="#f59e0b"
emptyColor="#d4d4d8"
/>
);
}The DIY version, with half stars
For most apps a rating input is five icons and a touch handler, and a dependency is heavier than the feature. Expo ships @expo/vector-icons, and Ionicons has star, star-half, and star-outline, which is all three states you need:
// StarRating.tsx
import { Pressable, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
type Props = { value: number; onChange?: (v: number) => void; size?: number; halfStars?: boolean };
export function StarRating({ value, onChange, size = 28, halfStars = true }: Props) {
const readOnly = !onChange;
return (
<View
style={{ flexDirection: 'row', gap: 4 }}
accessibilityRole={readOnly ? 'text' : 'adjustable'}
accessibilityLabel={'Rating ' + value + ' out of 5'}
// Lets VoiceOver / TalkBack swipe up and down to change the value.
accessibilityActions={readOnly ? undefined : [{ name: 'increment' }, { name: 'decrement' }]}
onAccessibilityAction={(e) => {
if (!onChange) return;
const step = halfStars ? 0.5 : 1;
if (e.nativeEvent.actionName === 'increment') onChange(Math.min(5, value + step));
if (e.nativeEvent.actionName === 'decrement') onChange(Math.max(0, value - step));
}}
>
{[1, 2, 3, 4, 5].map((star) => {
const name =
value >= star ? 'star' : value >= star - 0.5 ? 'star-half' : 'star-outline';
return (
<Pressable
key={star}
disabled={readOnly}
hitSlop={6}
onPress={(e) => {
// Left half of the icon = half star, right half = full star.
const half = halfStars && e.nativeEvent.locationX < size / 2;
onChange?.(half ? star - 0.5 : star);
}}
>
<Ionicons name={name} size={size} color={name === 'star-outline' ? '#d4d4d8' : '#f59e0b'} />
</Pressable>
);
})}
</View>
);
}Two things worth keeping even if you change everything else. The accessibilityActions block is what makes the control usable with a screen reader: five unlabeled star buttons are a maze, one adjustable control with increment and decrement is a standard pattern. And the same component serves as the read-only display when you omit onChange, so the average in your list and the input in your form never drift apart visually.
Swipe-to-rate is the one feature the libraries add that this version lacks. If you want it, wrap the row in a Pan gesture from react-native-gesture-handler and map the x position to a value with the same half-step math.
Part 2: asking users to rate your app
This is a different problem with a different API. Both stores provide a native in-app review sheet: the user rates without leaving your app, and you never find out what they chose. In Expo it is expo-store-review:
import * as StoreReview from 'expo-store-review';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Call this after a genuine success: third workout logged, first export, etc.
export async function maybeAskForReview() {
// hasAction() is false on simulators, on web, and on devices without the store app.
if (!(await StoreReview.hasAction())) return;
const asked = await AsyncStorage.getItem('review:lastAskedAt');
const days = asked ? (Date.now() - Number(asked)) / 86_400_000 : Infinity;
// Your own gate. Apple allows 3 prompts per 365 days; the OS enforces it silently,
// so spend the chances well instead of firing on every launch.
if (days < 120) return;
await AsyncStorage.setItem('review:lastAskedAt', String(Date.now()));
await StoreReview.requestReview();
}What actually happens when you call it:
- iOS shows the SKStoreReviewController sheet, at most three times per 365 days per user. Beyond that the call resolves and nothing appears. In development builds it appears every time; in TestFlight it never appears.
- Android shows the Google Play In-App Review flow, subject to a quota Google does not publish. It may also show nothing. It requires the app to be installed from Play, so it does nothing on a sideloaded debug APK.
- Both resolve the Promise immediately, with no result. Do not write code that waits to see if the user rated.
If you want a fallback link to the store listing for a settings-page “Rate us” row, StoreReview.storeUrl() returns the right URL from your app config when ios.appStoreUrl and android.playStoreUrl are set in app.json. Open it with Linking.openURL.
The rules that get apps rejected
Apple’s App Review Guideline 5.6.1 and the Play Developer policy agree on the shape of what is not allowed, and reviewers do check:
- No custom prompt that imitates the system sheet.You may ask in your own words (“Enjoying the app?”) but not draw fake stars that look like the store UI.
- No filtering. A screen that sends happy users to the review prompt and unhappy users to a feedback form is the classic violation. Offer feedback to everyone, or offer the review to everyone, not one or the other based on their answer.
- No incentives. No credits, unlocks, or content for rating.
- No interrupting. Not on first launch, not mid-task, not during onboarding. After a success the user chose, when nothing else is on screen.
The honest version of the funnel: a neutral “Send feedback” row in settings that always exists, and requestReview() fired on your own gate after real success. It converts less than the filtered pattern, and it does not get your update rejected three days before a launch.
Putting both in a real app
Apps built with ShipNative use the DIY star component above for in-app ratings, because it inherits the app’s icon set and colours and ships with the accessibility actions, and expo-store-reviewbehind a success-count gate for the store prompt. If your prompt says “let users rate recipes” you get the first; if it says “ask for an App Store review after the third completed order” you get the second, with the gate and without the filter screen.