Users • 2026-03-31 • 5分読了
執筆: Jake Lynch(ゲスト著者)、Taylor Harrison(ゲスト著者)
Fig(Food is Good)は、食事制限やアレルギーを持つ人々が実際に食べられる安全な食品を見つけ、重篤な反応を避けられるよう支援します。成分のスキャンや安全な製品の発見、レストランが特定のニーズにどの程度対応しているかの確認まで、Figは複雑な食事ニーズを持つ人々の外食や買い物をより簡単かつ安全にします。何百万人もの人々(Figチームのほとんども含む!)が安全で自信を持った食事選びにFigを頼っています。
昨年、私たちはExpo App Awardのファイナリストになりました。Expoチームはアプリのネイティブ感とキビキビした操作性を評価してくれました。本記事ではその点について詳しく紹介します。
なぜExpoなのか
Figは当初標準的なReact Nativeアプリとして始まりましたが、開発の加速と信頼性向上のために徐々にExpoを採用してきました。エンジニアは5人しかいないため、複雑さに時間を浪費する余裕がありません。コミュニティ製パッケージが壊れてメンテナンスされなくなり痛い目にあった経験から、SDKを段階的に取り込む決断をしました。ライブラリへの信頼が高まり、将来的にさらに統合する計画です。
また、MicrosoftがAppCenterを廃止した昨年、重要なJavaScriptパッチのOTA配信をExpoのEAS Updatesに移行しました。
Expo SDKはFigの成功に不可欠だった
ここでは、小規模チームで高速かつ高性能なアプリを開発するうえで役立ったExpo SDKパッケージのいくつかを紹介します。
expo-image
expo-imageは、Figが画像やSVGを効率的にレンダリングして、コミュニティのメンバーにとって使いやすいアプリ体験を提供するのに役立ちました。標準のReact NativeのImageコンポーネントはSVGをサポートしないため、当初は react-native-svg を使ってアイコンなどのSVG描画を行っていました。SVG文字列をSvgXmlに渡すと、strokeやstroke-width、fillの属性を簡単に設定できます。
しかしSoftware Mansionのブログを読んで、SVGからコンポーネントツリーを作ることにはコストが伴い、静的なアイコンには過剰であると判断しました。アイコンをexpo-imageに移行することでネイティブライブラリにSVGレンダリングを任せ、SVG内部を操作・アニメーションしたい場合だけreact-native-svgを使うようにしています。
expo-imageはSVGの変更能力が限定的(tintColorは非透明ピクセル全体の色を設定する)なため、stroke-widthなどを変えたい場合は追加のSVGをバンドルする必要がありますが、パフォーマンスのトレードオフは十分に価値があると考えています。
react-native-svg(例):
// Imports svg as a string
import thumbsUpIcon from '@assets/icons/thumbs-up.svg';
import { iconDefault } from '@utils/colors.util';
const Icon: FunctionComponent<IconProps> = ({ iconSize, color, rotation, transform, height, width, ...otherProps }) => {
const parsedIconSize = !height && !width ? getIconSize(iconSize) : undefined;
const sizeProps: { height?: NumberProp; width?: NumberProp } = {};
if (parsedIconSize || height) {
sizeProps.height = parsedIconSize ?? height;
}
if (parsedIconSize || width) {
sizeProps.width = parsedIconSize ?? width;
}
// svg string is passed via `xml` in otherProps
return (
<SvgXml
color={color ?? iconDefault}
transform={rotation ? [{ rotate: `${rotation}deg` }] : transform}
{...sizeProps}
{...otherProps}
/>
);
};
expo-image(例):
import { Image } from 'expo-image';
import styled, { css } from 'styled-components/native';
export const iconAssets = {
thumbsUp: require('@assets/icons/thumbs-up.svg'),
...
} as const;
export const StyledImage = styled(Image)<{ width?: number; height?: number; rotation?: number }>`
${({ width }) => width !== undefined && css`width: ${width}px;`}
${({ height }) => height !== undefined && css`height: ${height}px;`}
${({ rotation }) => !!rotation && css`transform: rotate(${rotation}deg);`}
`;
const Icon: FunctionComponent<IconProps> = ({ iconSource: iconName, iconSize, height, width, rotation, color, ...otherProps }) => {
const parsedIconSize = !height && !width ? getIconSize(iconSize) : undefined;
return (
<StyledImage
tintColor={color}
source={typeof iconName === 'object' ? iconName : iconAssets[iconName]}
width={parsedIconSize ?? width}
height={parsedIconSize ?? height}
rotation={rotation}
contentFit="contain"
{...otherProps}
/>
);
};
expo-location
Figはレストラン検索機能で位置情報を扱うためにexpo-locationを使用しています。@react-native-community/geolocationから移行し、より積極的にメンテナンスされているライブラリで位置情報を細かく制御できるようになりました。
私たちは近似座標とより正確な座標を同時に取得し、キャッシュされた位置情報が十分新しければそれを利用します。これにより、地図ビューやレストラン検索を素早く近隣の店舗で埋められるようにしています。位置情報はコンテキストに保存され、アプリ全体(特に新しいレストラン検索機能)で利用します。
Figの位置情報許可を求める画面や、許可後のMapBoxビューがあります。
コード例:
import * as Location from 'expo-location';
const locationMaximumAge = 3 * 60 * 1000;
const locationRequiredAccuracy = 11;
const approximateLocationRequiredAccuracy = 3001;
export const LocationProvider: FunctionComponent<PropsWithChildren<LocationProviderProps>> = ({ children }) => {
const [coordinates, setCoordinates] = useState<Coordinates>();
const [approximateCoordinates, setApproximateCoordinates] = useState<Coordinates>();
const { permissionStatus: locationPermissionStatus, triggerRequestPermission: triggerRequestLocationPermission, } = usePermission('location');
const fetchCoordinates = useCallback(async (setNewCoords: (coords: Coordinates) => void, requiredAccuracy: number, locationAccuracy: Location.LocationAccuracy, maxAge?: number,) => {
try {
let location = await Location.getLastKnownPositionAsync({ requiredAccuracy, maxAge, });
if (!location) {
location = await Location.getCurrentPositionAsync({ accuracy: locationAccuracy, });
}
const newCoordinates = location?.coords;
setNewCoords(newCoordinates);
return newCoordinates;
} catch (e) {
logFigError('Error getting location', { error: e, });
}
}, [mockLatitude, mockLongitude]);
const updateLocation = useCallback(async () => {
const [, newCoordinates] = await Promise.all([
fetchCoordinates(setApproximateCoordinates, approximateLocationRequiredAccuracy, Location.LocationAccuracy.Lowest),
fetchCoordinates(setCoordinates, locationRequiredAccuracy, Location.LocationAccuracy.High, locationMaximumAge),
]);
return newCoordinates;
}, [fetchCoordinates]);
useEffectOnce(locationPermissionStatus === 'granted', () => { updateLocation(); });
return (
<LocationContext.Provider value={{ coordinates, approximateCoordinates, updateLocation }}>
{children}
</LocationContext.Provider>
);
};
expo-store-review
プラットフォームのアップデートでAPIが非推奨になり、コミュニティパッケージが壊れることがありました。例えばiOS 18でSKStoreReviewControllerが非推奨になり、react-native-in-app-reviewが動作しなくなったことがありました。expo-store-reviewを使うことで、必要なOSバージョンをサポートするレビュー促進のライブラリに素早く移行できました。スクリーンへの組み込みもシンプルで、多くのコミュニティライブラリとは異なり、基盤となるAPIが変わってもExpoのライブラリは継続的にメンテナンスされています。
コード例:
import * as StoreReview from 'expo-store-review';
if (await StoreReview.hasAction()) {
StoreReview.requestReview();
}
App Center廃止後のExpoのOTAアップデートによる重要パッチの配信
App Centerが2025年3月にCodePushを廃止した後、Figは重要な修正や実験的な体験テストを即座に配信するためにExpo EAS Updatesを採用しました。これによりコード出荷に自信を持てるようになり、本番に到達したバグに迅速に対処できるようになりました。生産ビルドは今もBitriseを使用していますが、必要に応じてExpoのOTA更新システムが重要なアップデートをユーザーに確実に届けてくれる点を高く評価しています。
Expoとともに歩むFigの未来
Figは今後もメンテナンス負荷を下げ、ビルドの複雑さを簡素化し、プラットフォームの拡張に伴う新しいネイティブ機能を活用するため、さらに多くのExpoモジュールを統合していきます。以前はReact Nativeのバージョンアップが恐怖のタスクで、壊れるまで避けていましたが、より多くのExpoパッケージへ移行したことでアップグレードプロセスは容易で信頼できるものになり、サポートされているReact Nativeのバージョンを保つために開発サイクルを無駄にしなくて済むようになりました。