@rific/splash-gate 0.1.2 → 0.2.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 +31 -5
- package/dist/index.d.mts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +8 -4
- package/dist/index.mjs +7 -3
- package/package.json +10 -12
- package/src/index.ts +2 -2
- package/src/splashGate.ts +28 -4
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Name every async condition your Expo app's first screen actually depends on (a l
|
|
|
4
4
|
|
|
5
5
|
## Why
|
|
6
6
|
|
|
7
|
-
`SplashScreen.preventAutoHideAsync()` only holds the splash up. It's on you to call `hideAsync()` at the right moment. Call it too early (e.g. the instant your theme loads) and anything else your first screen depends on but forgot to wait for, an icon font, a hydrated preference, pops in visibly a moment after the splash lifts. `
|
|
7
|
+
`SplashScreen.preventAutoHideAsync()` only holds the splash up. It's on you to call `hideAsync()` at the right moment. Call it too early (e.g. the instant your theme loads) and anything else your first screen depends on but forgot to wait for, an icon font, a hydrated preference, pops in visibly a moment after the splash lifts. `createGate` makes every condition explicit up front, so nothing can be forgotten silently.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -17,9 +17,9 @@ npm install @rific/splash-gate
|
|
|
17
17
|
```ts
|
|
18
18
|
// splashGate.ts, created once, at module scope, listing every condition this app's first
|
|
19
19
|
// screen actually depends on
|
|
20
|
-
import {
|
|
20
|
+
import { createGate } from '@rific/splash-gate'
|
|
21
21
|
|
|
22
|
-
export const { markReady, useReady, pendingGates } =
|
|
22
|
+
export const { markReady, useReady, pendingGates, Gate } = createGate(['theme', 'fonts', 'keyboardLayout'])
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
```tsx
|
|
@@ -44,10 +44,11 @@ export const Theme = ({ children }) => {
|
|
|
44
44
|
// KeyboardLayoutProvider.tsx
|
|
45
45
|
import { useReady } from './splashGate'
|
|
46
46
|
|
|
47
|
+
const [layout, setLayout] = useState<string | null>(null)
|
|
47
48
|
const [loaded, setLoaded] = useState(false)
|
|
48
49
|
useEffect(() => {
|
|
49
50
|
void AsyncStorage.getItem(STORAGE_KEY).then((stored) => {
|
|
50
|
-
if (stored)
|
|
51
|
+
if (stored) setLayout(stored)
|
|
51
52
|
setLoaded(true) // mark loaded whether or not a saved value was actually found
|
|
52
53
|
})
|
|
53
54
|
}, [])
|
|
@@ -56,14 +57,39 @@ useReady('keyboardLayout', loaded)
|
|
|
56
57
|
|
|
57
58
|
The splash screen hides exactly once, the moment `theme`, `fonts`, and `keyboardLayout` have all reported ready, in whatever order they actually resolve.
|
|
58
59
|
|
|
60
|
+
## Guarding against a child that renders before its data does
|
|
61
|
+
|
|
62
|
+
`useReady` only defers when the splash screen *hides* — it says nothing about when your own children first *render*. That's fine for a plain condition like `fontsLoaded`, but it's a trap for a component that mounts a third-party provider whose internal state locks in on first render (a `useState(() => ...)` lazy initializer wrapping a loaded prop, which is how most context providers are written). Render that provider immediately with a placeholder default and patch the real prop in once it loads, and the provider never notices the patch — it already locked onto the placeholder, permanently, even though the gate reports in correctly and the splash hides at the right moment. From the outside this looks exactly like the setting silently failing to persist.
|
|
63
|
+
|
|
64
|
+
`Gate` closes that gap: it registers the gate exactly like `useReady` does, and in the same step withholds `children` until `ready` is `true`, so a gate can't be reported without also blocking whatever depends on it from mounting too early.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// KeyboardLayoutProvider.tsx
|
|
68
|
+
import { Gate } from './splashGate' // from createGate(['theme', 'fonts', 'keyboardLayout'])
|
|
69
|
+
|
|
70
|
+
const [layout, setLayout] = useState<string | null>(null) // null = not loaded yet
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
void AsyncStorage.getItem(STORAGE_KEY).then((stored) => setLayout(stored ?? 'qwerty'))
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
// ThirdPartyLayoutProvider's own `layout` state is a lazy useState(() => initialLayout) — it only
|
|
76
|
+
// reads `initialLayout` on its very first mount, so it must not mount until the real value exists.
|
|
77
|
+
return (
|
|
78
|
+
<Gate gate='keyboardLayout' ready={layout !== null}>
|
|
79
|
+
<ThirdPartyLayoutProvider initialLayout={layout}>{children}</ThirdPartyLayoutProvider>
|
|
80
|
+
</Gate>
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
59
84
|
## API
|
|
60
85
|
|
|
61
|
-
### `
|
|
86
|
+
### `createGate(gates)`
|
|
62
87
|
|
|
63
88
|
Takes an array of gate names (typically `as const` for literal-type safety) and returns:
|
|
64
89
|
|
|
65
90
|
- `markReady(gate)`: marks one gate ready. Safe to call more than once for the same gate, and safe in any order. Hides the splash screen exactly once, once every gate is ready. For a gate that only resolves via a one-shot callback (a library's own `onReady` prop, a promise `.then`), call this directly there.
|
|
66
91
|
- `useReady(gate, ready)`: for a gate whose readiness is already a plain boolean (state from a hook like `useFonts`, or your own derived condition), marks it ready once `ready` becomes `true`. Bound to this gate instance, so there's nothing to pass but the two things that actually vary at each call site.
|
|
92
|
+
- `Gate`: a component, bound to this gate instance. `useReady(gate, ready)` plus render-gating in one step — see above. Renders `children` once `ready` is `true`, otherwise `fallback` (default `null`).
|
|
67
93
|
- `pendingGates()`: the gate names still outstanding, for a debug log during development.
|
|
68
94
|
|
|
69
95
|
Call this once, at module scope, not inside a component body, or you'll get a fresh gate (and a fresh splash-hide race) on every render.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface GateProps<T extends string> {
|
|
4
|
+
gate: T;
|
|
5
|
+
ready: boolean;
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
1
9
|
type SplashGate<T extends string> = {
|
|
2
10
|
markReady: (gate: T) => void;
|
|
3
11
|
useReady: (gate: T, ready: boolean) => void;
|
|
4
12
|
pendingGates: () => T[];
|
|
13
|
+
Gate: (props: GateProps<T>) => ReactNode;
|
|
5
14
|
};
|
|
6
|
-
declare function
|
|
15
|
+
declare function createGate<T extends string>(gates: readonly T[]): SplashGate<T>;
|
|
7
16
|
|
|
8
|
-
export { type SplashGate,
|
|
17
|
+
export { type GateProps, type SplashGate, createGate };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface GateProps<T extends string> {
|
|
4
|
+
gate: T;
|
|
5
|
+
ready: boolean;
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
1
9
|
type SplashGate<T extends string> = {
|
|
2
10
|
markReady: (gate: T) => void;
|
|
3
11
|
useReady: (gate: T, ready: boolean) => void;
|
|
4
12
|
pendingGates: () => T[];
|
|
13
|
+
Gate: (props: GateProps<T>) => ReactNode;
|
|
5
14
|
};
|
|
6
|
-
declare function
|
|
15
|
+
declare function createGate<T extends string>(gates: readonly T[]): SplashGate<T>;
|
|
7
16
|
|
|
8
|
-
export { type SplashGate,
|
|
17
|
+
export { type GateProps, type SplashGate, createGate };
|
package/dist/index.js
CHANGED
|
@@ -30,14 +30,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
33
|
+
createGate: () => createGate
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
|
|
37
37
|
// src/splashGate.ts
|
|
38
38
|
var SplashScreen = __toESM(require("expo-splash-screen"));
|
|
39
39
|
var import_react = require("react");
|
|
40
|
-
function
|
|
40
|
+
function createGate(gates) {
|
|
41
41
|
const pending = new Set(gates);
|
|
42
42
|
let hidden = false;
|
|
43
43
|
const tryHide = () => {
|
|
@@ -55,10 +55,14 @@ function createSplashGate(gates) {
|
|
|
55
55
|
}, [ready, gate]);
|
|
56
56
|
};
|
|
57
57
|
const pendingGates = () => Array.from(pending);
|
|
58
|
+
const Gate = ({ gate, ready, fallback = null, children }) => {
|
|
59
|
+
useReady(gate, ready);
|
|
60
|
+
return ready ? children : fallback;
|
|
61
|
+
};
|
|
58
62
|
tryHide();
|
|
59
|
-
return { markReady, useReady, pendingGates };
|
|
63
|
+
return { markReady, useReady, pendingGates, Gate };
|
|
60
64
|
}
|
|
61
65
|
// Annotate the CommonJS export names for ESM import in node:
|
|
62
66
|
0 && (module.exports = {
|
|
63
|
-
|
|
67
|
+
createGate
|
|
64
68
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/splashGate.ts
|
|
2
2
|
import * as SplashScreen from "expo-splash-screen";
|
|
3
3
|
import { useEffect } from "react";
|
|
4
|
-
function
|
|
4
|
+
function createGate(gates) {
|
|
5
5
|
const pending = new Set(gates);
|
|
6
6
|
let hidden = false;
|
|
7
7
|
const tryHide = () => {
|
|
@@ -19,9 +19,13 @@ function createSplashGate(gates) {
|
|
|
19
19
|
}, [ready, gate]);
|
|
20
20
|
};
|
|
21
21
|
const pendingGates = () => Array.from(pending);
|
|
22
|
+
const Gate = ({ gate, ready, fallback = null, children }) => {
|
|
23
|
+
useReady(gate, ready);
|
|
24
|
+
return ready ? children : fallback;
|
|
25
|
+
};
|
|
22
26
|
tryHide();
|
|
23
|
-
return { markReady, useReady, pendingGates };
|
|
27
|
+
return { markReady, useReady, pendingGates, Gate };
|
|
24
28
|
}
|
|
25
29
|
export {
|
|
26
|
-
|
|
30
|
+
createGate
|
|
27
31
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rific/splash-gate",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Name every async condition an Expo app's first screen depends on (theme, fonts, saved preferences, auth, ...) and hold the splash screen up until all of them report ready — instead of hand-rolling a fresh ad hoc gate per project",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"expo",
|
|
@@ -41,29 +41,28 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
43
43
|
"build:watch": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
44
|
+
"ci": "npm run lint && npm test && npm run typecheck && npm run build",
|
|
44
45
|
"fix": "eslint --fix",
|
|
45
46
|
"lint": "eslint",
|
|
46
47
|
"prepublishOnly": "npm run build",
|
|
47
48
|
"release": "git push --follow-tags",
|
|
48
|
-
"release:major": "npm version major &&
|
|
49
|
-
"release:minor": "npm version minor &&
|
|
50
|
-
"release:patch": "npm version patch &&
|
|
49
|
+
"release:major": "npm version major && npm run release",
|
|
50
|
+
"release:minor": "npm version minor && npm run release",
|
|
51
|
+
"release:patch": "npm version patch && npm run release",
|
|
51
52
|
"test": "jest",
|
|
52
53
|
"test:watch": "jest --watchAll",
|
|
53
54
|
"typecheck": "tsc --noEmit",
|
|
54
|
-
"preversion": "npm run
|
|
55
|
+
"preversion": "npm run ci"
|
|
55
56
|
},
|
|
57
|
+
"prettier": "@infinitetoken/eslint-config/prettier",
|
|
56
58
|
"devDependencies": {
|
|
59
|
+
"@infinitetoken/eslint-config": "^0.1.4",
|
|
60
|
+
"@infinitetoken/tsconfig": "^0.1.1",
|
|
57
61
|
"@testing-library/dom": "^10.4.1",
|
|
58
62
|
"@testing-library/react": "^16.3.2",
|
|
59
63
|
"@types/jest": "^30.0.0",
|
|
60
64
|
"@types/react": "^19.0.0",
|
|
61
|
-
"@typescript-eslint/parser": "^8.59.3",
|
|
62
65
|
"eslint": "^9.39.4",
|
|
63
|
-
"eslint-config-prettier": "^10.1.8",
|
|
64
|
-
"eslint-plugin-package-json": "^1.0.0",
|
|
65
|
-
"eslint-plugin-prettier": "^5.5.5",
|
|
66
|
-
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
67
66
|
"expo-splash-screen": "^57.0.5",
|
|
68
67
|
"jest": "^30.4.2",
|
|
69
68
|
"jest-environment-jsdom": "^30.4.1",
|
|
@@ -73,8 +72,7 @@
|
|
|
73
72
|
"ts-jest": "^29.4.9",
|
|
74
73
|
"ts-node": "^10.9.2",
|
|
75
74
|
"tsup": "^8.0.0",
|
|
76
|
-
"typescript": "^6.0.3"
|
|
77
|
-
"typescript-eslint": "^8.59.3"
|
|
75
|
+
"typescript": "^6.0.3"
|
|
78
76
|
},
|
|
79
77
|
"peerDependencies": {
|
|
80
78
|
"expo-splash-screen": ">=57.0.0",
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { SplashGate } from './splashGate'
|
|
2
|
-
export {
|
|
1
|
+
export type { GateProps, SplashGate } from './splashGate'
|
|
2
|
+
export { createGate } from './splashGate'
|
package/src/splashGate.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import * as SplashScreen from 'expo-splash-screen'
|
|
2
|
-
import { useEffect } from 'react'
|
|
2
|
+
import { ReactNode, useEffect } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface GateProps<T extends string> {
|
|
5
|
+
gate: T
|
|
6
|
+
ready: boolean
|
|
7
|
+
// Rendered in place of `children` while `ready` is false. Defaults to null — the common case is
|
|
8
|
+
// that whatever this wraps has nothing sensible to show until its data exists (see Gate below),
|
|
9
|
+
// and the splash screen is still up for that entire window anyway, so there's normally nothing
|
|
10
|
+
// to fill this with.
|
|
11
|
+
fallback?: ReactNode
|
|
12
|
+
children: ReactNode
|
|
13
|
+
}
|
|
3
14
|
|
|
4
15
|
export type SplashGate<T extends string> = {
|
|
5
16
|
// Marks one named gate ready. Safe to call more than once for the same gate (a no-op after the
|
|
@@ -15,6 +26,14 @@ export type SplashGate<T extends string> = {
|
|
|
15
26
|
// Gate names still outstanding, in no particular order. For a debug log or a "why is this still
|
|
16
27
|
// up" check during development, not something app code should need to branch on.
|
|
17
28
|
pendingGates: () => T[]
|
|
29
|
+
// useReady only defers when the splash screen *hides* — it says nothing about when your own
|
|
30
|
+
// children first *render*. A component that mounts a child whose state locks in on first render
|
|
31
|
+
// (a lazy useState(() => ...) initializer wrapping a loaded value, as most third-party providers
|
|
32
|
+
// do) can already be stuck on stale defaults by the time useReady's effect even runs, even though
|
|
33
|
+
// the gate itself reports in correctly and the splash stays up for the right duration. Gate
|
|
34
|
+
// closes that gap: it registers the gate (so the splash still waits on it) *and* withholds
|
|
35
|
+
// `children` until `ready` is true, in one call, so those two things can't drift apart.
|
|
36
|
+
Gate: (props: GateProps<T>) => ReactNode
|
|
18
37
|
}
|
|
19
38
|
|
|
20
39
|
// One named condition per thing your first screen actually depends on (a loaded theme, a loaded
|
|
@@ -25,7 +44,7 @@ export type SplashGate<T extends string> = {
|
|
|
25
44
|
// until every named gate has reported in, then it hides itself exactly once, so nothing your
|
|
26
45
|
// app's first screen shows can ever be caught mid-load, wrong-then-corrected, a beat behind the
|
|
27
46
|
// rest.
|
|
28
|
-
export function
|
|
47
|
+
export function createGate<T extends string>(gates: readonly T[]): SplashGate<T> {
|
|
29
48
|
const pending = new Set<T>(gates)
|
|
30
49
|
let hidden = false
|
|
31
50
|
|
|
@@ -42,7 +61,7 @@ export function createSplashGate<T extends string>(gates: readonly T[]): SplashG
|
|
|
42
61
|
|
|
43
62
|
const useReady = (gate: T, ready: boolean) => {
|
|
44
63
|
// markReady isn't in this dep array. It's this closure's own stable reference (one per
|
|
45
|
-
//
|
|
64
|
+
// createGate call), not a per-render value, so there's nothing to gain by re-running
|
|
46
65
|
// this effect if it somehow changed identity.
|
|
47
66
|
useEffect(() => {
|
|
48
67
|
if (ready) markReady(gate)
|
|
@@ -51,10 +70,15 @@ export function createSplashGate<T extends string>(gates: readonly T[]): SplashG
|
|
|
51
70
|
|
|
52
71
|
const pendingGates = () => Array.from(pending)
|
|
53
72
|
|
|
73
|
+
const Gate = ({ gate, ready, fallback = null, children }: GateProps<T>) => {
|
|
74
|
+
useReady(gate, ready)
|
|
75
|
+
return ready ? children : fallback
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
// Nothing to wait for (called with an empty list, or every gate was already satisfied some
|
|
55
79
|
// other way). Hide right away instead of leaving the splash up forever with nothing left that
|
|
56
80
|
// could ever call markReady again.
|
|
57
81
|
tryHide()
|
|
58
82
|
|
|
59
|
-
return { markReady, useReady, pendingGates }
|
|
83
|
+
return { markReady, useReady, pendingGates, Gate }
|
|
60
84
|
}
|