Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/screen-option-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@granite-js/react-native': major
'@granite-js/router': minor
'@granite-js/plugin-router': minor
'create-granite-app': patch
---

Separate app registration from routing. Granite.registerApp now accepts only appName and renders the supplied app directly with initial props and visibility lifecycle. Move context, URL resolution, screen options, and gesture integration to an explicitly rendered Router.

Add core and router entry points. Router owns its navigation container and back-handler scope, accepts initialUrl and an injected onRootBack action, and supports hook-derived defaultScreenOptions through normal React props. The options callback receives routeIndex and onBack; defaultScreenOption remains a deprecated Router alias. Update generated apps and examples to the explicit composition API.

Extract routing, typed route definitions, and back handlers into @granite-js/router. The React Native package depends on and re-exports it, with a host visibility adapter. Standalone consumers can inject activity without depending on app registration or host bridges. Update route generation to target the owning package while preserving legacy module augmentation.
70 changes: 70 additions & 0 deletions docs/guides/granite-router/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,76 @@ navigation.dispatch(

- Use `CommonActions.reset` to keep only a specific screen in the history and delete the rest of the screen history.

## Render Router inside your app

`Granite.registerApp` registers your component and supplies native initial props and visibility lifecycle. It does not create a router. The implementation lives in `@granite-js/router`, a dependency of `@granite-js/react-native`. Import the host-aware `Router` adapter from `@granite-js/react-native/router` and render it where navigation is needed. A routerless app can import registration APIs from `@granite-js/react-native/core` without loading React Navigation.

Router owns its NavigationContainer, native stack, safe-area provider, and back-handler scope. Each Router has independent navigation and back-handler state. The adapter forwards host activity to the standalone package; the package does not import the registration runtime or host bridges. You do not need to wrap it in a NavigationContainer or register it with Granite.

```tsx
import { useColorScheme } from 'react-native';
import { Granite, type InitialProps } from '@granite-js/react-native/core';
import { Router, BackButton } from '@granite-js/react-native/router';
import { closeView, getSchemeUri } from '@granite-js/react-native';
import { context } from '../require.context';

function App({ scheme }: InitialProps) {
const dark = useColorScheme() === 'dark';

return (
<Router
context={context}
prefix="example://app"
initialUrl={scheme}
getInitialUrl={(url) => url ?? getSchemeUri()}
onRootBack={closeView}
defaultScreenOptions={({ routeIndex, onBack }) => ({
headerTintColor: dark ? 'white' : 'black',
title: routeIndex === 0 ? 'Welcome' : 'Details',
...(routeIndex === 0 && {
headerLeft: (props) => <BackButton {...props} onPress={onBack} />,
}),
})}
/>
);
}

export default Granite.registerApp(App, { appName: 'example' });
```

For a custom theme, place a component that calls `useTheme()` below your ThemeProvider and render Router from that component. Call hooks in the parent component, never inside `defaultScreenOptions`. Changing the resulting options updates mounted screens without resetting the stack. Keep Router's React key and route component identities stable.

`defaultScreenOptions` accepts an object or a function receiving the native-stack `route`, `navigation`, and `theme`, plus:

- `routeIndex`: this route's zero-based position, not the currently focused index. Duplicate pushes have distinct positions. A preloaded route not yet in the stack has index `-1`.
- `onBack`: runs registered back handlers, then goes back in the current navigation state or invokes `onRootBack` when no history remains. It does not directly remove the route that supplied the callback.

Router never calls a host close bridge itself. Supply `onRootBack` to connect one. Without it, root back has no host action and no default root back button is shown. Android hardware back at the root uses the supplied action; host-specific iOS gesture controls can be supplied through `setIosSwipeGestureEnabled` and `setiOSBackPressHandler`.

`initialUrl` and optional `getInitialUrl` resolve the entry URL on mount. Router does not infer them from a global app name or native bridge. Changing initial URL props does not navigate or reset an existing stack. Use navigation actions for subsequent navigation. `prefix` must match the URL scheme/host/path used by the app.

The legacy `defaultScreenOption` spelling is a deprecated Router prop retaining its original callback arguments. TypeScript rejects specifying both names; untyped callers use the plural option without merging. Page `screenOptions` and existing `navigation.setOptions` retain precedence. Existing non-root back-button behavior is retained.

### Migrating from automatic registration

This is a breaking registration API change:

- Move `context` and routing options from `Granite.registerApp` to `<Router>`.
- Render Router instead of returning the injected `children`; registration no longer inserts a Navigator.
- Replace registration's `initialScheme` with Router's `initialUrl`. Resolve function-based URLs in your app or `getInitialUrl`.
- Supply an explicit `prefix`, and `onRootBack` when the host view should close.
- Move gesture callbacks and `screenContainer` / `defaultErrorComponent` to Router.
- Move wrappers that use navigation/back hooks into `screenContainer` or page/layout components below Router. Providers used to compute options belong above Router.
- Replace wrappers that clone the injected Navigator with `defaultScreenOptions` on Router.

### Using the Router package independently

Install `@granite-js/router` with compatible `@granite-js/native`, `react`, and `react-native` peers. Import `Router`, `createRoute`, and the navigation/back hooks directly from `@granite-js/router`; no Granite registration is required. The standalone Router uses AppState by default. Supply `isActive` when a host owns activity state.

The existing root and `/router` exports in `@granite-js/react-native` share route definitions and back-handler contexts with the new package. New generated route types augment `@granite-js/router`; old declarations augmenting `@granite-js/react-native` remain supported. Add `@granite-js/router` as a direct app dependency when using the new generated declarations or direct imports.

React Navigation remains the internal navigation engine. This change separates registration and Router composition; it does not replace the engine or the existing typed-route definition API.

## References

- [React Navigation Official Documentation](https://reactnavigation.org/)
Expand Down
70 changes: 70 additions & 0 deletions docs/ko/guides/granite-router/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,76 @@ navigation.dispatch(

- `CommonActions.reset`으로 특정 화면만 기록에 남기고 나머지 화면 기록을 삭제할 수 있어요.

## 앱 안에서 Router 렌더링하기

`Granite.registerApp`은 앱 컴포넌트를 등록하고 네이티브 초기 props와 visibility 생명주기를 제공해요. Router는 자동으로 만들지 않아요. 구현은 별도 `@granite-js/router` 패키지가 소유하고 `@granite-js/react-native`가 dependency로 가져요. 호스트와 연결된 `Router` adapter를 `@granite-js/react-native/router`에서 가져와 필요한 위치에서 렌더링하세요. 라우팅이 없는 앱은 `@granite-js/react-native/core`에서 등록 API를 가져와 React Navigation을 로드하지 않고 사용할 수 있어요.

Router는 NavigationContainer, native stack, safe-area provider와 뒤로가기 핸들러 범위를 자체적으로 관리해요. 각 Router의 navigation 상태와 뒤로가기 핸들러는 독립적이에요. adapter가 호스트 activity를 전달하며 Router 패키지는 등록 런타임이나 호스트 bridge를 import하지 않아요. NavigationContainer로 감싸거나 Granite에 Router를 등록할 필요가 없어요.

```tsx
import { useColorScheme } from 'react-native';
import { Granite, type InitialProps } from '@granite-js/react-native/core';
import { Router, BackButton } from '@granite-js/react-native/router';
import { closeView, getSchemeUri } from '@granite-js/react-native';
import { context } from '../require.context';

function App({ scheme }: InitialProps) {
const dark = useColorScheme() === 'dark';

return (
<Router
context={context}
prefix="example://app"
initialUrl={scheme}
getInitialUrl={(url) => url ?? getSchemeUri()}
onRootBack={closeView}
defaultScreenOptions={({ routeIndex, onBack }) => ({
headerTintColor: dark ? 'white' : 'black',
title: routeIndex === 0 ? 'Welcome' : 'Details',
...(routeIndex === 0 && {
headerLeft: (props) => <BackButton {...props} onPress={onBack} />,
}),
})}
/>
);
}

export default Granite.registerApp(App, { appName: 'example' });
```

커스텀 테마를 사용한다면 ThemeProvider 아래의 컴포넌트에서 `useTheme()`을 호출하고, 해당 컴포넌트가 Router를 렌더링하도록 구성하세요. 훅은 부모 컴포넌트에서 호출하고 `defaultScreenOptions` 안에서는 호출하지 않아요. 계산된 옵션이 바뀌면 기존 스택과 화면 상태를 유지하면서 반영해요. Router의 React key와 route 컴포넌트 identity를 유지해야 해요.

`defaultScreenOptions`는 객체 또는 함수를 받아요. 함수에는 native-stack의 `route`, `navigation`, `theme`와 다음 값이 전달돼요.

- `routeIndex`: 현재 활성 index가 아닌 해당 route의 0부터 시작하는 스택 위치예요. 같은 페이지를 push해도 서로 다른 위치를 가지며, 아직 스택에 없는 preload route는 `-1`이에요.
- `onBack`: 등록된 핸들러를 거친 뒤 현재 navigation 상태에서 뒤로 이동하거나, 돌아갈 화면이 없으면 `onRootBack`을 호출해요. callback을 받은 route를 직접 제거하지는 않아요.

Router는 호스트 닫기 bridge를 직접 호출하지 않아요. 필요한 앱에서 `onRootBack`으로 연결하세요. 생략하면 루트에서 호스트 동작을 실행하지 않고 기본 루트 뒤로가기 버튼도 표시하지 않아요. Android hardware back도 루트에서 지정한 동작을 사용해요. 호스트의 iOS 제스처 제어는 `setIosSwipeGestureEnabled`, `setiOSBackPressHandler`로 주입할 수 있어요.

`initialUrl`과 선택적인 `getInitialUrl`은 마운트 시 진입 URL을 결정해요. Router는 전역 앱 이름이나 네이티브 bridge에서 자동으로 읽지 않아요. 초기 URL props가 바뀌어도 기존 스택을 초기화하거나 이동하지 않으며, 이후 이동에는 navigation action을 사용해요. `prefix`는 앱에서 사용하는 URL의 scheme/host/path와 일치해야 해요.

기존 단수형 `defaultScreenOption`은 원래 callback 인자를 유지하는 deprecated Router prop이에요. 두 이름을 함께 지정하면 타입 오류가 나고, 타입 검사 없는 호출에서는 복수형만 사용해요. 페이지 `screenOptions`와 기존 `navigation.setOptions`가 우선하며, 후속 화면의 기본 뒤로가기 버튼 동작은 유지해요.

### 자동 등록 방식에서 마이그레이션하기

앱 등록 API의 호환성이 깨지는 변경이에요.

- `context`와 라우팅 설정을 `Granite.registerApp`에서 `<Router>`로 옮기세요.
- 주입되던 `children`을 반환하는 대신 Router를 직접 렌더링하세요. 등록 함수가 Navigator를 넣어주지 않아요.
- 등록 옵션의 `initialScheme`은 Router의 `initialUrl`로 옮기세요. 함수형 URL은 앱이나 `getInitialUrl`에서 계산하세요.
- `prefix`를 명시하고, 호스트 화면을 닫아야 하면 `onRootBack`을 연결하세요.
- 제스처 callback, `screenContainer`, `defaultErrorComponent`는 Router에 전달하세요.
- navigation/back 훅을 쓰는 래퍼는 Router 아래의 `screenContainer` 또는 페이지/레이아웃으로 옮기세요. 옵션 계산에 사용하는 Provider는 Router 위에 둬요.
- 주입된 Navigator를 clone하는 래퍼는 Router의 `defaultScreenOptions`로 대체하세요.

### Router 패키지만 사용하기

호환되는 `@granite-js/native`, `react`, `react-native` peer와 함께 `@granite-js/router`를 설치하세요. `Router`, `createRoute`, navigation/back 훅을 `@granite-js/router`에서 직접 가져오면 Granite 등록 없이 사용할 수 있어요. 독립 Router는 기본적으로 AppState를 사용하고, 호스트가 activity를 관리한다면 `isActive`를 전달해요.

기존 `@granite-js/react-native`의 루트와 `/router` 경로도 새 패키지와 같은 route 정의와 back-handler context를 공유해요. 새로 생성하는 route 타입은 `@granite-js/router`를 확장하고, 이전 `@granite-js/react-native` 타입 확장도 호환돼요. 새 생성 파일이나 직접 import를 사용한다면 앱 dependencies에 `@granite-js/router`를 추가하세요.

내부 navigation 엔진은 React Navigation을 유지해요. 이번 변경은 앱 등록과 Router 구성을 분리하며, 엔진이나 기존 typed-route 정의 API를 교체하지 않아요.

## 레퍼런스

- [React Navigation 공식 문서](https://reactnavigation.org/)
Expand Down
45 changes: 7 additions & 38 deletions docs/ko/reference/react-native/core/Granite.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,16 @@ sourcePath: packages/react-native/src/app/Granite.tsx

# Granite

## 시그니처

```typescript
Granite: {
registerApp(AppContainer: ComponentType<PropsWithChildren<InitialProps>>, { appName, context, router }: GraniteProps): (initialProps: InitialProps) => JSX.Element;
readonly appName: string;
}
```

### 프로퍼티

<ul class="post-parameters-ul">
<li class="post-parameters-li post-parameters-li-root">
<span class="post-parameters--name">registerApp</span><span class="post-parameters--required">필수</span> · <span class="post-parameters--type">RegisterService</span>
<br />
<p class="post-parameters--description">이 함수는 서비스의 기본 환경을 설정해주고, 복잡한 설정을 따로 할 필요 없이 서비스 개발을 빠르게 시작할 수 있게 도와줘요. <code>appName</code>만 전달하면 파일 기반 라우팅, 쿼리 파라미터 처리, 뒤로 가기 제어 등 여러 기능을 바로 사용할 수 있어요.</p>
</li>
</ul>

`Granite.registerApp` 함수가 제공하는 기능은 다음과 같아요.

- 라우팅: 파일 경로에 맞게 URL이 자동으로 매핑돼요. Next.js의 파일 기반 라우팅과 비슷한 방식으로 동작해요. 예를 들어 `/my-service/pages/index.ts` 파일은 `scheme://my-service` 주소로 접근할 수 있고, `/my-service/pages/home.ts` 파일은 `scheme://my-service/home` 주소로 접근할 수 있어요.
- 쿼리 파라미터: URL 스킴으로 전달 받은 쿼리 파라미터를 쉽게 사용할 수 있어요. 예를 들어, `referrer` 파라미터를 받아서 로그를 남길 수 있어요.
- 뒤로 가기 제어: 뒤로 가기 이벤트를 제어할 수 있어요. 예를 들어, 사용자가 화면에서 뒤로 가기를 누르면 다이얼로그를 띄우거나 화면을 닫을 수 있어요.
- 화면 가시성(Visibility): 화면이 사용자에게 보이는지, 가려져 있는지 알 수 있어요. 예를 들어, 사용자가 홈 화면으로 나갔을 때 이 값을 활용해 특정 동작을 처리할 수 있어요.

## 예제

### `Granite` 컴포넌트로 만드는 예제
앱 컴포넌트를 등록하고 네이티브 초기 props와 visibility 생명주기를 제공해요. 라우팅은 선택적이며 앱 안에서 Router를 직접 렌더링해야 해요.

```tsx
import { PropsWithChildren } from 'react';
import { Granite, InitialProps } from '@granite-js/react-native';
import { context } from '../require.context';
import { Granite, type InitialProps } from '@granite-js/react-native/core';

function AppContainer({ children }: PropsWithChildren<InitialProps>) {
return <>{children}</>;
function App(props: InitialProps) {
return <MyContent colorPreference={props.initialColorPreference} />;
}

export default Granite.registerApp(AppContainer, {
appName: 'my-app',
context,
});
export default Granite.registerApp(App, { appName: 'my-app' });
```

[Router 구성과 마이그레이션](/ko/guides/granite-router/routing#앱-안에서-router-렌더링하기)
10 changes: 4 additions & 6 deletions docs/ko/reference/react-native/core/InitialProps.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,18 @@ type InitialProps = AndroidInitialProps | IOSInitialProps;
::: code-group

```tsx [_app.tsx]
import { PropsWithChildren } from 'react';
import { Granite, InitialProps } from '@granite-js/react-native';
import { context } from '../require.context';
import { Text } from 'react-native';
import { Granite, InitialProps } from '@granite-js/react-native/core';

const APP_NAME = 'my-app-name';

function AppContainer({ children, ...initialProps }: PropsWithChildren<InitialProps>) {
function AppContainer(initialProps: InitialProps) {
console.log({ initialProps });
return <>{children}</>;
return <Text>{initialProps.initialColorPreference}</Text>;
}

export default Granite.registerApp(AppContainer, {
appName: APP_NAME,
context,
});
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sourcePath: packages/react-native/src/router/hooks/useIsInitialScreen.ts
sourcePath: packages/router/src/router/hooks/useIsInitialScreen.ts
---

# useIsInitialScreen
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sourcePath: packages/react-native/src/use-back-event/useBackEvent.tsx
sourcePath: packages/router/src/use-back-event/useBackEvent.tsx
---

# useBackEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sourcePath: packages/react-native/src/use-back-event/useBackHandler.tsx
sourcePath: packages/router/src/use-back-event/useBackHandler.tsx
---

# useBackHandler
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sourcePath: packages/react-native/src/router/createRoute.ts
sourcePath: packages/router/src/router/createRoute.ts
---

# useParams
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sourcePath: 'packages/react-native/src/router/components/useRouterBackHandler.tsx'
sourcePath: 'packages/router/src/router/components/useRouterBackHandler.tsx'
---

# useRouterBackHandler
Expand Down
2 changes: 1 addition & 1 deletion docs/ko/reference/react-native/ui/BackButton.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sourcePath: 'packages/react-native/src/router/components/BackButton.tsx'
sourcePath: 'packages/router/src/router/components/BackButton.tsx'
---

# BackButton
Expand Down
Loading
Loading