React Navigation custom route params and typescript - reactjs

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?

Related

Axios Error Response Using TypeScript, React and React-Query

I need to display the error message and if it has no error message just default it to an error status code. Problem right now is that it says Property 'message' does not exist on type 'MyErrorResponse'.
auth service
import { AxiosError } from 'axios'
import { useMutation } from 'react-query'
import { _axios, URL_TEMPLATES } from '../api'
export type LoginPayload = {
username: string;
password: string;
};
type LoginResponse = {
data: {
username: string;
email: string;
token: string;
};
};
type MyErrorResponse = {
errors: { detail: string }[];
};
export const useLogin = () => {
return useMutation<LoginResponse, AxiosError<MyErrorResponse>, LoginPayload>(
(payload) => _axios.post(URL_TEMPLATES.LOGIN, { ...payload })
)
}
component
const { mutate, error } = useLogin()
{error && (
<Typography variant='h6' sx={{ color: theme.palette.red, mt: 2 }}>
{error.response?.data.message}
</Typography>
)}
Your MyErrorResponse should be typed according what you return from the backend. In this case, it should be,
type MyErrorResponse = {
message: string
};
the hint is from the error you got, Property 'message' does not exist on type 'MyErrorResponse'. It's a typescript error that indicates you have invalid type.

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

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

TS2559: Type '{ children: never[]; }' has no properties in common with type 'IntrinsicAttributes & { post?: IPost | undefined; }'

