@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 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. `createSplashGate` makes every condition explicit up front, so nothing can be forgotten silently.
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 { createSplashGate } from '@rific/splash-gate'
20
+ import { createGate } from '@rific/splash-gate'
21
21
 
22
- export const { markReady, useReady, pendingGates } = createSplashGate(['theme', 'fonts', 'keyboardLayout'])
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) setLayoutState(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
- ### `createSplashGate(gates)`
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 createSplashGate<T extends string>(gates: readonly T[]): SplashGate<T>;
15
+ declare function createGate<T extends string>(gates: readonly T[]): SplashGate<T>;
7
16
 
8
- export { type SplashGate, createSplashGate };
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 createSplashGate<T extends string>(gates: readonly T[]): SplashGate<T>;
15
+ declare function createGate<T extends string>(gates: readonly T[]): SplashGate<T>;
7
16
 
8
- export { type SplashGate, createSplashGate };
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
- createSplashGate: () => createSplashGate
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 createSplashGate(gates) {
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
- createSplashGate
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 createSplashGate(gates) {
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
- createSplashGate
30
+ createGate
27
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/splash-gate",
3
- "version": "0.1.2",
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 && git push --follow-tags",
49
- "release:minor": "npm version minor && git push --follow-tags",
50
- "release:patch": "npm version patch && git push --follow-tags",
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 lint && npm test && npm run build"
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 { createSplashGate } from './splashGate'
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 createSplashGate<T extends string>(gates: readonly T[]): SplashGate<T> {
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
- // createSplashGate call), not a per-render value, so there's nothing to gain by re-running
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
  }