@daltonr/pathwrite-react-native 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -227
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# @daltonr/pathwrite-react-native
|
|
2
2
|
|
|
3
|
-
React Native adapter for
|
|
4
|
-
|
|
5
|
-
Works with Expo (managed and bare workflow) and bare React Native projects. Targets iOS and Android.
|
|
3
|
+
React Native adapter for Pathwrite — exposes path engine state via `useSyncExternalStore` with stable callbacks, an optional context provider, and an optional `PathShell` default UI built from React Native primitives.
|
|
6
4
|
|
|
7
5
|
## Installation
|
|
8
6
|
|
|
@@ -10,293 +8,191 @@ Works with Expo (managed and bare workflow) and bare React Native projects. Targ
|
|
|
10
8
|
npm install @daltonr/pathwrite-core @daltonr/pathwrite-react-native
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
Peer dependencies: `react >=
|
|
14
|
-
|
|
15
|
-
## Exports
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
import {
|
|
19
|
-
PathShell, // React Native UI shell
|
|
20
|
-
usePath, // Hook — creates a scoped engine
|
|
21
|
-
usePathContext, // Hook — reads the nearest PathProvider
|
|
22
|
-
PathProvider, // Context provider
|
|
23
|
-
// Core types re-exported for convenience:
|
|
24
|
-
PathEngine,
|
|
25
|
-
PathData,
|
|
26
|
-
PathDefinition,
|
|
27
|
-
PathEvent,
|
|
28
|
-
PathSnapshot,
|
|
29
|
-
PathStep,
|
|
30
|
-
PathStepContext,
|
|
31
|
-
StepChoice,
|
|
32
|
-
SerializedPathState,
|
|
33
|
-
} from "@daltonr/pathwrite-react-native";
|
|
34
|
-
```
|
|
11
|
+
Peer dependencies: `react-native >= 0.73.0`, `react >= 18.0.0`
|
|
35
12
|
|
|
36
13
|
---
|
|
37
14
|
|
|
38
|
-
## Quick
|
|
39
|
-
|
|
40
|
-
The fastest way to get started. `PathShell` manages the engine lifecycle, renders the active step, and provides navigation buttons. The default header shows numbered step dots (✓ when completed), the current step title, and a progress bar.
|
|
15
|
+
## Quick start
|
|
41
16
|
|
|
42
17
|
```tsx
|
|
43
|
-
import { PathShell } from "@daltonr/pathwrite-react-native";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import { ReviewStep } from "./ReviewStep";
|
|
18
|
+
import { PathShell, usePathContext } from "@daltonr/pathwrite-react-native";
|
|
19
|
+
import type { PathDefinition, PathData } from "@daltonr/pathwrite-core";
|
|
20
|
+
import { View, Text, TextInput, TouchableOpacity } from "react-native";
|
|
47
21
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<PathShell
|
|
51
|
-
path={myPath}
|
|
52
|
-
initialData={INITIAL_DATA}
|
|
53
|
-
onComplete={(data) => console.log("Done!", data)}
|
|
54
|
-
steps={{
|
|
55
|
-
details: <DetailsStep />,
|
|
56
|
-
review: <ReviewStep />,
|
|
57
|
-
}}
|
|
58
|
-
/>
|
|
59
|
-
);
|
|
22
|
+
interface SignupData extends PathData {
|
|
23
|
+
name: string;
|
|
60
24
|
}
|
|
61
|
-
```
|
|
62
25
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const { snapshot, setData } = usePathContext<MyData>();
|
|
71
|
-
const name = snapshot?.data.name ?? "";
|
|
26
|
+
const signupPath: PathDefinition<SignupData> = {
|
|
27
|
+
id: "signup",
|
|
28
|
+
steps: [
|
|
29
|
+
{ id: "details", title: "Your Details", canMoveNext: ({ data }) => data.name.trim().length >= 2 },
|
|
30
|
+
{ id: "review", title: "Review" },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
72
33
|
|
|
34
|
+
function DetailsStep() {
|
|
35
|
+
const { snapshot, setData } = usePathContext<SignupData>();
|
|
36
|
+
if (!snapshot) return null;
|
|
73
37
|
return (
|
|
74
38
|
<TextInput
|
|
75
|
-
value={name}
|
|
39
|
+
value={snapshot.data.name}
|
|
76
40
|
onChangeText={(text) => setData("name", text)}
|
|
77
41
|
placeholder="Your name"
|
|
78
42
|
/>
|
|
79
43
|
);
|
|
80
44
|
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## Option A — `usePath` hook (component-scoped)
|
|
86
|
-
|
|
87
|
-
Each call creates an isolated engine. Good when a single screen owns the path.
|
|
88
|
-
|
|
89
|
-
```tsx
|
|
90
|
-
import { usePath } from "@daltonr/pathwrite-react-native";
|
|
91
|
-
import { myPath } from "./my-path";
|
|
92
|
-
|
|
93
|
-
function MyScreen() {
|
|
94
|
-
const { snapshot, start, next, previous, setData } = usePath({
|
|
95
|
-
onEvent(event) {
|
|
96
|
-
if (event.type === "completed") console.log("Done!", event.data);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
start(myPath, { name: "" });
|
|
102
|
-
}, []);
|
|
103
45
|
|
|
46
|
+
function ReviewStep() {
|
|
47
|
+
const { snapshot } = usePathContext<SignupData>();
|
|
104
48
|
if (!snapshot) return null;
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<View>
|
|
108
|
-
<Text>{snapshot.stepTitle ?? snapshot.stepId}</Text>
|
|
109
|
-
<Text>Step {snapshot.stepIndex + 1} of {snapshot.stepCount}</Text>
|
|
110
|
-
<Pressable onPress={previous} disabled={snapshot.isNavigating}>
|
|
111
|
-
<Text>Back</Text>
|
|
112
|
-
</Pressable>
|
|
113
|
-
<Pressable onPress={next} disabled={snapshot.isNavigating || !snapshot.canMoveNext}>
|
|
114
|
-
<Text>{snapshot.isLastStep ? "Complete" : "Next"}</Text>
|
|
115
|
-
</Pressable>
|
|
116
|
-
</View>
|
|
117
|
-
);
|
|
49
|
+
return <Text>Signing up as {snapshot.data.name}</Text>;
|
|
118
50
|
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## Option B — `PathProvider` + `usePathContext` (tree-scoped)
|
|
122
|
-
|
|
123
|
-
Share one engine across a component tree — step components read state without prop-drilling.
|
|
124
51
|
|
|
125
|
-
|
|
126
|
-
import { PathProvider } from "@daltonr/pathwrite-react-native";
|
|
127
|
-
|
|
128
|
-
// Root — start the path once, then render children
|
|
129
|
-
function WizardRoot() {
|
|
130
|
-
const { snapshot, start, next } = usePathContext();
|
|
131
|
-
useEffect(() => { start(myPath, {}); }, []);
|
|
132
|
-
return (
|
|
133
|
-
<View>
|
|
134
|
-
{snapshot && <Text>Step {snapshot.stepIndex + 1}</Text>}
|
|
135
|
-
<Pressable onPress={next}><Text>Next</Text></Pressable>
|
|
136
|
-
</View>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Wrap in the provider at the navigation/screen level
|
|
141
|
-
export function WizardScreen() {
|
|
52
|
+
export function SignupFlow() {
|
|
142
53
|
return (
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
54
|
+
<PathShell
|
|
55
|
+
path={signupPath}
|
|
56
|
+
initialData={{ name: "" }}
|
|
57
|
+
onComplete={(data) => console.log("Done!", data)}
|
|
58
|
+
steps={{ details: <DetailsStep />, review: <ReviewStep /> }}
|
|
59
|
+
/>
|
|
146
60
|
);
|
|
147
61
|
}
|
|
148
62
|
```
|
|
149
63
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
## Path definition
|
|
153
|
-
|
|
154
|
-
Path definitions are plain objects — no classes, no decorators, framework-agnostic.
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
import type { PathDefinition } from "@daltonr/pathwrite-react-native";
|
|
158
|
-
|
|
159
|
-
interface MyData {
|
|
160
|
-
name: string;
|
|
161
|
-
agreed: boolean;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export const myPath: PathDefinition<MyData> = {
|
|
165
|
-
id: "onboarding",
|
|
166
|
-
steps: [
|
|
167
|
-
{
|
|
168
|
-
id: "name",
|
|
169
|
-
title: "Your Name",
|
|
170
|
-
canMoveNext: ({ data }) => data.name.trim().length >= 2,
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
id: "terms",
|
|
174
|
-
title: "Terms",
|
|
175
|
-
canMoveNext: ({ data }) => data.agreed,
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
id: "done",
|
|
179
|
-
title: "All Done",
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
};
|
|
183
|
-
```
|
|
64
|
+
Step components call `usePathContext()` to access engine state — no prop drilling needed. `<PathShell>` provides the context automatically.
|
|
184
65
|
|
|
185
66
|
---
|
|
186
67
|
|
|
187
|
-
##
|
|
68
|
+
## Metro config
|
|
188
69
|
|
|
189
|
-
|
|
70
|
+
Metro does not follow symlinks by default, so workspace packages installed above the app root are invisible to the bundler. This is the most common setup issue when using Pathwrite in a monorepo. Create or update `metro.config.js` in your React Native or Expo app:
|
|
190
71
|
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```
|
|
72
|
+
```js
|
|
73
|
+
// metro.config.js
|
|
74
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
75
|
+
// For bare React Native: const { getDefaultConfig } = require("@react-native/metro-config");
|
|
76
|
+
const path = require("path");
|
|
197
77
|
|
|
198
|
-
|
|
78
|
+
const projectRoot = __dirname;
|
|
79
|
+
const workspaceRoot = path.resolve(projectRoot, "../../.."); // adjust depth to your repo
|
|
199
80
|
|
|
200
|
-
|
|
81
|
+
const config = getDefaultConfig(projectRoot);
|
|
201
82
|
|
|
202
|
-
|
|
83
|
+
// 1. Watch workspace source files outside the app root.
|
|
84
|
+
config.watchFolders = [workspaceRoot];
|
|
203
85
|
|
|
204
|
-
|
|
205
|
-
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{ id: "address-ie", title: "Irish Address" },
|
|
211
|
-
],
|
|
212
|
-
}
|
|
213
|
-
```
|
|
86
|
+
// 2. Map package names directly to their source directories.
|
|
87
|
+
config.resolver.extraNodeModules = {
|
|
88
|
+
"@daltonr/pathwrite-core": path.resolve(workspaceRoot, "packages/core"),
|
|
89
|
+
"@daltonr/pathwrite-react-native": path.resolve(workspaceRoot, "packages/react-native-adapter"),
|
|
90
|
+
// Add any other workspace packages your app imports here.
|
|
91
|
+
};
|
|
214
92
|
|
|
215
|
-
|
|
93
|
+
// 3. Restrict node_modules lookup to the app's own folder.
|
|
94
|
+
config.resolver.nodeModulesPaths = [
|
|
95
|
+
path.resolve(projectRoot, "node_modules"),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// 4. Pin react/react-native/scheduler to the app's own copies.
|
|
99
|
+
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
100
|
+
if (
|
|
101
|
+
moduleName === "react" || moduleName.startsWith("react/") ||
|
|
102
|
+
moduleName === "react-native" || moduleName.startsWith("react-native/") ||
|
|
103
|
+
moduleName === "scheduler" || moduleName.startsWith("scheduler/")
|
|
104
|
+
) {
|
|
105
|
+
try {
|
|
106
|
+
return { filePath: require.resolve(moduleName, { paths: [projectRoot] }), type: "sourceFile" };
|
|
107
|
+
} catch { /* fall through */ }
|
|
108
|
+
}
|
|
109
|
+
return context.resolveRequest(context, moduleName, platform);
|
|
110
|
+
};
|
|
216
111
|
|
|
217
|
-
|
|
218
|
-
<PathShell
|
|
219
|
-
path={myPath}
|
|
220
|
-
steps={{
|
|
221
|
-
name: <NameStep />,
|
|
222
|
-
"address-us": <USAddressStep />,
|
|
223
|
-
"address-ie": <IrishAddressStep />,
|
|
224
|
-
done: <DoneStep />,
|
|
225
|
-
}}
|
|
226
|
-
/>
|
|
112
|
+
module.exports = config;
|
|
227
113
|
```
|
|
228
114
|
|
|
229
|
-
|
|
115
|
+
Every new workspace package your app imports must be added to `extraNodeModules`. After changing this file, restart Metro with `npx expo start --clear` (or `npx react-native start --reset-cache` for bare RN).
|
|
230
116
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
Push a sub-path onto the stack from within a step. The parent path resumes from the same step when the sub-path completes.
|
|
117
|
+
---
|
|
234
118
|
|
|
235
|
-
|
|
236
|
-
const { startSubPath } = usePathContext();
|
|
119
|
+
## usePath / PathShell
|
|
237
120
|
|
|
238
|
-
|
|
239
|
-
startSubPath(subWizardPath, {});
|
|
240
|
-
}
|
|
241
|
-
```
|
|
121
|
+
The API is identical to `@daltonr/pathwrite-react`. `usePath` creates an engine instance scoped to the calling component; `usePathContext` reads the nearest `PathShell` or `PathProvider` ancestor.
|
|
242
122
|
|
|
243
|
-
|
|
123
|
+
### usePath return value
|
|
244
124
|
|
|
245
|
-
|
|
125
|
+
| Field | Type | Description |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `snapshot` | `PathSnapshot \| null` | Current state. `null` when no path is active. |
|
|
128
|
+
| `start(definition, data?)` | function | Start a path. |
|
|
129
|
+
| `next()` | function | Advance one step. Completes on the last step. |
|
|
130
|
+
| `previous()` | function | Go back one step. |
|
|
131
|
+
| `cancel()` | function | Cancel the active path or sub-path. |
|
|
132
|
+
| `goToStep(stepId)` | function | Jump to a step by ID, bypassing guards. |
|
|
133
|
+
| `goToStepChecked(stepId)` | function | Jump to a step by ID, checking the current step's guard first. |
|
|
134
|
+
| `setData(key, value)` | function | Update a single data field. Type-checked when `TData` is provided. |
|
|
135
|
+
| `resetStep()` | function | Restore data to step-entry state. |
|
|
136
|
+
| `startSubPath(definition, data?, meta?)` | function | Push a sub-path. |
|
|
137
|
+
| `restart(definition, data?)` | function | Tear down any active path and start fresh. |
|
|
138
|
+
|
|
139
|
+
All returned callbacks are referentially stable.
|
|
140
|
+
|
|
141
|
+
### PathShell props
|
|
246
142
|
|
|
247
143
|
| Prop | Type | Default | Description |
|
|
248
144
|
|---|---|---|---|
|
|
249
145
|
| `path` | `PathDefinition` | required | The path to drive. |
|
|
250
|
-
| `steps` | `Record<string, ReactNode>` | required | Map of step ID
|
|
146
|
+
| `steps` | `Record<string, ReactNode>` | required | Map of step ID to content. Keys must exactly match step IDs. |
|
|
251
147
|
| `initialData` | `PathData` | `{}` | Initial data passed to `engine.start()`. |
|
|
252
|
-
| `engine` | `PathEngine` | — | Externally
|
|
148
|
+
| `engine` | `PathEngine` | — | Externally-managed engine (e.g. from `restoreOrStart()`). |
|
|
253
149
|
| `autoStart` | `boolean` | `true` | Start the path automatically on mount. |
|
|
254
|
-
| `onComplete` | `(data) => void` | — | Called when the path completes. |
|
|
255
|
-
| `onCancel` | `(data) => void` | — | Called when the path is cancelled. |
|
|
256
|
-
| `onEvent` | `(event) => void` | — | Called for every engine event. |
|
|
150
|
+
| `onComplete` | `(data: PathData) => void` | — | Called when the path completes. |
|
|
151
|
+
| `onCancel` | `(data: PathData) => void` | — | Called when the path is cancelled. |
|
|
152
|
+
| `onEvent` | `(event: PathEvent) => void` | — | Called for every engine event. |
|
|
257
153
|
| `backLabel` | `string` | `"Previous"` | Label for the back button. |
|
|
258
154
|
| `nextLabel` | `string` | `"Next"` | Label for the next button. |
|
|
259
155
|
| `completeLabel` | `string` | `"Complete"` | Label for the next button on the last step. |
|
|
260
156
|
| `cancelLabel` | `string` | `"Cancel"` | Label for the cancel button. |
|
|
261
157
|
| `hideCancel` | `boolean` | `false` | Hide the cancel button. |
|
|
262
|
-
| `hideProgress` | `boolean` | `false` | Hide the progress header
|
|
263
|
-
| `disableBodyScroll` | `boolean` | `false` | Replace the `ScrollView` body
|
|
264
|
-
| `footerLayout` | `"wizard" \| "form" \| "auto"` | `"auto"` | `"wizard"
|
|
158
|
+
| `hideProgress` | `boolean` | `false` | Hide the progress header. Also hidden automatically for single-step paths. |
|
|
159
|
+
| `disableBodyScroll` | `boolean` | `false` | Replace the `ScrollView` body with a plain `View`. Use when a step contains a `FlatList` or other virtualized list. |
|
|
160
|
+
| `footerLayout` | `"wizard" \| "form" \| "auto"` | `"auto"` | `"wizard"`: Back on left. `"form"`: Cancel on left, no Back. |
|
|
265
161
|
| `renderHeader` | `(snapshot) => ReactNode` | — | Replace the default progress header entirely. |
|
|
266
|
-
| `renderFooter` | `(snapshot, actions) => ReactNode` | — | Replace the default
|
|
162
|
+
| `renderFooter` | `(snapshot, actions) => ReactNode` | — | Replace the default navigation buttons. |
|
|
267
163
|
| `style` | `StyleProp<ViewStyle>` | — | Override for the root `View`. |
|
|
268
164
|
|
|
269
|
-
|
|
165
|
+
### PathShellHandle and the restart() ref pattern
|
|
270
166
|
|
|
271
|
-
|
|
167
|
+
`PathShell` is a `forwardRef` component that exposes a `PathShellHandle`. Use a ref to call `restart()` imperatively from outside the shell — for example, from a parent screen's header button:
|
|
272
168
|
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
onEvent?: (event: PathEvent) => void;
|
|
277
|
-
})
|
|
278
|
-
```
|
|
169
|
+
```tsx
|
|
170
|
+
import { useRef } from "react";
|
|
171
|
+
import { PathShell, PathShellHandle } from "@daltonr/pathwrite-react-native";
|
|
279
172
|
|
|
280
|
-
|
|
173
|
+
export function OnboardingScreen() {
|
|
174
|
+
const shellRef = useRef<PathShellHandle>(null);
|
|
281
175
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
| `resetStep` | `() => void` | Restore data to step-entry state. |
|
|
294
|
-
| `restart` | `(path, data?) => void` | Tear down and restart fresh. |
|
|
176
|
+
return (
|
|
177
|
+
<PathShell
|
|
178
|
+
ref={shellRef}
|
|
179
|
+
path={myPath}
|
|
180
|
+
initialData={{ name: "" }}
|
|
181
|
+
onComplete={(data) => console.log(data)}
|
|
182
|
+
steps={{ name: <NameStep /> }}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
295
187
|
|
|
296
188
|
---
|
|
297
189
|
|
|
298
|
-
##
|
|
190
|
+
## Further reading
|
|
299
191
|
|
|
300
|
-
|
|
192
|
+
- [React Native getting started guide](../../docs/getting-started/frameworks/react-native.md)
|
|
193
|
+
- [Navigation guide](../../docs/guides/navigation.md)
|
|
194
|
+
- [Full docs](../../docs/README.md)
|
|
195
|
+
|
|
196
|
+
---
|
|
301
197
|
|
|
302
|
-
|
|
198
|
+
© 2026 Devjoy Ltd. MIT License.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daltonr/pathwrite-react-native",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "React Native adapter for @daltonr/pathwrite-core — hooks, context provider, and optional PathShell default UI.",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"react-native": ">=0.72.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@daltonr/pathwrite-core": "^0.10.
|
|
47
|
+
"@daltonr/pathwrite-core": "^0.10.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"react": "^18.3.1",
|