• Stars
    star
    299
  • Rank 139,269 (Top 3 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 3 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🦅 Anchor links and scroll-to utilities for React Native (+ Web)

React Native Anchor 🦅

Anchor links and scroll-to utilities for React Native (+ Web). It has zero dependencies.

Installation

yarn add @nandorojo/anchor

If you're using react-native-web, you'll need at least version 0.15.3.

This works great to scroll to errors in Formik forms. See the ScrollToField component.

Usage

This is the simplest usage:

import React from 'react'
import { ScrollTo, Target, ScrollView } from '@nandorojo/anchor'
import { View, Text } from 'react-native'

export default function App() {
  return (
    <View style={{ flex: 1 }}>
      <ScrollView>
        <ScrollTo target="bottom-content">
          <Text>Scroll to bottom content</Text>
        </ScrollTo>
        <View style={{ height: 1000 }} />
        <Target name="bottom-content">
          <View style={{ height: 100, backgroundColor: 'blue' }} />
        </Target>
      </ScrollView>
    </View>
  )
}

The library exports a ScrollView and FlatList component you can use as drop-in replacements for the react-native ones.

Note that the scroll to will only work if you use this library's scrollable components, or if you use a custom scrollable with the AnchorProvider, as shown in the example below.

Use custom scrollables

If you want to use your own scrollable, that's fine. You'll just have to do 2 things:

  1. Wrap them with the AnchorProvider
  2. Register the scrollable with useRegisterScroller

That's all the exported ScrollView does for you.

import { AnchorProvider, useRegisterScrollable } from '@nandorojo/anchor'
import { ScrollView } from 'react-native'

export default function Provider() {
  return (
    <AnchorProvider>
      <MyComponent />
    </AnchorProvider>
  )
}

// make sure this is the child of AnchorProvider
function MyComponent() {
  const { register } = useRegisterScroller()

  return (
    <ScrollView ref={register}>
      <YourContentHere />
    </ScrollView>
  )
}

If you need horizontal scrolling, make sure you pass the horizontal prop to both the AnchorProvider, and the ScrollView.

import { AnchorProvider, useRegisterScrollable } from '@nandorojo/anchor'
import { ScrollView } from 'react-native'

export default function Provider() {
  return (
    <AnchorProvider horizontal>
      <MyComponent />
    </AnchorProvider>
  )
}

// make sure this is the child of AnchorProvider
function MyComponent() {
  const { register } = useRegisterScroller()

  return (
    <ScrollView horizontal ref={register}>
      <YourContentHere />
    </ScrollView>
  )
}

Trigger a scroll-to event

There are a few options for triggering a scroll-to event. The basic premise is the same as HTML anchor links. You need 1) a target to scroll to, and 2) something to trigger the scroll.

The simplest way to make a target is to use the Target component.

Each target needs a unique name prop. The name indicates where to scroll.

import { ScrollView, Target } from '@nandorojo/anchor'

export default function App() {
  return (
    <ScrollView>
      <Target name="bottom">
        <YourComponent />
      </Target>
    </ScrollView>
  )
}

Next, we need a way to scroll to that target. The easiest way is to use the ScrollTo component:

import { ScrollView, Target } from '@nandorojo/anchor'
import { Text, View } from 'react-native'

export default function App() {
  return (
    <ScrollView>
      <ScrollTo target="bottom">
        <Text>Click me to scroll down</Text>
      </ScrollTo>
      <View style={{ height: 500 }} />
      <Target name="bottom">
        <YourComponent />
      </Target>
    </ScrollView>
  )
}

Create a custom scrollTo component

If you don't want to use the ScrollTo component, you can also rely on the useScrollTo with a custom pressable.

import { ScrollView, Target } from '@nandorojo/anchor'
import { Text, View } from 'react-native'

