Skip to content

no-use-before-define#152

Open
thomasttvo wants to merge 6 commits intothomas/functionalfrom
thomas/no-use-before-define
Open

no-use-before-define#152
thomasttvo wants to merge 6 commits intothomas/functionalfrom
thomas/no-use-before-define

Conversation

@thomasttvo
Copy link
Copy Markdown
Collaborator

@thomasttvo thomasttvo commented Dec 24, 2025

Reanimated has a hard requirement that a worklet must be defined before the worklet that calls it is. Otherwise, the app will crash. At the same time there's no linter that would apply the rule only for worklets, so we have to turn it on for everything. Quite inconvenient, but gets us one step toward reanimated for now, and can be revisited for future improvements.

There's no logic change in this PR. Only functions getting moved around.


Note

Refactors and reorders code to comply with no-use-before-define, extracting shared logic into hooks/helpers and adjusting call sites without changing behavior.

  • Introduces useZoomSubject (measurement) and useDebugPoints (pinch debug) hooks, and moves zoom step calc to helper/getNextZoomStep
  • Updates ReactNativeZoomableView to use new hooks, replaces inline helpers, and reorders pan/gesture handlers and setup
  • Minor formatting and eslint-disable annotations in AnimatedTouchFeedback, StaticPin, and debug helpers; map files added

Written by Cursor Bugbot for commit 16483a8. Configure here.

thomasttvo and others added 2 commits December 22, 2025 11:39
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> Enforces import sorting via eslint-plugin-simple-import-sort and
updates import order across the codebase.
> 
> - **Tooling/ESLint**:
> - Add `eslint-plugin-simple-import-sort` and enable
`simple-import-sort/imports` and `simple-import-sort/exports` rules in
`package.json`.
> - **Codebase**:
> - Reorder and group imports in `example/App.tsx`,
`src/ReactNativeZoomableView.tsx`, `src/components/StaticPin.tsx`,
`src/debugHelper/index.tsx`, `src/helper/index.ts`,
`src/animations/index.ts`, `src/index.tsx`, and `src/typings/index.ts`.
>   - Minor export order adjustments in `src/index.tsx`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
b5f576e. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: thomasvo <thomas.vo@openspace.ai>
@thomasttvo thomasttvo changed the base branch from master to thomas/functional December 24, 2025 19:58
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> Upgrade to React Native 0.79 with Metro 0.82 and updated tooling, plus
import/export reordering and regenerated artifacts.
> 
> - **Build/Tooling**:
> - Upgrade to React Native `0.79.7` (Metro `0.82.x`, new CLI/debug
middleware, Gradle/codegen updates, Hermes/terser stack).
> - Bump Jest/Babel/tooling (e.g., `babel-jest` 29.x), add
`eslint-plugin-simple-import-sort`, and raise peer `react-native` to
`>=0.79.0`.
> - **Codebase tweaks**:
> - Reorder/normalize imports and exports across `src/`, `lib/`, and
module files to match new lint rules; regenerate sourcemaps.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e105c42. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: thomasvo <thomas.vo@openspace.ai>
@thomasttvo thomasttvo added the enhancement New feature or request label Jan 2, 2026
@thomasttvo
Copy link
Copy Markdown
Collaborator Author

bugbot run

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no bugs!


Comment @cursor review or bugbot run to trigger another review on this PR

@thomasttvo
Copy link
Copy Markdown
Collaborator Author

@claude please review this PR

@thomasttvo
Copy link
Copy Markdown
Collaborator Author

@claude review

thomasttvo and others added 3 commits April 13, 2026 15:18
## Summary
- Removes lib/ from git tracking to avoid merge conflicts
- Moves build step from pre-commit hook to CI/CD
- PR checks now verify builds succeed
- GitHub Actions handles build + npm publish on release

## Changes
- Remove `bob build` from pre-commit hook
- Add lib/ to .gitignore
- Update CI workflow to build on PRs
- Create release workflow to build and publish to npm
- Configure release-it to skip npm publish (GitHub Actions handles it)
- Update CONTRIBUTING.md with new process

## Release Process
1. Run `yarn release` locally (creates tag + GitHub release)
2. GitHub Actions automatically builds and publishes to npm

