TypeScript Best Practices for React Developers TypeScript has become an essential tool for React developers, providing type safety, better IDE support, and improved code maintainability. In this guide, we’ll explore the best practices for using TypeScript effectively in React applications.
Component Type Definitions Functional Components 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import React from 'react' ;interface Props { title : string ; count : number ; isVisible ?: boolean ; } const MyComponent : React .FC <Props > = ({ title, count, isVisible = true } ) => { return ( <div > <h1 > {title}</h1 > {isVisible && <p > Count: {count}</p > } </div > ); }; const MyComponent = ({ title, count, isVisible = true }: Props ) => { return ( <div > <h1 > {title}</h1 > {isVisible && <p > Count: {count}</p > } </div > ); };
Props with Children 1 2 3 4 5 6 7 8 9 10 11 12 13 interface ContainerProps { className ?: string ; children : React .ReactNode ; } const Container = ({ className, children }: ContainerProps ) => { return <div className ={className} > {children}</div > ; }; interface ButtonGroupProps { children : React .ReactElement <ButtonProps > | React .ReactElement <ButtonProps >[]; }
State Management with TypeScript useState Hook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React , { useState } from 'react' ;interface User { id : number ; name : string ; email : string ; } const UserProfile = ( ) => { const [loading, setLoading] = useState (false ); const [user, setUser] = useState<User | null >(null ); const [users, setUsers] = useState<User []>([]); const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error' >('idle' ); return ( <div > {loading && <p > Loading...</p > } {user && <p > Welcome, {user.name}!</p > } </div > ); };
useReducer Hook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 interface State { count : number ; error : string | null ; } type Action = | { type : 'INCREMENT' } | { type : 'DECREMENT' } | { type : 'SET_ERROR' ; payload : string } | { type : 'RESET' }; const initialState : State = { count : 0 , error : null , }; const reducer = (state : State , action : Action ): State => { switch (action.type ) { case 'INCREMENT' : return { ...state, count : state.count + 1 , error : null }; case 'DECREMENT' : return { ...state, count : state.count - 1 , error : null }; case 'SET_ERROR' : return { ...state, error : action.payload }; case 'RESET' : return initialState; default : return state; } }; const Counter = ( ) => { const [state, dispatch] = useReducer (reducer, initialState); return ( <div > <p > Count: {state.count}</p > {state.error && <p > Error: {state.error}</p > } <button onClick ={() => dispatch({ type: 'INCREMENT' })}>+</button > <button onClick ={() => dispatch({ type: 'DECREMENT' })}>-</button > </div > ); };
Event Handlers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import React , { ChangeEvent , FormEvent , MouseEvent } from 'react' ;interface FormData { email : string ; password : string ; } const LoginForm = ( ) => { const [formData, setFormData] = useState<FormData >({ email : '' , password : '' , }); const handleInputChange = (e : ChangeEvent <HTMLInputElement > ) => { const { name, value } = e.target ; setFormData (prev => ({ ...prev, [name]: value, })); }; const handleSubmit = (e : FormEvent <HTMLFormElement > ) => { e.preventDefault (); }; const handleButtonClick = (e : MouseEvent <HTMLButtonElement > ) => { e.preventDefault (); }; return ( <form onSubmit ={handleSubmit} > <input type ="email" name ="email" value ={formData.email} onChange ={handleInputChange} /> <input type ="password" name ="password" value ={formData.password} onChange ={handleInputChange} /> <button type ="submit" onClick ={handleButtonClick} > Login </button > </form > ); };
Custom Hooks with TypeScript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import { useState, useEffect } from 'react' ;function useApi<T>(url : string ) { const [data, setData] = useState<T | null >(null ); const [loading, setLoading] = useState (true ); const [error, setError] = useState<string | null >(null ); useEffect (() => { const fetchData = async ( ) => { try { setLoading (true ); const response = await fetch (url); if (!response.ok ) { throw new Error ('Network response was not ok' ); } const result : T = await response.json (); setData (result); } catch (err) { setError (err instanceof Error ? err.message : 'An error occurred' ); } finally { setLoading (false ); } }; fetchData (); }, [url]); return { data, loading, error }; } interface Post { id : number ; title : string ; body : string ; } const BlogPost = ({ postId }: { postId: number } ) => { const { data : post, loading, error } = useApi<Post >(`/api/posts/${postId} ` ); if (loading) return <div > Loading...</div > ; if (error) return <div > Error: {error}</div > ; if (!post) return <div > Post not found</div > ; return ( <article > <h1 > {post.title}</h1 > <p > {post.body}</p > </article > ); };
Context API with TypeScript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import React , { createContext, useContext, useReducer, ReactNode } from 'react' ;interface AuthState { user : User | null ; isAuthenticated : boolean ; loading : boolean ; } type AuthAction = | { type : 'LOGIN_START' } | { type : 'LOGIN_SUCCESS' ; payload : User } | { type : 'LOGIN_FAILURE' } | { type : 'LOGOUT' }; interface AuthContextType { state : AuthState ; login : (email : string , password : string ) => Promise <void >; logout : () => void ; } const AuthContext = createContext<AuthContextType | undefined >(undefined );const authReducer = (state : AuthState , action : AuthAction ): AuthState => { switch (action.type ) { case 'LOGIN_START' : return { ...state, loading : true }; case 'LOGIN_SUCCESS' : return { user : action.payload , isAuthenticated : true , loading : false }; case 'LOGIN_FAILURE' : return { user : null , isAuthenticated : false , loading : false }; case 'LOGOUT' : return { user : null , isAuthenticated : false , loading : false }; default : return state; } }; interface AuthProviderProps { children : ReactNode ; } export const AuthProvider = ({ children }: AuthProviderProps ) => { const [state, dispatch] = useReducer (authReducer, { user : null , isAuthenticated : false , loading : false , }); const login = async (email : string , password : string ) => { dispatch ({ type : 'LOGIN_START' }); try { const user = await authenticateUser (email, password); dispatch ({ type : 'LOGIN_SUCCESS' , payload : user }); } catch (error) { dispatch ({ type : 'LOGIN_FAILURE' }); } }; const logout = ( ) => { dispatch ({ type : 'LOGOUT' }); }; return ( <AuthContext.Provider value ={{ state , login , logout }}> {children} </AuthContext.Provider > ); }; export const useAuth = ( ) => { const context = useContext (AuthContext ); if (context === undefined ) { throw new Error ('useAuth must be used within an AuthProvider' ); } return context; };
Advanced Patterns Generic Components 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 interface ListProps <T> { items : T[]; renderItem : (item : T ) => React .ReactNode ; keyExtractor : (item : T ) => string | number ; } function List <T>({ items, renderItem, keyExtractor }: ListProps <T>) { return ( <ul > {items.map(item => ( <li key ={keyExtractor(item)} > {renderItem(item)} </li > ))} </ul > ); } const users : User [] = [];const posts : Post [] = [];<List items ={users} renderItem ={user => <span > {user.name}</span > } keyExtractor={user => user.id} /> <List items ={posts} renderItem ={post => <span > {post.title}</span > } keyExtractor={post => post.id} />
Conditional Props 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type ButtonProps = { children : React .ReactNode ; onClick : () => void ; } & ( | { variant : 'primary' ; color ?: never } | { variant : 'secondary' ; color : 'blue' | 'red' | 'green' } ); const Button = ({ children, onClick, variant, ...props }: ButtonProps ) => { return ( <button onClick ={onClick} className ={ `btn btn- ${variant } ${ 'color ' in props ? `btn- ${props.color }` : '' }`} > {children} </button > ); }; <Button variant ="primary" onClick ={() => {}}>Primary</Button > <Button variant ="secondary" color ="blue" onClick ={() => {}}>Secondary</Button >
Best Practices Summary 1. Use Strict TypeScript Configuration 1 2 3 4 5 6 7 8 9 10 { "compilerOptions" : { "strict" : true , "noImplicitAny" : true , "noImplicitReturns" : true , "noUnusedLocals" : true , "noUnusedParameters" : true } }
2. Prefer Type Inference 1 2 3 4 5 const [count, setCount] = useState<number >(0 );const [count, setCount] = useState (0 );
3. Use Utility Types 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface User { id : number ; name : string ; email : string ; password : string ; } type PublicUser = Pick <User , 'id' | 'name' | 'email' >;type UserWithoutPassword = Omit <User , 'password' >;type PartialUser = Partial <User >;type RequiredUser = Required <User >;
4. Create Reusable Type Definitions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export interface ApiResponse <T> { data : T; message : string ; success : boolean ; } export interface PaginatedResponse <T> extends ApiResponse <T[]> { pagination : { page : number ; limit : number ; total : number ; }; } const { data : users } = useApi<PaginatedResponse <User >>('/api/users' );
Conclusion TypeScript significantly improves the React development experience by providing type safety, better IDE support, and catching errors at compile time. By following these best practices, you’ll write more maintainable and robust React applications.
Key takeaways:
Use proper type definitions for props and state
Leverage TypeScript’s type inference when possible
Create reusable type definitions
Use utility types for common patterns
Implement proper error handling with typed contexts
Start implementing these patterns in your React projects and experience the benefits of type-safe development! 🎯