Button for saving content located in expo-router header

I am a beginner in react native and I am attempting to connect a save button in a modal header using expo-router. I have managed to make the button work within the modal itself, but I would like it to be located in the header set by expo-router. It doesn't feel right to transfer the data-saving logic to the routing file, and even when I tried that, I encountered realm context errors. I am unsure of how to proceed. Is there a way to achieve this with expo? It seems like a common use case, but I haven't been able to find any examples, including in their documentation.

_layout.tsx

import React from 'react';
import { RealmProvider } from '@realm/react';
import { router, Stack } from 'expo-router';

import { HeaderButton } from '@components/common/fields/HeaderButton';
import { schemas } from '@models/index';

export default function Layout() {
  return (
    <RealmProvider schema={schemas} deleteRealmIfMigrationNeeded={true}>
      <Stack>
        <Stack.Screen
          name="index"
          options={{
            headerShown: false,
          }}
        />
        <Stack.Screen
          name="modal"
          options={{
            presentation: 'modal',
            headerTitle: () => (
              <HeaderButton displayText="<" handleClick={() => router.back()} />
            ),
            // headerRight: () => (
            //   <HeaderButton
            //     handleClick={}
            //     displayText="Save"
            //   />
            // ),
          }}
        />
      </Stack>
    </RealmProvider>
  );
}

modal.tsx

import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { View } from 'react-native';

import { AddRecipeForm } from '@screens/recipe/AddRecipeForm';

export default function Modal() {
  return (
    <View>
      <StatusBar style="light" />
      <AddRecipeForm />
    </View>
  );
}

AddRecipeForm.tsx

import React, { useCallback } from 'react';
import { useRealm } from '@realm/react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { StyleSheet, View } from 'react-native';

import { HeaderButton } from '@components/common/fields/HeaderButton';
import { TextField } from '@components/common/fields/TextField';
import { Recipe } from '@models/Recipe';
import colors from '@styles/colors';

interface AddRecipeFormProps {
  userId?: string;
}

interface FormData {
  userId?: string;
  description: string;
  name: string;
}

export const AddRecipeForm: React.FC<AddRecipeFormProps> = ({
  // toggleModal,
  userId,
}) => {
  const { control, handleSubmit } = useForm<FormData>();
  const realm = useRealm();

  const handleAddRecipe = useCallback(
    (name: string, description: string): void => {
      if (!description && !name) {
        return;
      }

      realm.write(() => {
        return realm.create(Recipe, {
          name,
          description,
          userId: userId ?? 'SYNC_DISABLED',
        });
      });
    },
    [realm, userId],
  );

  const onSubmit: SubmitHandler<FormData> = (data) => {
    handleAddRecipe(data.name, data.description);
  };

  return (
    <>
      <View style={styles.container}>
        <View style={styles.buttonContainer}>
          <HeaderButton
            displayText="Save"
            handleClick={handleSubmit(onSubmit)}
          />
          {/* <CustomButton displayText="Cancel" handleClick={toggleModal} /> */}
        </View>
        <Controller
          control={control}
          // rules={{ required: true }}
          render={({ field: { onChange, value } }) => (
            <TextField
              value={value}
              onChangeText={onChange}
              placeholder="name"
            />
          )}
          name="name"
        />
        <Controller
          control={control}
          // rules={{ required: true }}
          render={({ field: { onChange, value } }) => (
            <TextField
              value={value}
              onChangeText={onChange}
              placeholder="description"
            />
          )}
          name="description"
        />
      </View>
    </>
  );
};

tldr; Specifically, I want to relocate the HeaderButton and its functionalities for usage in the modal header defined in the _layout.tsx Stack. Moving all the logic out of the component into the layout doesn't seem ideal (and didn't work for me). Is there a way to achieve this?

Answer №1

After some exploration, I finally cracked the code on how to handle the expo-router by delving into the documentation for react-navigation. Check out the details at https://reactnavigation.org/docs/header-buttons/#header-interaction-with-its-screen-component

The trick is to declare it in the _layout.tsx file and then customize it within the actual component using navigation.setOptions. Take a look at the use of the headerRight props in the two files below.

_layout.tsx

export default function Layout() {
  const router = useRouter();
  return (
    <RealmProvider schema={schemas} deleteRealmIfMigrationNeeded={true}>
      <Stack>
        <Stack.Screen
          name="index"
          options={{
            headerShown: false,
          }}
        />
        <Stack.Screen
          name="modal"
          options={{
            presentation: 'modal',
            headerTitle: 'Add Recipe',
            headerLeft: () => (
              <HeaderButton handleClick={() => router.back()} displayText="<" />
            ),
            headerRight: () => <HeaderButton displayText="Save" />,
          }}
        />
      </Stack>
    </RealmProvider>
  );
}

AddRecipeForm.tsx

export const AddRecipeForm: React.FC<AddRecipeFormProps> = ({
  // toggleModal,
  userId,
}) => {
  const { control, handleSubmit } = useForm<FormData>();
  const realm = useRealm();
  const navigation = useNavigation();
  const router = useRouter();

  const handleAddRecipe = useCallback(
    (name: string, description: string): void => {
      if (!description && !name) {
        return;
      }

      realm.write(() => {
        return realm.create(Recipe, {
          name,
          description,
          userId: userId ?? 'SYNC_DISABLED',
        });
      });
    },
    [realm, userId],
  );

  useEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <HeaderButton displayText="Save" handleClick={handleSubmit(onSubmit)} />
      ),
    });
  }, [navigation]);

  const onSubmit: SubmitHandler<FormData> = (data) => {
    handleAddRecipe(data.name, data.description);
    router.back();
  };

  return (
    <View style={styles.container}>
      <Controller
        control={control}
        // rules={{ required: true }}
        render={({ field: { onChange, value } }) => (
          <TextField value={value} onChangeText={onChange} placeholder="name" />
        )}
        name="name"
      />
      <Controller
        control={control}
        // rules={{ required: true }}
        render={({ field: { onChange, value } }) => (
          <TextField
            value={value}
            onChangeText={onChange}
            placeholder="description"
          />
        )}
        name="description"
      />
    </View>
  );
};