Requires `NPM_TOKEN` secret to be set in GitHub repo settings.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Implements CI/CD for builds and releases, and removes built output
from the repo.
> 
> - **Checks workflow**: Updates `.github/workflows/lint.yml` to run on
`push` (master) and `pull_request`; sets up Node 16 with yarn cache;
installs deps; runs TypeScript, ESLint, and `react-native-builder-bob`
build
> - **Release workflow**: Adds `.github/workflows/release.yml` to build
and `npm publish` on GitHub Release creation (uses `NPM_TOKEN`)
> - **Changelog config**: Adds `.github/release.yml` with label-based
release notes categories/exclusions
> - **Repo hygiene**: Adds `lib/` to `.gitignore` and removes committed
`lib/commonjs/*` build artifacts
> - **Docs**: Updates `CONTRIBUTING.md` and `README.md` with new build,
release, and changelog process
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
205b2df. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an automated Release workflow to publish packages on GitHub
release events.

* **Chores**
* CI now runs on PRs and master pushes, uses Node 20, enables Yarn
cache, enforces frozen installs, and includes a build check; added build
output to .gitignore and simplified pre-commit hooks; package scripts
updated.

* **Documentation**
* CONTRIBUTING clarified build/release process and publishing
requirements.

* **Breaking Changes**
* Several runtime components, helpers, animation utilities, and
TypeScript typings were removed — imports and types will need updating.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: thomasvo <thomas.vo@openspace.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…e-define

# Conflicts:
#	.github/workflows/lint.yml
#	lib/commonjs/ReactNativeZoomableView.js
#	lib/commonjs/ReactNativeZoomableView.js.map
#	lib/commonjs/animations/index.js
#	lib/commonjs/animations/index.js.map
#	lib/commonjs/components/AnimatedTouchFeedback.js
#	lib/commonjs/components/AnimatedTouchFeedback.js.map
#	lib/commonjs/components/StaticPin.js
#	lib/commonjs/components/StaticPin.js.map
#	lib/commonjs/components/index.js
#	lib/commonjs/components/index.js.map
#	lib/commonjs/debugHelper/index.js
#	lib/commonjs/debugHelper/index.js.map
#	lib/commonjs/helper/calcNewScaledOffsetForZoomCentering.js
#	lib/commonjs/helper/calcNewScaledOffsetForZoomCentering.js.map
#	lib/commonjs/helper/coordinateConversion.js
#	lib/commonjs/helper/coordinateConversion.js.map
#	lib/commonjs/helper/index.js
#	lib/commonjs/helper/index.js.map
#	lib/commonjs/index.js
#	lib/commonjs/index.js.map
#	lib/commonjs/typings/index.js
#	lib/commonjs/typings/index.js.map
#	lib/module/ReactNativeZoomableView.js
#	lib/module/ReactNativeZoomableView.js.map
#	lib/module/animations/index.js
#	lib/module/animations/index.js.map
#	lib/module/components/AnimatedTouchFeedback.js
#	lib/module/components/AnimatedTouchFeedback.js.map
#	lib/module/components/StaticPin.js
#	lib/module/components/StaticPin.js.map
#	lib/module/components/index.js
#	lib/module/components/index.js.map
#	lib/module/debugHelper/index.js
#	lib/module/debugHelper/index.js.map
#	lib/module/helper/calcNewScaledOffsetForZoomCentering.js
#	lib/module/helper/calcNewScaledOffsetForZoomCentering.js.map
#	lib/module/helper/coordinateConversion.js
#	lib/module/helper/coordinateConversion.js.map
#	lib/module/helper/index.js
#	lib/module/helper/index.js.map
#	lib/module/index.js
#	lib/module/index.js.map
#	lib/module/typings/index.js
#	lib/module/typings/index.js.map
#	lib/typescript/ReactNativeZoomableView.d.ts
#	lib/typescript/animations/index.d.ts
#	lib/typescript/components/AnimatedTouchFeedback.d.ts
#	lib/typescript/components/StaticPin.d.ts
#	lib/typescript/components/index.d.ts
#	lib/typescript/debugHelper/index.d.ts
#	lib/typescript/helper/calcNewScaledOffsetForZoomCentering.d.ts
#	lib/typescript/helper/coordinateConversion.d.ts
#	lib/typescript/helper/index.d.ts
#	lib/typescript/index.d.ts
#	lib/typescript/typings/index.d.ts
#	package.json
#	src/ReactNativeZoomableView.tsx
#	src/animations/index.ts
#	yarn.lock
PR #158 removed checked-in lib/ and added it to .gitignore.
These remaining tracked files are now redundant.
Comment on lines +955 to +984
onPanResponderRelease: _handlePanResponderEnd,
onPanResponderTerminate: (evt, gestureState) => {
// We should also call _handlePanResponderEnd
// to properly perform cleanups when the gesture is terminated
// (aka gesture handling responsibility is taken over by another component).
// This also fixes a weird issue where
// on real device, sometimes onPanResponderRelease is not called when you lift 2 fingers up,
// but onPanResponderTerminate is called instead for no apparent reason.
_handlePanResponderEnd(evt, gestureState);
props.onPanResponderTerminate?.(
evt,
gestureState,
_getZoomableViewEventObject()
);
},
onPanResponderTerminationRequest: (evt, gestureState) =>
!!props.onPanResponderTerminationRequest?.(
evt,
gestureState,
_getZoomableViewEventObject()
),
// Defaults to true to prevent parent components, such as React Navigation's tab view, from taking over as responder.
onShouldBlockNativeResponder: (evt, gestureState) =>
props.onShouldBlockNativeResponder?.(
evt,
gestureState,
_getZoomableViewEventObject()
) ?? true,
onStartShouldSetPanResponderCapture: (evt, gestureState) =>
!!props.onStartShouldSetPanResponderCapture?.(evt, gestureState),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Five PanResponder callbacks in the empty-deps useLayoutEffect close directly over the render-scope props variable, permanently capturing initial-mount values and ignoring all later prop updates. This is a pre-existing issue carried forward unchanged by the refactor: onPanResponderTerminate, onPanResponderTerminationRequest, onShouldBlockNativeResponder, onStartShouldSetPanResponderCapture, and onMoveShouldSetPanResponderCapture should be wrapped with useLatestCallback the same way the other four handlers are.

