React Native: How to setup React Navigation types so I don't have to create NavigationProps on each component I use? - reactjs

So right now I have a routes.tsx file that holds all my types. But on the screens where I use useNavigation() I always need to create a type for it in that component. How do I properly set up a global type for my routes so I don't have to do this?
routes.tsx
export type AuthStackParamList = {
Landing: undefined;
GetStarted: undefined;
VerifyOtp: { email: string };
PrivacyPolicy: undefined;
TermsOfService: undefined;
};
export type AppTabParamList = {
HomeScreen: undefined;
FriendsScreen: undefined;
NotificationsScreen: undefined;
SettingsScreen: undefined;
};
export type OnboardingStackParamList = {
UsernameScreen: undefined;
};
export type HomeTabStackParamList = {
Home: undefined;
};
export type FriendsTabStackParamList = {
Friends: undefined;
SearchUsers: undefined;
};
export type SettingsTabStackParamList = {
Settings: undefined;
EditName: { id: string; name: string };
EditUsername: { id: string; username: string };
DeleteAccount: undefined;
};
AuthStack.tsx
const AuthStack = createNativeStackNavigator<AuthStackParamList>();
export function AuthStackNavigator() {
return (
<AuthStack.Navigator
initialRouteName="Landing"
}}>
<AuthStack.Screen
name="Landing"
component={LandingScreen}
options={{ headerShown: false }}
/>
<AuthStack.Screen
name="GetStarted"
component={GetStartedScreen}
options={{ headerTitle: '' }}
/>
<AuthStack.Screen
name="VerifyOtp"
component={VerifyOTPScreen}
options={{ headerShown: false, gestureEnabled: false }}
/>
<AuthStack.Screen
name="TermsOfService"
component={TermsOfServiceScreen}
options={{ headerTitle: 'Terms of Service' }}
/>
<AuthStack.Screen
name="PrivacyPolicy"
component={PrivacyPolicy}
options={{ headerTitle: 'Privacy Policy' }}
/>
</AuthStack.Navigator>
);
}
GetStartedScreen.tsx
This is what I want to avoid having to do whenever I need to tap into useNavigation
type GetStartedScreenNavigationProps = NativeStackNavigationProp<
AuthStackParamList,
'GetStarted'
>;
const GetStartedScreen = () => {
const navigation = useNavigation<GetStartedScreenNavigationProps>();

You could create a custom hook in a separate file like this:
export const useAuthStackNavigation = <K extends keyof AuthStackParamList>() =>
{
return useNavigation<NativeStackNavigationProp<AuthStackParamList, K>>();
};
And then in your screen:
const GetStartedScreen = () => {
const navigation = useAuthStackNavigation<'GetStarted'>();

I think there is no way to do that.
Because useNavigation is a hook.
And a hook is always have to be created inside any component.
You can't create the hook and export it to use in any other component.

According to the docs a declare global should work with useNavigation.
Instead of manually annotating these APIs, you can specify a global
type for your root navigator which will be used as the default type.
To do this, you can add this snippet somewhere in your codebase:
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
The RootParamList interface lets React Navigation know about the params accepted by your root navigator. Here we extend the type RootStackParamList because that's the type of params for our stack navigator at the root. The name of this type isn't important.
Here is a link to the chapter: https://reactnavigation.org/docs/typescript/#specifying-default-types-for-usenavigation-link-ref-etc

Related

React Navigation custom route params and typescript

I have an Error screen that needs to know the error number and a description to display the error and I can't figure out how to tell TS which types are my Error screen custom params. I'm calling my Error screen this way.
navigation.dispatch(
StackActions.replace('Error', {
statusCode: 400,
description: 'Error description',
}),
);
This is my Error screen component. TS complains in route.params
export default function ErrorScreen({ route, navigation }: RootStackScreenProps<'Error'>) {
const { statusCode, description } = route.params; <-- TS error here
// #ts-ignore
const { setClientURL } = appState(({ setClientURL }) => ({
setClientURL,
}));
TS2339: Property 'description' does not exist on type 'Readonly {
key: string; index: number; routeNames: string[]; history?: unknown[]
| undefined; routes: NavigationRoute []; type: string; stale: false;
}>> | undefined>'.
These are my types definitions
import { NavigatorScreenParams } from '#react-navigation/native';
import { NativeStackScreenProps } from '#react-navigation/native-stack';
export type RootStackParamList = {
Root: undefined;
Error: NavigatorScreenParams<ErrorParamList> | undefined;
Notifications: undefined;
Log: undefined;
};
export type RootStackScreenProps<Screen extends keyof RootStackParamList> = NativeStackScreenProps<
RootStackParamList,
Screen
>;
export type ErrorParamList = {
Error: {
statusCode: number;
description: string;
};
};
This is my navigator create code
export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) {
return (
<NavigationContainer
linking={LinkingConfiguration}
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<RootNavigator />
</NavigationContainer>
);
}
/**
* A root stack navigator is often used for displaying modals on top of all other content.
* https://reactnavigation.org/docs/modal
*/
const Stack = createNativeStackNavigator<RootStackParamList>();
function RootNavigator() {
const colorScheme = useColorScheme();
return (
<Stack.Navigator>
<Stack.Screen
name="Error"
component={ErrorScreen}
options={({ navigation }: RootStackScreenProps<'Error'>) => ({
headerLeft: () => null,
title: 'Ups!...',
headerShadowVisible: false,
headerStyle: {
backgroundColor: '#ffe56d',
},
headerTitleStyle: {
fontWeight: 'bold',
fontSize: 30,
},
gestureEnabled: false,
headerBackVisible: false,
})}
/>
How can I tell TS that for my Error screen route.params can have statusCode and description primitives?

TypeScript Polymorphic Components in React with conditional props

I'm trying to write a Polymorphic component in React that supports both host elements such as (div, button, etc) and custom React components.
but there's a constraint of needing whatever component is passed in to the as prop that it must have an onClick prop
here's my work in progress.
import React, { useCallback, useRef, useLayoutEffect } from 'react';
export type GaClickTrackerProps<Type extends React.ElementType> = {
as: Type;
fieldsObject: {
hitType: UniversalAnalytics.HitType; // 'event'
eventCategory: string;
eventAction: string;
eventLabel?: string | undefined;
eventValue?: number | undefined;
nonInteraction?: boolean | undefined;
};
onClick?: (...args: any[]) => any;
} & React.ComponentPropsWithoutRef<Type>;
export function GaClickTracker<Type extends React.ElementType>({
onClick,
fieldsObject,
as: Component,
...props
}: GaClickTrackerProps<Type>) {
const fieldsObjectRef = useRef(fieldsObject);
const onClickRef = useRef(onClick);
useLayoutEffect(() => {
onClickRef.current = onClick;
fieldsObjectRef.current = fieldsObject;
});
const handleClick: React.MouseEventHandler<Type> = useCallback((event) => {
onClickRef.current?.(event);
if (fieldsObjectRef.current) {
ga('send', fieldsObjectRef.current);
}
}, []);
return (
<Component onClick={handleClick} {...props} />
);
}
Instead of going into the "polymorphic component" direction, for this particular issue it would be far better to have an util function like this:
import type { MouseEventHandler } from "react";
type Fields = {
eventAction: string;
eventCategory: string;
eventLabel?: string | undefined;
eventValue?: number | undefined;
hitType: UniversalAnalytics.HitType;
nonInteraction?: boolean | undefined;
};
export const sendAnalytics =
(fields?: Fields) =>
<Type>(handler?: MouseEventHandler<Type>): MouseEventHandler<Type> =>
event => {
handler?.(event); // We call the passed handler first
// And we only call `ga` if the we have fields and the default wasn't prevented
return !event.defaultPrevented && fields
? ga("submit", fields)
: undefined;
};
And then you can use it like this:
import { sendAnalytics } from "~/utils/sendAnalytics.js";
const sendIDKAnalytics = sendAnalytics({
eventAction: "IDK",
eventCategory: "IDK",
hitType: "IDK",
});
export const TrackedButton = ({ onClick, ...props }) => (
<button onClick={sendIDKAnalytics(onClick)} {...props} />
);
Being a curried function, you could even reuse the same settings in different components if you wanted. And if you want to "Reactify" it further, you could make it a hook, like useAnalytics and go from there.
The "polymorphic" approach would make it unnecessarily complex with no real gains to DX:
import { createElement } from "react";
// This:
const Polymorphic = ({ as: Component, ...props }) => <Component {...props} />;
// Is not different from just doing this:
const Polymorphic = ({ as, ...props }) => createElement(as, props);
// And when using any of those, your code would look something like this,
// which is not ideal:
<Polymorphic as="main">
<Polymorphic as="nav">
<Polymorphic as="a" href="https://example.com">
Hello
</Polymorphic>
</Polymorphic>
</Polymorphic>;

No overload matches this call

I'm currently working on a school project, and i have this error "No overload matches this call".
The whole error code is here:
No overload matches this call.
Overload 1 of 2, '(props: Omit<any, "initialRouteName" | "children" | "screenOptions" | "defaultScreenOptions"> & DefaultRouterOptions<string> & { children: ReactNode; screenOptions?: BottomTabNavigationOptions | ... 1 more ... | undefined; defaultScreenOptions?: BottomTabNavigationOptions | ... 1 more ... | undefined; }, context?: any): ReactElement<...> | ... 1 more ... | null', gave the following error.
Type '(route: { name: string;}) => { tabBarIcon: (size: number, color: string) => JSX.Element; }' is not assignable to type 'BottomTabNavigationOptions | ((props: { route: RouteProp<ParamListBase, string>; navigation: any; }) => BottomTabNavigationOptions) | undefined'.
Type '(route: { name: string;}) => { tabBarIcon: (size: number, color: string) => JSX.Element; }' is not assignable to type '(props: { route: RouteProp<ParamListBase, string>; navigation: any; }) => BottomTabNavigationOptions'.
Types of parameters 'route' and 'props' are incompatible.
Property 'name' is missing in type '{ route: RouteProp<ParamListBase, string>; navigation: any; }' but required in type '{ name: string; }'.
This error came up when i had to fix some any type errors in CreateScreenOptions:
Be aware that i have tsconfig.json sat to strict due to my teacher.
app.navigator.tsx:
import React from "react";
import { createBottomTabNavigator } from "#react-navigation/bottom-tabs";
import { Ionicons } from "#expo/vector-icons";
import { HomeNavigator } from "./home.navigator";
const Tab = createBottomTabNavigator();
const TAB_ICON: {[key: string]: any} = {
Hjem: "md-home",
};
const createScreenOptions = (route: {name: string}) => {
const iconName = TAB_ICON[route.name];
return {
tabBarIcon: ( size: number, color: string) => (
<Ionicons name={iconName} size={size} color={color} />
),
};
};
export const AppNavigator = () => (
<Tab.Navigator
screenOptions={createScreenOptions}
tabBarOptions={{
activeTintColor: "red",
inactiveTintColor: "blue",
}}
>
<Tab.Screen name="Hjem" component={HomeNavigator} />
</Tab.Navigator>
);
home.navigator.tsx:
import React from "react";
import { createStackNavigator } from "#react-navigation/stack";
import { HomeScreen } from "../../features/home/home.screen";
const HomeStack = createStackNavigator();
export const HomeNavigator = () => {
return (
<HomeStack.Navigator headerMode="none">
<HomeStack.Screen name="Home" component={HomeScreen} />
</HomeStack.Navigator>
);
};
home.screen.tsx:
import React from "react";
import { Text, StyleSheet } from "react-native";
export const HomeScreen = () => {
return(
<Text>Hello World!</Text>
)
}
The problem is here:
export const AppNavigator = () => (
<Tab.Navigator
screenOptions={createScreenOptions}
tabBarOptions={{
activeTintColor: "red",
inactiveTintColor: "blue",
}}
>
<Tab.Screen name="Hjem" component={HomeNavigator} />
</Tab.Navigator>
);
More specifically here:
screenOptions={createScreenOptions}
If you check the issue is that the the type of screen options uses a string as key
Check this issue: https://github.com/react-navigation/react-navigation/issues/7855
Also here is another potential issue:
const createScreenOptions = (route: {name: string}) => {
const iconName = TAB_ICON[route.name];
return {
tabBarIcon: ( size: number, color: string) => (
<Ionicons name={iconName} size={size} color={color} />
),
};
};
That function takes one parameter but you are calling it like this:
screenOptions={createScreenOptions}
And maybe you should be calling it like this:
screenOptions={createScreenOptions({name:"some name"})}

'AuthProvider' cannot be used as a JSX component

I attempt to make auth flow in Context Provider and have troubles with TypeScript.
Here is problem fragment of Provider code:
interface AuthUserProviderProps {
children?: React.ReactNode
}
interface AuthUserInterface {
token: string;
setToken: (value: string) => void;
loggedIn: boolean;
};
setBrand: (value: {}) => void;
}
export const authUser = createContext<AuthUserInterface | null >(null);
const AuthUserProvider = ({ children }: AuthUserProviderProps) => {
if (accessTokenVAR() && !jwtToken?.roles.toString().includes('admin')) {
return isBrowser ? window.location.replace('www.somewhere.com') : ''
}
return (
<authUser.Provider
value={{
token,
setToken,
loggedIn,
}}
>
{ children}
</authUser.Provider>
);
};
export default AuthUserProvider;
App.tsx
export default function App({ Component, pageProps }: AppProps) {
const apolloClient = useApollo(pageProps.initialApolloState);
return (
<ThemeProvider theme={TifTheme}>
<AuthUserProvider> <===This line throws error
<ApolloProvider client={apolloClient} >
<CssBaseline />
<Component {...pageProps} />
</ApolloProvider>
</AuthUserProvider>
</ThemeProvider>
);
}
The error is 'AuthUserProvider' cannot be used as a JSX component. Its return type 'void | "" | Element' is not a valid JSX element. Type 'void' is not assignable to type 'Element.' and it is caused by return redirect.
But I have no clue how to handle this error
Your analysis should be correct, it's the return of the "redirect". You can easily type properties of React functional components with React.FunctionComponent.
I would recommend to write a little useAccess hook and use that to get a boolean for display reasons or redirect the user with a useEffect. As you redirect to a different page, it shouldn't matter what the component returns.
So I modified your code to this, I made some changes and added comments, let me know if it helps.
import { createContext, FunctionComponent, useEffect, useMemo } from "react";
interface AuthUserInterface {
token: string;
setToken: (value: string) => void;
loggedIn: boolean;
setBrand: (value: any) => void;
}
const AuthUser = createContext<AuthUserInterface | null>(null);
const useAccess = (isBrowser: boolean) => {
const hasAccess = useMemo(
() => accessTokenVAR() && !jwtToken?.roles.toString().includes("admin"),
// TODO: check if that's correct, these variables were not in your answer,
// might need to add/pass them to the hook
[accessTokenVAR, jwtToken]
);
useEffect(() => {
// redirect here if has
if (hasAccess && isBrowser) window.location.replace("www.somewhere.com");
}, [isBrowser, hasAccess]);
return hasAccess;
};
interface AuthUserProviderProps {
isBrowser: boolean;
}
const AuthUserProvider: FunctionComponent<AuthUserProviderProps> = ({
children,
isBrowser
}) => {
const hasAccess = useAccess(isBrowser);
return (
<>
{!hasAccess && <p>"No access"</p>}
{hasAccess && (
<AuthUser.Provider
value={{
// TODO: values not in question, where do they come from
token,
setToken,
loggedIn
}}
>
{children}
</AuthUser.Provider>
)}
</>
);
};
export { AuthUserProvider as default, AuthUser };

How to pass the type of useState variables/setters from to child component in TypeScript

I have this parent component that uses useState:
export type Info = {
color: 'red' | 'yellow' | 'green' | 'blue'
isActive: boolean
}
const ParentComponent: React.FC =(props: any) => {
const [tab, setTab] = useState(0)
const [info, setInfo] = useState({
color: 'red',
isActive: false,
})
return (
<ChildComponent
tab={tab}
setTab={setTab}
info={info}
setInfo={setInfo}
/>
)
}
It renders this child component:
interface Props {
tab: number
setTab: (p: number) => void
info: Info
setInfo: (p: Info | ((p: Info) => any)) => void
}
const ChildComponent: React.FC<Props> = ({
tab,
setTab,
info,
setInfo,
}) => (
/* some JSX */
)
Right now I have to type all the props manually for ChildComponent, even though the types are already defined in ParentComponent. Is there a way to pass the typings of tab, setTab, info, and setInfo from ParentComponent to ChildComponent?
ChildComponent will not be able to directly infer the typings from ParentComponent. However, what you can do to achieve a similar result would be to define a common interface for both ParentComponent and ChildComponent.
First, you define a common interface/type for your ParentComponent state.
export interface CommonState {
tab: number;
info: Info;
}
Then, we use the interface when we declare the useState hooks in ParentComponent.
const [tab, setTab] = useState<CommonState['tab']>(0)
const [info, setInfo] = useState<CommonState['info']>({
color: 'red',
isActive: false,
});
Then, on your ChildComponent, instead of declaring a brand new type for the child, it could extend off the CommonState interface, such that the typings are shared.
interface Props extends CommonState {
setTab: (p: number) => void
setInfo: (p: Info | ((p: Info) => any)) => void
}

Resources