Hopefully this breakdown will assist others, especially considering the somewhat limited guidance provided in the expo documentation.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Leveraging React and TypeScript's capabilities to pass around arguments efficiently

As I integrate TypeScript into my application, I find myself at a juncture where I need to specify the following: { id, label, type, styles, ...props } Incorporating this structure into a component like the one below: const TestComponent = ({ id, label, t ...

The function parameter in Angular's ngModelChange behaves differently than $event

How can I pass a different parameter to the $event in the function? <div class='col-sm'> <label class="col-3 col-form-label">Origen</label> <div class="col-4"> <select ...

Decorator used in identifying the superclass in Typescript

I am working with an abstract class that looks like this export abstract class Foo { public f1() { } } and I have two classes that extend the base class export class Boo extends Foo { } export class Moo extends Foo { } Recently, I created a custom ...

What is a practice for utilizing navCtrl.push() with a variable storing a class name?

Currently, I am utilizing Visual Studio Code for Ionic 3 development with AngularJS/Typescript. In my code, I am using this.navCtrl.push() to navigate to different pages within the application. Specifically, I have two classes/pages named "level1" and "lev ...

Unexpected behavior with onKeyPress in React-Native Windows resulting in missing key press events

Currently, I am working on a Windows app using react-native version 0.54.0. For one of the functionalities, I have incorporated a TextInput element and would like to use onKeyPress. Here is my code snippet: <TextInput ref = { this.setTextInputRef } on ...

Cease the generation of dynamically produced sounds

I am encountering an issue in Angular where I am unable to stop playing an audio from a service. Below is my play() method: play(item: string): void { const audio = new Audio(); audio.src = item; audio.load(); audio.play(); } In order to stop all ...

Tips for achieving asynchronous data retrieval using Angular Observable inside another Observable

What is my goal? I have several components with similar checks and data manipulation activities. I aim to centralize these operations in an observable. To do this, I created an observable called "getData" within my service... The unique aspect of "getData ...

Creating a mock instance of a class and a function within that class using Jest

I am currently testing a class called ToTest.ts, which creates an instance of another class named Irrelevant.ts and calls a method on it called doSomething. // ToTest.ts const irrelevant = new Irrelevant(); export default class ToTest { // ... some impl ...

Choosing the correct key and handling parsing errors

As I work on converting a class component to TypeScript, I encountered an error while trying to implement one of my onChange methods. The error message stated: "Argument of type '{ [x: number]: any; }' is not assignable to parameter of type &ap ...

"Exploring the best practice: Defining types in React with Typescript before or after

As a newcomer to typescript, I have noticed that some projects declare the type before the component while others declare it after the component. Can someone explain the differences between these approaches? export type TProps = { item: string; } expor ...

Ensure that TypeScript compiled files are set to read-only mode

There is a suggestion on GitHub to implement a feature in tsc that would mark compiled files as readonly. However, it has been deemed not feasible and will not be pursued. As someone who tends to accidentally modify compiled files instead of the source fil ...

Using TypeScript's generic rest parameters to form a union return type

Right now, in TypeScript you can define dynamic generic parameters. function bind<U extends any[]>(...args: U); However, is it possible for a function to return a union of argument types? For example: function bind<U extends any[]>(...args: ...

Error encountered while loading a plugin in Typescript and RequireJS compilation process

Currently, I am working on a Typescript application in Visual Studio 2015 where RequireJS is used for loading modules. I have successfully loaded various modules from .ts classes and external libraries by using their typing .d.ts files. However, I have en ...

How can I incorporate percentage values into input text in Angular?

How can I include a percent sign in an input field using Angular, without relying on jQuery? I am looking for a solution that is identical to what I would achieve with jQuery. Here is the current status of my project: ...

Using Angular's setTimeout() function with an external lambda that includes a parameter

My goal is to tackle two issues at once: 1) using setTimeout( #action#, timeMillis) with #action# as a lambda 2) supplying the lambda with a parameter. The common method of setTimeout( ()=>{ #callback# }, timeMillis) works flawlessly when extracting () ...

Encountering a NgForm provider error in Angular 4.4.6 development mode

UPDATE: Identifying the root of the issue has led me to search for a suitable solution. NOTE: This complication is specific to development mode (not production, and not utilizing AOT). The "Update" resolution I am implementing can be found here. In an a ...

What is the process for removing a document with the 'where' function in Angular Fire?

var doc = this.afs.collection('/documents', ref => ref.where('docID', '==', docID)); After successfully retrieving the document requested by the user with the code above, I am unsure of how to proceed with deleting that do ...

When retrieving objects using Angular's HttpClient, properties may be null or empty

I am working with a service class in Angular that utilizes the HttpClient to retrieve data from a web service. The web service responds with a JSON object structured like this: { "id": "some type of user id", "name": "The name of the user", "permiss ...

What are the best scenarios for creating a constructor in Angular 2 using Typescript?

Check out these sample constructors I found in the Angular 2 documentation: export class AppComponent implements OnInit { title = 'Tour of heroes'; heroes: Hero[]; selectedHero: Hero; constructor(private heroService: HeroService ...

Activating Ionic6 Stack Modal through JavaScript or TypeScript

Is it possible to trigger the modal using script code instead of a button? I have searched through various examples in the tutorial, but all of them rely on the modal trigger mechanism. <ion-button id="open-modal" expand="block">O ...