function CustomScrollTo() {
  const { scrollTo } = useScrollTo()

  const onPress = () => {
    scrollTo('scrollhere') // required: target name

    // you can also pass these optional parameters:
    scrollTo('scrollhere', {
      animated: true, // default true
      offset: -10 // offset to scroll to, default -10 pts
    })
  }

  return <Text onPress={onPress}>Scroll down</Text>
}

export default function App() {
  return (
    <ScrollView>
      <CustomScrollTo />
      <View style={{ height: 500 }} />
      <Target name="scrollhere">
        <YourComponent />
      </Target>
    </ScrollView>
  )
}

useRegisterTarget()

The basic usage for determing the target to scroll to is using the Target component.

However, if you want to use a custom component as your target, you'll use the useRegisterTarget hook.

import { ScrollTo, useRegisterTarget, ScrollView } from '@nandorojo/anchor';
import { View } from 'react-native'

function BottomContent() {
  const { register } = useRegisterTarget()

  const ref = register('bottom-content') // use a unique name here

  return <View ref={ref} />
}

function App() {
return (
  <ScrollView>
    <ScrollTo target="bottom-content">Scroll to bottom content</Anchor>
    <View style={{ height: 500 }} />
    <BottomContent />
  </ScrollView>
);
}

Web usage

Smooth Scrolling

This works with web (react-native-web 0.15.3 or higher).

To support iOS browsers, you should polyfill the smooth scroll API.

yarn add smoothscroll-polyfill

Then at the root of your app (App.js, or pages/_app.js for Next.js) call this:

import { Platform } from 'react-native'

if (Platform.OS === 'web' && typeof window !== 'undefined') {
  require('smoothscroll-polyfill').polyfill()
}

Patch

react-native-web's resolution for scroll position is currently wrong.

