I am new to react native and am trying to hook up a save button in a modal header using expo-router
. I can get the button to do what I need in the modal itself, but I would like this to be in the header that is set from the expo-router. It doesn't seem right to move the data-saving logic into the routing file, and even when I do I get these realm context errors. So I don't know how to proceed. Is there a way to do what I need with expo? Seems to me like this use case should be common, but I can't seem to find an example anywhere, including 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; More specifically I want to move the HeaderButton
and all of its functionality to be used in the modal header that is defined in the _layout.tsx
Stack. It doesn't seem appropriate to move all the logic out of the component and into the layout (nor could I get it to work). Is there a way to acomplish this?
I finally figured it out. I actually figured the expo-router
by reading the react-navigation
docs. https://reactnavigation.org/docs/header-buttons/#header-interaction-with-its-screen-component
Essentially you declare it in _layout.tsx
and sort of "override" it in the actual component using navigation.setOptions
. See the headerRight
props in the 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>
);
};
Hope this helps someone, especially with the expo
docs being so sparse.