Extended reasoning...

Root cause

In src/ReactNativeZoomableView.tsx, PanResponder.create is called inside a useLayoutEffect(() => { ... }, []) — a one-time mount effect. The five inline arrow functions passed to the PanResponder each directly reference the function-component's props variable:

onPanResponderTerminate: (evt, gestureState) => {
  _handlePanResponderEnd(evt, gestureState);
  props.onPanResponderTerminate?.(evt, gestureState, _getZoomableViewEventObject());
},
onPanResponderTerminationRequest: (evt, gestureState) =>
  !!props.onPanResponderTerminationRequest?.(evt, gestureState, _getZoomableViewEventObject()),
onShouldBlockNativeResponder: (evt, gestureState) =>
  props.onShouldBlockNativeResponder?.(evt, gestureState, _getZoomableViewEventObject()) ?? true,
onStartShouldSetPanResponderCapture: (evt, gestureState) =>
  !!props.onStartShouldSetPanResponderCapture?.(evt, gestureState),
onMoveShouldSetPanResponderCapture: (evt, gestureState) =>
  !!props.onMoveShouldSetPanResponderCapture?.(evt, gestureState),

Because the useLayoutEffect runs only once (empty deps), JavaScript's closure captures the props object from the very first render. On every subsequent render, React creates a new props object — but the callbacks still call the initial prop functions.

Why the fix pattern exists but was not applied here

The other four main handlers — _handlePanResponderEnd, _handlePanResponderMove, _handlePanResponderGrant, _handleStartShouldSetPanResponder — are all wrapped with useLatestCallback. That hook stores the latest function in a useRef (updated each render) and returns a stable wrapper that always calls ref.current. This correctly breaks the stale-closure problem.

The five inline callbacks above were never given the same treatment, making them permanently stale after the first render.

Concrete proof

Suppose a parent component conditionally toggles onShouldBlockNativeResponder based on a piece of state:

const [blocking, setBlocking] = useState(true);
<ReactNativeZoomableView
  onShouldBlockNativeResponder={() => blocking}
/>
// Later: setBlocking(false)

After setBlocking(false), the view re-renders with a new props.onShouldBlockNativeResponder that returns false. However, the PanResponder was created once at mount with the closure capturing the original props, which returned true. Every subsequent native gesture event will still call the stale function and return true, so native scroll containers will always be blocked regardless of what the parent passes.