Until this issue is closed (necolas/react-native-web#2109), I'll be using this patch with patch-package:

Click me to view patch
diff --git a/node_modules/@nandorojo/anchor/lib/module/index.js b/node_modules/@nandorojo/anchor/lib/module/index.js
index 6decdc0..d27b884 100644
--- a/node_modules/@nandorojo/anchor/lib/module/index.js
+++ b/node_modules/@nandorojo/anchor/lib/module/index.js
@@ -1,7 +1,7 @@
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 import * as React from 'react';
-import { View, ScrollView as NativeScrollView, FlatList as NativeFlatList, findNodeHandle, Pressable } from 'react-native';
+import { View, ScrollView as NativeScrollView, FlatList as NativeFlatList, findNodeHandle, Pressable, Platform } from 'react-native';
 const {
   createContext,
   forwardRef,
@@ -130,7 +130,10 @@ const useCreateAnchorsContext = ({
         return new Promise(resolve => {
           var _targetRefs$current, _targetRefs$current2;
 
-          const node = scrollRef.current && findNodeHandle(scrollRef.current);
+          const node = Platform.select({
+            default: scrollRef.current,
+            web: scrollRef.current && scrollRef.current.getInnerViewNode  && scrollRef.current.getInnerViewNode()
+          })
 
           if (!node) {
             return resolve({
diff --git a/node_modules/@nandorojo/anchor/src/index.js b/node_modules/@nandorojo/anchor/src/index.js
index 7259856..80fc63c 100644
--- a/node_modules/@nandorojo/anchor/src/index.js
+++ b/node_modules/@nandorojo/anchor/src/index.js
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { View, ScrollView as NativeScrollView, FlatList as NativeFlatList, findNodeHandle, Pressable, } from 'react-native';
+import { View, ScrollView as NativeScrollView, FlatList as NativeFlatList, findNodeHandle, Pressable, Platform } from 'react-native';
 const { createContext, forwardRef, useContext, useMemo, useRef, useImperativeHandle, } = React;
 // from react-merge-refs (avoid dependency)
 function mergeRefs(refs) {
@@ -109,7 +109,11 @@ const useCreateAnchorsContext = ({ horizontal, }) => {
             horizontal,
             scrollTo: (name, { animated = true, offset = -10 } = {}) => {
                 return new Promise((resolve) => {
-                    const node = scrollRef.current && findNodeHandle(scrollRef.current);
+                    const node = Platform.select({
+                      default: scrollRef.current,
+                      web: scrollRef.current && scrollRef.current.getInnerViewRef()
+                    })
+                    // const node = scrollRef.current && findNodeHandle(scrollRef.current);
                     if (!node) {
                         return resolve({
                             success: false,

Gotchas

One thing to keep in mind: the parent view of a ScrollView on web must have a fixed height. Otherwise, the ScrollView will just use window scrolling. This is a common source of confusion on web, and it took me a while to learn.

Typically, it's solved by doing this:

import { View, ScrollView } from 'react-native'

export default function App() {
  return (
    <View style={{ flex: 1 }}>
      <ScrollView />
    </View>
  )
}

By wrapping ScrollView with a flex: 1 View, we are confining its parent's size. If this doesn't solve it, try giving your parent View a fixed height:

import { View, ScrollView, Platform } from 'react-native'

export default function App() {
  return (
    <View style={{ flex: 1, height: Platform.select({ web: '100vh', default: undefined }) }}>
      <ScrollView />
    </View>
  )
}

Imports

import {
  AnchorProvider,
  ScrollView,
  FlatList,
  useRegisterTarget,
  useScrollTo,
  ScrollTo,
  Target,
  useRegisterScroller,
  useAnchors
} from '@nandorojo/anchor'

ScrollTo

const Trigger = () => (
  <ScrollTo
    target="bottom"
    options={{
      animated: true,
      offset: -10
    }}
  />
)

Props

  • target required unique string indicating the name of the Target to scroll to
  • options optional dictionary
    • animated = true whether the scroll should animate or not
    • offset = -10 a number in pixels to offset the scroll by. By default, it scrolls 10 pixels above the content.

Target

const ContentToScrollTo = () => <Target name="bottom-content" />

Props

  • name required, unique string that identifies this View to scroll to
    • it only needs to be unique within a given ScrollView. You can reuse names for different scrollables, but I'd avoid doing that.

useScrollTo

A react hook that returns a scrollTo(name, options?) function. This serves as an alternative to the ScrollTo component.

The first argument is required. It's a string that corresponds to your target's unique name prop.

The second argument is an optional options object, which is identical to the ScrollTo component's options prop.

import { ScrollView, Target } from '@nandorojo/anchor'
import { Text, View } from 'react-native'

function CustomScrollTo() {
  const { scrollTo } = useScrollTo()

  const onPress = () => {
    scrollTo('scrollhere') // required: target name

    // you can also pass these optional parameters:
    scrollTo('scrollhere', {
      animated: true, // default true
      offset: -10 // offset to scroll to, default -10 pts
    })
  }

  return <Text onPress={onPress}>Scroll down</Text>
}

export default function App() {
  return (
    <ScrollView>
      <CustomScrollTo />
      <View style={{ height: 500 }} />
      <Target name="scrollhere">
        <YourComponent />
      </Target>
    </ScrollView>
  )
}

useRegisterScroller

A hook that returns a register function. This is an alternative option to using the ScrollView or FlatList components provided by this library.

Note that, to use this, you must first wrap the scrollable with AnchorProvider. It's probably easier to just use the exported ScrollView, but it's your call.

import { AnchorProvider, useRegisterScrollable } from '@nandorojo/anchor'
import { ScrollView } from 'react-native'

// make sure this is the child of AnchorProvider
function MyComponent() {
  const { register } = useRegisterScroller()

  return (
    <ScrollView ref={register}>
      <YourContentHere />
    </ScrollView>
  )
}

export default function Provider() {
  return (
    <AnchorProvider>
      <MyComponent />
    </AnchorProvider>
  )
}

useAnchors

If you need to control a ScrollView or FlatList from outside of their scope:

import React from 'react'
import { useAnchors, ScrollView } from '@nandorojo/anchor'

export default function App() {
 const anchors = useAnchors()

 const onPress = () => {
   anchors.current?.scrollTo('list')
 }

 return (
   <ScrollView anchors={anchors}>
     <Target name="list" />
   </ScrollView>
 )
}

Formik Error Usage

  1. Create a ScrollToField component:
import React, { useEffect, useRef } from 'react'
import { Target, useScrollTo } from '@nandorojo/anchor'
import { useFormikContext } from 'formik'

function isObject(value?: object) {
  return value && typeof value === 'object' && value.constructor === Object
}

function getRecursiveName(object?: object): string {
  if (!object || !isObject(object)) {
    return ''
  }
  const currentKey = Object.keys(object)[0]
  if (!currentKey) {
    return ''
  }
  if (!getRecursiveName(object[currentKey])) {
    return currentKey
  }
  return currentKey + '.' + getRecursiveName(object[currentKey])
}

export function ScrollToField({ name }: { name: string }) {
  const { submitCount, errors } = useFormikContext()

  const { scrollTo } = useScrollTo()
  const previousSubmitCount = useRef(submitCount)
  const errorPath = getRecursiveName(errors)

  useEffect(
    function scrollOnSubmissionError() {
      if (!errorPath) return

      if (submitCount > previousSubmitCount.current && name) {
        if (name === errorPath) {
          scrollTo(name).then((didScroll) => console.log('[scroll-to-field] did scroll', name, didScroll))
        }
      }
      previousSubmitCount.current = submitCount
    },
    [errorPath, errors, name, scrollTo, submitCount]
  )

  return <Target name={name} />
}
  1. Add it alongside your field:
const InputField = ({ name }) => {
  const [{ value }] = useField(name)

  return (
    <View>
      <ScrollToField name={name} />
      <TextInput value={value} />
    </View>
  )
}

Contributing

See the contributing guide to learn how to contribute to the repository and the development workflow.

License

MIT

More Repositories

1

moti

🐼 The React Native (+ Web) animation library, powered by Reanimated 3.
TypeScript
3,956
star
2

solito

🧍‍♂️ React Native + Next.js, unified.
TypeScript
3,426
star
3

dripsy

🍷 Responsive, unstyled UI primitives for React Native + Web.
TypeScript
1,918
star
4

zeego

Menus for React (Native) done right.
TypeScript
1,520
star
5

burnt

Crunchy toasts for React Native. 🍞
Java
1,140
star
6

swr-firestore

Implement Vercel's useSWR for querying Firestore in React/React Native/Expo apps. 👩‍🚒🔥
TypeScript
763
star
7

expo-next-react-navigation

⛴ Make Next.js and react-navigation play nicely together with an Expo/React Native Web app.
TypeScript
388
star
8

react-navigation-heavy-screen

⚡️Optimize heavy screens to prevent lags during React Navigation transitions.
TypeScript
320
star
9

swr-react-native

React Native/React Navigation compatibility for Vercel's useSWR hook. 🐮
TypeScript
285
star
10

galeria

The React (Native) Image Viewer. 📷
TypeScript
169
star
11

expo-next-monorepo

💸 Expo + Next.js monorepo starter.
JavaScript
114
star
12

expo-spotify-party

🎹 Listen to Spotify with your friends in real-time. (React Native Expo + Next.js)
TypeScript
88
star
13

nextjs-conf-22-example

Example repository for the 2022 Next.js Conf about React Native.
TypeScript
67
star
14

react-native-iconic

🎨 Iconic icons for React Native (+Web)
TypeScript
65
star
15

react-native-heroicons

Hero Icons for React Native. 🫡
TypeScript
64
star
16

zero-to-10m

Resources from my talk at Next.js Conf: "Zero to $10 Million with React Native + Next.js"
57
star
17

expo-firestore-offline-persistence

❄️ Enable Firestore offline persistence in Expo apps without detaching.
JavaScript
51
star
18

tamagui-intellisense

VSCode Plugin for Tamagui
JavaScript
41
star
19

sf-symbols-typescript

TypeScript types for iOS SF Symbols 🍏
TypeScript
36
star
20

doorman

🚪Doorman is a full-stack phone authentication solution for Expo + Firebase auth.
TypeScript
29
star
21

reanimated-tree-shaking

A Next.js playground to test Reanimated bundle size.
TypeScript
24
star
22

sticky

Implement position: sticky in React Native
TypeScript
15
star
23

ts-monorepo-autoimport-guide

A guide for making VSCode's autoimport work in a monorepo.
12
star
24

expo-gatsby-navigation

🤵Make Gatsby and React Navigation play nicely together with an Expo/React Native Web app.
TypeScript
12
star
25

tgui

TypeScript
11
star
26

react-native-2030

The implementation of my App.js Conf talk, "React Native 2030"
11
star
27

fuego

🔥Firebase Firestore hooks & components for React Native/React.
TypeScript
10
star
28

shared-animations

🕺A global state manager for React Native animations.
JavaScript
10
star
29

expo-electron-copy-magic

Copy clip sucks, so this is a better one made with Expo + Electron.
TypeScript
10
star
30

lint-expo

🐠 A mini script that configures prettier and eslint for React/Expo projects.
JavaScript
9
star
31

tamagui-starter

Starter project for issue reproductions on CodeSandbox
TypeScript
8
star
32

doorman-examples

🥳A repository of example apps for Doorman (Expo + Firebase phone auth)
TypeScript
8
star
33

solito-test

TypeScript
6
star
34

react-doorman

React Core library for Doorman's Firebase Auth, without the RN dependencies. 👾Meant for use in Gatsby/other React apps.
TypeScript
4
star
35

expo-eas-semantic-release

git push → automated Expo EAS builds, over-the-air updates, & prerelease versions.
JavaScript
4
star
36

lexical-ios-react-native

Just an experiment with Expo Modules
Swift
4
star
37

expo-starter-project

eslint, src folder, etc
TypeScript
3
star
38

panda

🐼 Design websites in seconds. Export clean code. (PRE-ALPHA)
JavaScript
3
star
39

expo-navigation-core

Package with reusable expo web navigation hooks / components.
TypeScript
2
star
40

lint-react-native-app

🤓 Simple command to add ESLint and Prettier to a react-native project.
TypeScript
2
star
41

thet

TypeScript
2
star
42

animateMePlz

⚡️ A lightweight jQuery extension for scrolling animations.
HTML
2
star
43

expo-router-bug

TypeScript
1
star
44

nandorojo

1
star
45

plaid-bug-repro

Repro for https://github.com/plaid/react-native-plaid-link-sdk/issues/436
Objective-C
1
star
46

expo-next-issue

⚡️React-native-paper issue with expo / next.
JavaScript
1
star
47

react-native-bootstrap

React-bootstrap, remade for React Native (& React Native For Web).
TypeScript
1
star
48

reanimated-next13-issue

Reanimated isn't working with Next.js 13.
JavaScript
1
star
49

react-native-beacons-example

Repo for react-native-beacons
TypeScript
1
star
50

react-spring-next-issue

Reproducible repo for this issue: https://github.com/react-spring/react-spring/issues/627#issuecomment-541235141
TypeScript
1
star
51

next-style-issue

Issue: prop did not match, server: x, client: y for issue: https://github.com/vercel/next.js/discussions/14469
TypeScript
1
star
52

writing-javascript-actions

1
star
53

eslint-config-nando

Stress-free react native linting. 🏋🏻‍♂️
JavaScript
1
star
54

Penn-Requirement-Counter

🤓 Find courses that count for one or more requirements at UPenn.
JavaScript
1
star
55

expo-web-img-picker-bug

SDK 41 image picker not working on web.
JavaScript
1
star
56

expo-next-webpack5-issue

Reproduction of Webpack 5 not working with Expo in a Next.js app
JavaScript
1
star