I cant understand whats the probem is.
I have already saw answers to this Error here, I saw one of the issues of others
is a repetition of a function name, and not giving props to a component in the component tree. But I dont think I made those mistakes.
Ill be thankfull for help :)
the Error:
TS2559: Type '{ children: never[]; }' has no properties in common with type 'IntrinsicAttributes & { post?: IPost | undefined; }'.
28 |
29 | {posts}
30 |
| ^^^^
31 |
32 |
33 |
Feed component:
import React, { useEffect, useState } from 'react';
import { Outlet } from 'react-router';
import Post from './Post';
import {List} from './List'
async function getJSON(url: string) {
const res = await fetch(url);
return res.json();
}
export function Feed() {
const [posts, setPosts] = useState([{id: '', title: 'initial state'}])
const [page, setCount] = useState(0);
useEffect(() => {
async function setData() {
setPosts(await getJSON(`./data/posts.json?page=${page}`));
}
setData();
}, [page])
useEffect(() => {
console.log('render Feed');
console.log(posts)
})
return <div style={{backgroundColor:'#FAFAFA'}}>
<List>
{posts}
<Post>
</Post>
</List>
{/* <Post>
</Post> */}
<p>
<button onClick={() => setCount(page + 1)}>Page {page}</button>
</p>
</div>
}
Post component:
import React, { useEffect, useState } from 'react';
import { Link, useParams } from "react-router-dom";
import {List} from './List'
async function getJSON(url: string) {
const res = await fetch(url);
return res.json();
}
interface IPost {
id: string;
title: string;
}
export default function Post({ post }: { post?: IPost }) {
const [numLikes, addLike] = useState(0);
const [dynamicPost, setDynamicPost] = useState({ title: '', id: '' });
const params = useParams();
useEffect(() => {
async function setDynamicData() {
console.log('setDynamicData works')
setDynamicPost(await getJSON(`./data/post.${params.postId}.json`))
}
if (!post) {
setDynamicData();
}
console.log('post is set!')
}, [params.postId, post]);
return <div>
{post ? <div>
<PostBox>
<h2><Link style={{color: 'black', textDecoration: 'none' }} to={`/posts/${post?.id}`}>{post?.title || dynamicPost?.title}</Link></h2>
<p>Num likes {numLikes}</p>
<Like onLike={() => addLike(numLikes + 1)}>
<strong>Bla bla</strong>
</Like>
</PostBox>
</div> :
<div><h2 style={{ color: "red" }}>no posts yet</h2></div>}
</div>
}
interface IPropsLike {
onLike: () => void;
children: JSX.Element;
}
function Like({ onLike, children }: IPropsLike) {
return <button onClick={onLike}>{children}</button>
}
function PostBox({ children }: { children: JSX.Element[] }) {
return <div style={{
border: '4px solid black',
color:"black",
marginBottom: '1em',
width: '100%',
textAlign: 'center'}}>
{children}
</div>
}
The problem is you're giving Post child contents (a text node containing whitespace), but Post doesn't accept child content.
Here's a simpler replication:
interface IPost {
id: string;
title: string;
}
function Post({ post }: { post?: IPost }) {
return <div/>;
}
// v−−−− error here, because of the text node
const x = <Post post={{id: "x", title: "x"}}>
</Post>;
Playground link
Either:
Don't pass children to components that don't accept them:
<Post post={/*...*/} />
Playground link
or
Update Post so it accepts children:
export default function Post({ post, children }: { post?: IPost, children: ReactNode }) {
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^
// v−−−− no error here anymore, the text node is fine
const x = <Post post={{id: "x", title: "x"}}>
</Post>;
...and render the children in Post.
Playground link

react-aria and Typescript building a popover component error with value passing

Morning, all.
I have an issue I have been struggling with for a little while. I have been using react-aria to build a popover in Storybook. This popover is built in two components, the first one is the popover itself, this works fine:
import { StyledPopover } from './PopOver.styles';
import {
DismissButton,
FocusScope,
mergeProps,
useDialog,
useModal,
useOverlay,
} from 'react-aria';
import React, { forwardRef } from 'react';
import { useObjectRef } from '#react-aria/utils';
export interface PopoverProps {
title: string;
children: React.ReactNode;
isOpen: boolean;
onClose: () => void;
}
const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(
({ title, children, isOpen, onClose }, ref) => {
const forwardRef = useObjectRef(ref);
// Handle interacting outside the dialog and pressing
// the Escape key to close the modal.
const { overlayProps } = useOverlay(
{
onClose,
isOpen,
isDismissable: true,
},
forwardRef
);
// Hide content outside the modal from screen readers.
const { modalProps } = useModal();
// Get props for the dialog and its title
const { dialogProps, titleProps } = useDialog({}, forwardRef);
return (
<FocusScope restoreFocus>
<StyledPopover
{...mergeProps(overlayProps, dialogProps, modalProps)}
ref={ref}
>
<h3 {...titleProps} style={{ marginTop: 0 }}>
{title}
</h3>
{children}
<DismissButton onDismiss={onClose} />
</StyledPopover>
</FocusScope>
);
}
);
export { Popover };
Then I have the the button and the state:
import React from 'react';
import {
OverlayContainer,
useOverlayPosition,
useOverlayTrigger,
} from 'react-aria';
import { Button } from '../Button';
import { useOverlayTriggerState } from 'react-stately';
import { Popover } from 'Atoms/Popover/Popover';
export interface PopoverButtonProps {
title: string;
buttonText: string;
children: React.ReactNode;
disabled: boolean;
}
const PopoverButton = ({
buttonText,
title,
children,
disabled,
}: PopoverButtonProps) => {
const state: any = useOverlayTriggerState({});
const triggerRef = React.useRef<HTMLButtonElement>(null);
const overlayRef: any = React.useRef<HTMLDivElement>(null);
// Get props for the trigger and overlay. This also handles
// hiding the overlay when a parent element of the trigger scrolls
// (which invalidates the popover positioning).
const { triggerProps, overlayProps } = useOverlayTrigger(
{ type: 'dialog' },
state,
triggerRef
);
// Get popover positioning props relative to the trigger
const { overlayProps: positionProps } = useOverlayPosition({
targetRef: triggerRef,
overlayRef,
placement: 'top',
offset: 5,
isOpen: state.isOpen,
});
//
// const handleOnClick = (e: any) => {
// console.log(triggerProps);
// triggerProps.onPress && triggerProps.onPress(e);
// };
console.log(triggerProps);
return (
<>
<Button
onClick={(e: React.MouseEvent<HTMLInputElement>) =>
triggerProps.onPress && triggerProps.onPress(e)
}
style="secondary"
size="small"
disabled={disabled}
>
{buttonText}
</Button>
{state.isOpen && (
<OverlayContainer>
<Popover
{...overlayProps}
{...positionProps}
ref={overlayRef}
title={title}
isOpen={state.isOpen}
onClose={state.close}
>
{children}
</Popover>
</OverlayContainer>
)}
</>
);
};
export { PopoverButton };
Now, react-aria useButton takes an onPress, not an onClick.
So, in my Button component I have casted the onClick like this:
import classNames from 'classnames';
import { forwardRef } from 'react';
import { StyledButton } from './Button.styles';
import { useButton } from 'react-aria';
import { useObjectRef } from '#react-aria/utils';
import React from 'react';
export interface ButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'style'> {
children: React.ReactNode;
type?: 'submit' | 'button' | 'reset';
style?: 'primary' | 'secondary' | 'icon' | 'text';
size?: 'large' | 'medium' | 'small';
block?: boolean;
disabled?: boolean;
testId?: string;
onPress?: () => void;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
type,
style = 'primary',
size = 'large',
block = false,
disabled = false,
testId,
onPress,
...props
},
ref
) => {
const classes = classNames(style, `btn-${size}`, {
block,
});
const objRef = useObjectRef(ref);
const { buttonProps } = useButton({ onPress }, objRef);
return (
<StyledButton
{...buttonProps}
className={classes}
onClick={onPress}
type={type}
disabled={disabled}
data-testid={testId}
ref={ref}
{...props}
>
{children}
</StyledButton>
);
}
);
export { Button };
In my popoverButton component, I am passing (e) as it is required for react-aria:
onClick={(e: React.MouseEvent<HTMLInputElement>) =>
triggerProps.onPress && triggerProps.onPress(e)
}
However, I am getting these two errors -
on (e) -
Argument of type 'MouseEventHandler<HTMLButtonElement>' is not assignable to parameter of type 'PressEvent'.
Type 'MouseEventHandler<HTMLButtonElement>' is missing the following properties from type 'PressEvent': type, pointerType, target, shiftKey, and 3 more.ts(2345)
onClick -
Type '(e: React.MouseEventHandler<HTMLButtonElement>) => void | undefined' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
Types of parameters 'e' and 'event' are incompatible.
Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
Type 'MouseEvent<HTMLButtonElement, MouseEvent>' provides no match for the signature '(event: MouseEvent<HTMLButtonElement, MouseEvent>): void'.ts(2322)
index.d.ts(1446, 9): The expected type comes from property 'onClick' which is declared here on type 'IntrinsicAttributes & ButtonProps & RefAttributes<HTMLButtonElement>'
Now, the button works and the popup does appear, however it only disappears when clicking the button when it should dismiss when clicking anywhere on the screen. I think this is down to the issues I currently have with the onClick and onPress?
Any ideas?

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"})}

Resources