Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/skyybbanerjee/react-with-typescript

Using React ⚛️ with Typescript🟦
https://github.com/skyybbanerjee/react-with-typescript

axios css3 html5 react-hooks react-query react-query-devtools reactjs redux-toolkit typescript

Last synced: 3 days ago
JSON representation

Using React ⚛️ with Typescript🟦

Awesome Lists containing this project

README

        

This GitHub project provides a comprehensive guide to integrating TypeScript with React. It covers initial setup using vite and TypeScript, demonstrates various React & TypeScript concepts through clear examples, and delves into more advanced topics. Key aspects include:

- Component Structure and TypeScript Integration: Explains how to correctly type React components, manage return types, and handle potential TypeScript errors.
- Prop Handling and Typing: Offers insights on inline typing, using interfaces, and managing children props with TypeScript.
- State Management: Teaches TypeScript type inference in state management, showcasing various useState examples.
- Event Handling: Guides on typing events in React, such as form submissions and input changes.
- Complex Component Structures: Discusses complex use cases like conditional prop rendering based on type values.
- Context API with TypeScript: Provides a deep dive into using React's Context API in a TypeScript environment.
- Reducers and Global State Management: Includes examples of setting up reducers with TypeScript and using them in React components.
- Data Fetching: Demonstrates fetching data with TypeScript validation using tools like Zod, Axios, and React Query.
- Redux Toolkit (RTK) Integration: Shows how to integrate Redux Toolkit in a TypeScript-React setup, including creating slices and using hooks.
- Practical Application with Task Management: Concludes with a practical task management application, emphasizing localStorage use and handling task state.

Each section is presented with relevant code snippets and explanations, making it an ideal resource for developers looking to deepen their understanding of TypeScript in React applications.

## Setup

```sh
npm create vite@latest react-typescript -- --template react-ts
```

## Remove Boilerplate and Get Assets

# React & Typescript

- .tsx - file extension

## 01 - Component Return

- TypeScript infers JSX.Element, helps if no return

```tsx
// TypeScript infers JSX.Element
// this will trigger error
function Component() {}
export default Component;
```

- set function return type

```tsx
function Component(): JSX.Element | null | string {
return null;
return 'hello';
return

hello from typescript

;
}
export default Component;
```

## 02- Props

```tsx
function App() {
return (



);
}

export default App;
```

- inline types

```tsx
function Component({ name, id }: { name: string; id: number }) {
return (


Name : {name}


ID : {id}



);
}
export default Component;
```

- type or interface
- props object or {}

```tsx
type ComponentProps = {
name: string;
id: number;
};

function Component({ name, id }: ComponentProps) {
return (


Name : {name}


ID : {id}



);
}
export default Component;
```

- children prop

```tsx
function App() {
return (


hello world




);
}

export default App;
```

- React.ReactNode
- PropsWithChildren

```tsx
import { type PropsWithChildren } from 'react';

type ComponentProps = {
name: string;
id: number;
children: React.ReactNode;
};

// type ComponentProps = PropsWithChildren<{
// name: string;
// id: number;
// }>;

function Component({ name, id, children }: ComponentProps) {
return (


Name : {name}


ID : {id}


{children}

);
}
export default Component;
```

## 03 - State

- typescript infers primitive types
- by default [] is type never

```tsx
import { useState } from 'react';

function Component() {
const [text, setText] = useState('shakeAndBake');
const [number, setNumber] = useState(1);
const [list, setList] = useState([]);

return (


hello from typescript


{
// setText(1);
// setNumber('hello');
// setList([1, 3]);
setList(['hello', 'world']);
}}
>
click me


);
}
export default Component;
```

```tsx
import { useState } from 'react';

type Link = {
id: number;
url: string;
text: string;
};

const navLinks: Link[] = [
{
id: 1,
url: 'https://reactjs.org',
text: 'react docs',
},
{
id: 2,
url: 'https://reactrouter.com',
text: 'react router docs',
},
{
id: 3,
url: 'https://reacttraining.com',
// remove text property to see the error
text: 'react training',
},
];

function Component() {
const [text, setText] = useState('shakeAndBake');
const [number, setNumber] = useState(1);
const [list, setList] = useState([]);
const [links, setLinks] = useState(navLinks);
return (


hello from typescript


{
// setText(1);
// setNumber('hello');
// setList([1, 3]);
// setList(['hello', 'world']);
// setLinks([...links, { id: 4, url: 'hello', someValue: 'hello' }])
setLinks([...links, { id: 4, url: 'hello', text: 'hello' }]);
}}
>
click me


);
}
export default Component;
```

```tsx

```

## 04 - Events

- inline function infers object type

When you provide the exact HTML element type in the angle brackets (<>), like HTMLInputElement in your case, you're telling TypeScript exactly what kind of element the event is coming from. This helps TypeScript provide accurate suggestions and error checking based on the properties and methods that are specific to that kind of element. For example, an HTMLInputElement has properties like value and checked that other elements don't have. By specifying the exact element type, TypeScript can help you avoid mistakes and write safer code.

```tsx
import { useState } from 'react';

function Component() {
const [text, setText] = useState('');
const [email, setEmail] = useState('');

const handleChange = (e: React.ChangeEvent) => {
console.log(e.target.value);
setEmail(e.target.value);
};

return (

events example



setText(e.target.value)}
/>



submit



);
}
export default Component;
```

```tsx
import { useState } from 'react';

type Person = {
name: string;
};

function Component() {
const [text, setText] = useState('');
const [email, setEmail] = useState('');

const handleChange = (e: React.ChangeEvent) => {
console.log(e.target.value);
setEmail(e.target.value);
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// const formData = new FormData(e.currentTarget);
const formData = new FormData(e.target as HTMLFormElement);
// const data = Object.fromEntries(formData);
const text = formData.get('text') as string;
const person: Person = { name: text };
};

return (

events example



setText(e.target.value)}
/>



submit



);
}
export default Component;
```

The FormData API is a web technology that allows developers to easily construct and manage sets of key/value pairs representing form fields and their values. It is commonly used to send form data, including files, from a client (such as a web browser) to a server in a format that can be easily processed. The FormData API provides a way to programmatically create and manipulate form data, making it useful for AJAX requests and handling file uploads in web applications.

## 05 - Challenge - Profile Card

- initial approach (won't work as expected)

```tsx
type ProfileCardProps = {
type: 'basic' | 'advanced';
name: string;
email?: string;
};

function Component(props: ProfileCardProps) {
const { type, name, email } = props;

const alertType = type === 'basic' ? 'success' : 'danger';
const className = `alert alert-${alertType}`;
return (

user : {name}


{email &&

email : {email}

}

);
}
export default Component;
```

- another approach (won't work as expected)

```tsx
type ProfileCardProps = {
type: 'basic' | 'advanced';
name: string;
email?: string;
};

function Component(props: ProfileCardProps) {
const { type, name, email } = props;

const alertType = type === 'basic' ? 'success' : 'danger';
const className = `alert alert-${alertType}`;
return (

user : {name}


{type === advanced ?

email : {email}

: null}

);
}
export default Component;
```

- final approach

```tsx
type BasicProfileCardProps = {
type: 'basic';
name: string;
};

type AdvancedProfileCardProps = {
type: 'advanced';
name: string;
email: string;
};
type ProfileCardProps = BasicProfileCardProps | AdvancedProfileCardProps;
function Component(props: ProfileCardProps) {
const { type, name } = props;
if (type === 'basic')
return (

user : {name}



);

return (

user : {name}


email : {props.email}



);
}
export default Component;
```

## 06 - Context

- basic context

```tsx
import { createContext, useContext } from 'react';

const ThemeProviderContext = createContext<{ name: string } | undefined>(
undefined
);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (

{children}

);
}

export const useTheme = () => {
const context = useContext(ThemeProviderContext);

if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider');

return context;
};
```

basic-index.tsx

```tsx
import { useTheme, ThemeProvider } from './basic-context';

function ParentComponent() {
return (



);
return ;
}

function Component() {
const context = useTheme();
console.log(context);

return (


random component



);
}
export default ParentComponent;
```

context.tsx

```tsx
import { createContext, useState, useContext } from 'react';

type Theme = 'light' | 'dark' | 'system';

type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};

const ThemeProviderContext = createContext(
undefined
);

type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
};

export function ThemeProvider({
children,
defaultTheme = 'system',
}: ThemeProviderProps) {
const [theme, setTheme] = useState(defaultTheme);
return (

{children}

);
}

export const useTheme = () => {
const context = useContext(ThemeProviderContext);

if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider');

return context;
};
```

Component.tsx

```tsx
import { useTheme, ThemeProvider } from './context';

function ParentComponent() {
return (



);
return ;
}

function Component() {
const context = useTheme();
console.log(context);

return (


random component


{
if (context.theme === 'dark') {
context.setTheme('system');
return;
}
context.setTheme('dark');
}}
className='btn btn-center'
>
toggle theme


);
}
export default ParentComponent;
```

## 07 - Reducers

- starter code

```tsx
function Component() {
return (


Count: 0


Status: Active


console.log('increment')} className='btn'>
Increment

console.log('decrement')} className='btn'>
Decrement

console.log('reset')} className='btn'>
Reset



console.log('set status to active')}
className='btn'
>
Set Status to Active

console.log('set status to inactive')}
>
Set Status to Inactive



);
}
export default Component;
```

- reducer setup

reducer.ts

```ts
export type CounterState = {
count: number;
status: string;
};

export const initialState: CounterState = {
count: 0,
status: 'Pending...',
};

export const counterReducer = (
state: CounterState,
action: any
): CounterState => {
return state;
};
```

index.tsx

```tsx
import { useReducer } from 'react';
import { counterReducer, initialState } from './reducer';

function Component() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (


Count: {state.count}


Status: {state.status}



);
}
```

- setup count action

reducer

```ts
type UpdateCountAction = {
type: 'increment' | 'decrement' | 'reset';
};

// Extend the union type for all possible actions
type CounterAction = UpdateCountAction;

export const counterReducer = (
state: CounterState,
action: CounterAction
): CounterState => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return { ...state, count: 0 };
default:
return state;
}
};
```

index.tsx

```tsx


dispatch({ type: 'increment' })} className='btn'>
Increment

dispatch({ type: 'decrement' })} className='btn'>
Decrement

dispatch({ type: 'reset' })} className='btn'>
Reset


```

- setup active action

reducer.ts

```ts
export type CounterState = {
count: number;
status: string;
};

export const initialState: CounterState = {
count: 0,
status: 'Pending...',
};

type UpdateCountAction = {
type: 'increment' | 'decrement' | 'reset';
};
type SetStatusAction = {
type: 'setStatus';
payload: 'active' | 'inactive';
};

// Extend the union type for all possible actions
type CounterAction = UpdateCountAction | SetStatusAction;

export const counterReducer = (
state: CounterState,
action: CounterAction
): CounterState => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return { ...state, count: 0 };
case 'setStatus':
return { ...state, status: action.payload };
default:
const unhandledActionType: never = action;
throw new Error(
`Unexpected action type: ${unhandledActionType}. Please double check the counter reducer.`
);
}
};
```

```tsx
import { useReducer } from 'react';
import { counterReducer, initialState } from './reducer';

function Component() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (


Count: {state.count}


Status: {state.status}


dispatch({ type: 'increment' })} className='btn'>
Increment

dispatch({ type: 'decrement' })} className='btn'>
Decrement

dispatch({ type: 'reset' })} className='btn'>
Reset



dispatch({ type: 'setStatus', payload: 'active' })}
className='btn'
>
Set Status to Active

dispatch({ type: 'setStatus', payload: 'inactive' })}
>
Set Status to Inactive



);
}
export default Component;
```

## 08 - Fetch Data

- reference data fetching in typescript-tutorial

[Zod](https://zod.dev/)
[React Query](https://tanstack.com/query/latest/docs/framework/react/overview)
[Axios](https://axios-http.com/docs/intro)

```sh
npm i zod axios @tanstack/react-query

```

```tsx
import { useState, useEffect } from 'react';
const url = 'https://www.course-api.com/react-tours-project';

function Component() {
// tours
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(null);

useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch tours...`);
}

const rawData = await response.json();
console.log(rawData);
} catch (error) {
const message =
error instanceof Error ? error.message : 'there was an error...';
setIsError(message);
} finally {
setIsLoading(false);
}
};

fetchData();
}, []);

if (isLoading) {
return

Loading...

;
}

if (isError) {
return

Error: {isError}

;
}

return (


Tours



);
}
export default Component;
```

types.ts

```ts
import { z } from 'zod';

export const tourSchema = z.object({
id: z.string(),
name: z.string(),
image: z.string(),
info: z.string(),
price: z.string(),
// someValue: z.string(),
});

export type Tour = z.infer;
```

index-fetch.tsx

```tsx
import { useState, useEffect } from 'react';
const url = 'https://www.course-api.com/react-tours-project';
import { type Tour, tourSchema } from './types';
function Component() {
// tours
const [tours, setTours] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(null);

useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch tours...`);
}
const rawData: Tour[] = await response.json();
const result = tourSchema.array().safeParse(rawData);

if (!result.success) {
console.log(result.error.message);
throw new Error(`Failed to parse tours`);
}
setTours(result.data);
} catch (error) {
const message =
error instanceof Error ? error.message : 'there was an error...';
setIsError(message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);

if (isLoading) {
return

Loading...

;
}
if (isError) {
return

Error {isError}

;
}

return (


Tours


{tours.map((tour) => {
return (


{tour.name}


);
})}

);
}
export default Component;
```

- React Query

types.ts

```ts
import { z } from 'zod';
import axios from 'axios';
const url = 'https://course-api.com/react-tours-project';

export const tourSchema = z.object({
id: z.string(),
name: z.string(),
image: z.string(),
info: z.string(),
price: z.string(),
// someValue: z.string(),
});

export type Tour = z.infer;

export const fetchTours = async (): Promise => {
const response = await axios.get(url);
const result = tourSchema.array().safeParse(response.data);
if (!result.success) {
throw new Error('Parsing failed');
}
return result.data;
};
```

main.tsx

```tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(



);
```

index.tsx

```tsx
import { fetchTours } from './types';
import { useQuery } from '@tanstack/react-query';

function Component() {
const {
isPending,
isError,
error,
data: tours,
} = useQuery({
queryKey: ['tours'],
queryFn: fetchTours,
});

if (isPending) return

Loading...

;
if (isError) return

Error : {error.message}

;
return (

Tours


{tours.map((tour) => {
return (


{tour.name}


);
})}

);
}

export default Component;
```

## 09 - RTK

```tsx
function Component() {
return (


Count: 0


Status: Pending


console.log('increment')} className='btn'>
Increment

console.log('decrement')} className='btn'>
Decrement

console.log('reset')} className='btn'>
Reset



console.log('active')} className='btn'>
Set Status to Active

console.log('inactive')}>
Set Status to Inactive



);
}
export default Component;
```

- counterSlice.ts

```ts
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';

type CounterStatus = 'active' | 'inactive' | 'pending...';

type CounterState = {
count: number;
status: CounterStatus;
};

const initialState: CounterState = {
count: 0,
status: 'pending...',
};

export const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
reset: (state) => {
state.count = 0;
},
setStatus: (state, action: PayloadAction) => {
state.status = action.payload;
},
},
});

export const { increment, decrement, reset, setStatus } = counterSlice.actions;

export default counterSlice.reducer;
```

store.ts

```ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './starter/09-rtk/counterSlice';
// ...

export const store = configureStore({
reducer: {
counter: counterReducer,
},
});

export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
```

type RootState represents the type of the state stored in your Redux store. ReturnType is a utility type provided by TypeScript that can get the return type of a function. store.getState is a function that returns the current state stored in the Redux store. So ReturnType is the type of the state returned by store.getState, which is the type of the state in your Redux store.

type AppDispatch represents the type of the dispatch function in your Redux store. store.dispatch is the function you use to dispatch actions in Redux. typeof store.dispatch gets the type of this function. So AppDispatch is the type of the dispatch function in your Redux store.

hooks.ts

```ts
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook = useSelector;
```

export const useAppDispatch: () => AppDispatch = useDispatch;

This line is creating a custom hook called useAppDispatch that wraps around the useDispatch hook from Redux. The useDispatch hook returns the dispatch function from the Redux store. By creating a custom hook useAppDispatch, you can ensure that the dispatch function is correctly typed with your application's specific dispatch type (AppDispatch).

export const useAppSelector: TypedUseSelectorHook = useSelector;

This line is creating a custom hook called useAppSelector that wraps around the useSelector hook from Redux. The useSelector hook allows you to extract data from the Redux store state. By creating a custom hook useAppSelector, you can ensure that the selector functions passed to this hook are correctly typed with your application's specific state type (RootState).

main.tsx

```tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { store } from './store';
import { Provider } from 'react-redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(



);
```

index.tsx

```tsx
import { useAppSelector, useAppDispatch } from '../../hooks';
import { decrement, increment, reset, setStatus } from './counterSlice';
function Component() {
const { count, status } = useAppSelector((state) => state.counter);
const dispatch = useAppDispatch();
return (


Count: {count}


Status: {status}


dispatch(increment())} className='btn'>
Increment

dispatch(decrement())} className='btn'>
Decrement

dispatch(reset())} className='btn'>
Reset



dispatch(setStatus('active'))} className='btn'>
Set Status to Active

dispatch(setStatus('inactive'))}>
Set Status to Inactive



);
}
export default Component;
```

## Challenge - Task Application

### Setup

- Create the following in './starter/10-tasks':
- `Form.tsx` (with a basic return)
- `List.tsx` (with a basic return)
- `types.ts`
- Export a type named 'Task' with the following properties:
- `id: string`
- `description: string`
- `isCompleted: boolean`
- In `index.tsx`, import 'Task' type and set up a state value of type 'Task[]'.
- Also, import and render 'Form' and 'List' in `index.tsx`.

### Form

- Create a form with a single input.
- Set up a controlled input.
- Set up a form submit handler and ensure it checks for empty values.

### Add Task

- In `index.tsx`, create an 'addTask' function that adds a new task to the list.
- Pass 'addTask' as a prop to 'Form'.
- In 'Form', set up the correct type and invoke 'addTask' if the input has a value.

### Toggle Task

- In `index.tsx`, create a 'toggleTask' function that toggles 'isCompleted'.
- Pass the function and list as props to 'List'.
- In `List.tsx`:
- Set up the correct type for props.
- Render the list.
- Set up a checkbox in each item and add an 'onChange' handler.
- Invoke the 'toggleTask' functionality.

### Local Storage

- Incorporate LocalStorage into the application.

## 10 - Tasks

- create Form, List components

types.ts

```ts
export type Task = {
id: string;
description: string;
isCompleted: boolean;
};
```

index.tsx

```tsx
import { useEffect, useState } from 'react';
import Form from './Form';
import List from './List';
import { type Task } from './types';

function Component() {
const [tasks, setTasks] = useState([]);

return (




);
}
export default Component;
```

Form.tsx

```tsx
import { useState } from 'react';
import { type Task } from './types';

function Form() {
const [text, setText] = useState('');

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!text) {
alert('please enter a task');
return;
}
// add task
setText('');
};
return (

{
setText(e.target.value);
}}
/>

add task


);
}
export default Form;
```

index.tsx

```tsx
import { useEffect, useState } from 'react';
import Form from './Form';
import List from './List';
import { type Task } from './types';

function Component() {
const [tasks, setTasks] = useState([]);

const addTask = (task: Task) => {
setTasks([...tasks, task]);
};

return (





);
}
export default Component;
```

Form.tsx

```tsx
import { useState } from 'react';
import { type Task } from './types';

type FormProps = {
addTask: (task: Task) => void;
};

function Form({ addTask }: FormProps) {
const [text, setText] = useState('');

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!text) {
alert('please enter a task');
return;
}
addTask({
id: new Date().getTime().toString(),
description: text,
isCompleted: false,
});
setText('');
};
return (

{
setText(e.target.value);
}}
/>

add task


);
}
export default Form;
```

index.tsx

```tsx
const toggleTask = ({ id }: { id: string }) => {
setTasks(
tasks.map((task) => {
if (task.id === id) {
return { ...task, isCompleted: !task.isCompleted };
}
return task;
})
);
};
return (





);
```

List.tsx

```tsx
import { type Task } from './types';

type ListProps = {
tasks: Task[];
toggleTask: ({ id }: { id: string }) => void;
};

const List = ({ tasks, toggleTask }: ListProps) => {
return (


    {tasks.map((task) => {
    return (

  • {task.description}


    {
    toggleTask({ id: task.id });
    }}
    />

  • );
    })}

);
};
export default List;
```

index.tsx

```tsx
import { useEffect, useState } from 'react';
import Form from './Form';
import List from './List';
import { type Task } from './types';

// Load tasks from localStorage
function loadTasks(): Task[] {
const storedTasks = localStorage.getItem('tasks');
return storedTasks ? JSON.parse(storedTasks) : [];
}

function updateStorage(tasks: Task[]): void {
localStorage.setItem('tasks', JSON.stringify(tasks));
}

function Component() {
const [tasks, setTasks] = useState(() => loadTasks());

const addTask = (task: Task) => {
setTasks([...tasks, task]);
};

const toggleTask = ({ id }: { id: string }) => {
setTasks(
tasks.map((task) => {
if (task.id === id) {
return { ...task, isCompleted: !task.isCompleted };
}
return task;
})
);
};
useEffect(() => {
updateStorage(tasks);
}, [tasks]);
return (





);
}
export default Component;
```