Pre-existing nature

The same stale-closure pattern existed in the old code before this PR. The diff shows the removed lines had identical direct props.onPanResponderTerminate?., props.onShouldBlockNativeResponder?., etc. references inside the same one-time useLayoutEffect. This PR is a pure code reorganization (no-use-before-define compliance) and does not change this behavior in either direction.

Fix

Wrap each of the five callbacks with useLatestCallback before passing them to PanResponder.create, identical to how the four main handlers are handled.

Comment on lines 79 to 84
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-simple-import-sort": "^12.1.1",
"husky": "^4.2.5",
"lodash": "^4.17.21",
"pod-install": "^0.1.0",
"prettier": "^2.0.5",
"react": "17.0.2",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 lodash is imported at runtime in ReactNativeZoomableView.tsx but is listed only in peerDependencies, which are not automatically installed by package managers for library consumers. Any consumer project that does not explicitly add lodash to their own dependencies will get a hard crash (Cannot find module 'lodash') at runtime. Move lodash to the dependencies field so it is always installed automatically. Note: this is a pre-existing issue (lodash was previously in devDependencies, also not auto-installed), but this PR makes it more visible by moving it to peerDependencies.

Extended reasoning...

What the bug is and how it manifests

lodash is a hard runtime dependency of this library — ReactNativeZoomableView.tsx imports debounce and defaults from lodash at the top of the file. These are called during normal component lifecycle (defaults is applied on every render to merge prop defaults; debounce wraps a pinch handler). Despite being a runtime requirement, lodash appears only in peerDependencies (and previously was in devDependencies), never in dependencies. Package managers (npm, yarn classic v1) do NOT auto-install peerDependencies for library consumers, so any consumer who has not independently installed lodash will encounter a hard crash.

The specific code path that triggers it

In src/ReactNativeZoomableView.tsx:

  • Line 1: import { debounce, defaults } from 'lodash';
  • defaults is called when building the component's resolved props on every render cycle.
  • debounce is used to wrap debouncedOnStaticPinPositionChange which is invoked during pinch gesture handling.

Both usages are in the hot path of the component. There is no dynamic require, no try/catch fallback, and no optional chaining around the lodash calls.

Why existing code does not prevent it

peerDependencies communicates to consuming projects: 'you must install this yourself'. The library's own package.json has no entry in the dependencies field for lodash, so npm/yarn will never automatically install lodash for a consumer. If a consumer does npm install react-native-zoomable-view into a fresh project with no lodash, they will get no error during install — the crash only appears at runtime when the component is first mounted.

Impact

Any consumer project that does not already have lodash installed will encounter:

Error: Cannot find module 'lodash'

...the moment ReactNativeZoomableView is first rendered. This is a hard crash with no graceful degradation. Consumer projects built around this library (especially RN apps bootstrapped from a clean template) have no lodash by default, so this affects a realistic portion of users.

How to fix it

The simplest fix: move lodash from peerDependencies to dependencies in package.json:

"dependencies": {
  "lodash": "^4.17.21"
}

Remove it from peerDependencies entirely (or keep it there if you want to allow version deduplication). An alternative fix is to inline the two usages (debounce is available in react-native's bundled scheduler utilities; defaults can be replaced with Object.assign or spread syntax) and remove lodash as a dependency altogether.

Step-by-step proof

  1. Consumer runs: npm install react-native-zoomable-view
  2. npm resolves dependencies[] for the library (empty, lodash is not there). peerDependencies are noted but not installed.
  3. Consumer project has no lodash in their own package.json.
  4. Consumer renders <ReactNativeZoomableView> in their app.
  5. Metro bundler (or Node.js) attempts to resolve the import from 'lodash'.
  6. Module resolution fails — lodash is not on disk anywhere in node_modules.
  7. Hard crash: Error: Cannot find module 'lodash'.

Pre-existing note

Before this PR, lodash was in devDependencies (also not auto-installed for consumers). This PR moves it to peerDependencies, which is marginally better (it at least communicates the requirement), but still does not automatically install lodash. The root fix — placing lodash in dependencies — has never been applied. This issue is pre-existing but worth flagging because the PR touches package.json and is an opportunity to resolve it properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants