software-mansion / react-native-screens

Native navigation primitives for your React Native app.
https://docs.swmansion.com/react-native-screens/
MIT License
3.03k stars 514 forks source link

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

Open Rezrazi opened 1 week ago

Rezrazi commented 1 week ago

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

maciekstosio commented 1 day ago

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.