react-native-screens icon indicating copy to clipboard operation
react-native-screens copied to clipboard

Expo - Unable to set default text for header search bar using ref

Open Rezrazi opened this issue 1 year ago • 2 comments

Description

When trying to imperatively set the text of the search bar input using a ref, it doesn't work as ref is always null on render.

Steps to reproduce

import { Stack } from "expo-router";
import { useEffect, useRef } from "react";
import { SearchBarCommands } from "react-native-screens";

export default function HomeScreen() {
  const ref = useRef<SearchBarCommands>(null);

  useEffect(() => {
    // > null
    console.log(ref.current);

    ref.current?.setText("hey");
  }, []);

  return (
    <>
      <Stack.Screen
        options={{
          title: "Hello world",
          headerShown: true,
          headerSearchBarOptions: {
            placeholder: "Search...",
            placement: "stacked",
            hideNavigationBar: false,
            autoFocus: true,
            hideWhenScrolling: false,
            autoCapitalize: "none",
            inputType: "text",
            ref,
          },
        }}
      />
    </>
  );
}

Snack or a link to a repository

https://github.com/Rezrazi/react-native-search-bar-minimal

Screens version

3.34.0

React Native version

0.75.4

Platforms

iOS

JavaScript runtime

None

Workflow

Expo managed workflow

Architecture

None

Build type

Debug mode

Device

iOS simulator

Device model

iPhone 16 Pro

Acknowledgements

Yes

Rezrazi avatar Oct 07 '24 19:10 Rezrazi

Hi! Thanks for reporting the issue. Unfortunately, currently, I can only suggest using setTimeout with 40ms+ (this might be flaky depending on you're app size and device) as a workaround. We're talking internally about how to solve this issue. If you're interested in more details: There are two problems:

  1. setOptions which is used by the expo-router when providing the options prop, changes the state internally, which causes HeaderConfig to rerender (specifically, it renders with SearchBar component), only once the SearchBar component is rendered the ref becomes set. The screen content isn't rerendered when HeaderConfig changes, which makes sense, but because of that, the developer is not able to react to those changes - there is no way of knowing when ref becomes available to the developer.
  2. once the ref is filled (which can be achieved using setTimeout or forcing rerender i.e. by setting a state in useEffect), calling it might not cause any effect, that's because the native component might not be created yet, thus the 40ms+ timeout

On a side note - Screen component in the context of react-navigation (and expo-router) is a template on how to create a specific instance of a given screen, which can be pushed multiple times to the stack. So, passing ref to it might cause unexpected errors. I would suggest using:

const navigation = useNavigation();

useEffect/useLayoutEffect(() => {
navigation.setOptions({
headerSearchBarOptions: {...},
});
}, []);

inside screen content. I don't have much experience in using expo-router, but it looks like if you'd like to set some options on the screen like in your reproduction, you should do it in _layouts.tsx. Keep in mind that useLayouEffect delays the native render of the elements, but useEffect that will be fired immediately after it won't have ref set because the SearchBar component is not rendered yet.

maciekstosio avatar Oct 15 '24 11:10 maciekstosio

@Rezrazi i come up with another workaround, that could be better for your use-case:

export default function HomeScreen() {
  const navigation = useNavigation();
  const ref = useRef<SearchBarCommands>(null);

  useEffect(() => {
    navigation.setOptions({
      title: "Hello world",
      headerShown: true,
      headerSearchBarOptions: {
        placeholder: "Search...",
        placement: "stacked",
        hideNavigationBar: false,
        autoFocus: true,
        hideWhenScrolling: false,
        autoCapitalize: "none",
        inputType: "text",
        ref,
      },
    });

    const listener = navigation.addListener('transitionEnd', (event) => {
      if (event.data?.closing === false) {
        console.log(ref.current);
        ref.current?.setText('text')
      }
    });

    return () => {
      navigation.removeListener('transitionEnd', listener);
    }
  }, []);

  return (
    <View>
      <Text>Test</Text>
    </View>
  );
}

Let me know if that solves your problem!

maciekstosio avatar Oct 21 '24 04:10 maciekstosio

thank you @maciekstosio the workaround works

hopefully the behavior becomes consistent in the next versions

Rezrazi avatar Dec 21 '24 12:12 Rezrazi