Advanced TypeScript Patterns for React Applications

Explore advanced TypeScript patterns and techniques to build more robust and maintainable React applications.

Frontend Development
18 min read
Advanced TypeScript Patterns for React Applications

Advanced TypeScript Patterns for React Applications

TypeScript has become an essential tool for building robust React applications. In this article, we'll explore advanced patterns and techniques that can help you write more maintainable and type-safe code.

Why TypeScript for React?

Before diving into advanced patterns, let's understand why TypeScript is so valuable for React development:

  1. Type Safety: Catches errors during development rather than runtime
  2. Better IDE Experience: Enhanced autocompletion, navigation, and refactoring
  3. Self-Documenting Code: Props and state are explicitly typed
  4. Enhanced Component API Design: Forces you to think about component interfaces

If you're already using TypeScript with React, you might have noticed that basic type annotations are just the beginning. Let's explore more advanced patterns that can truly level up your development experience.

Generic Components: Step-by-Step Implementation

Generic components allow you to create highly reusable components that maintain type safety across different data types. Let's build a complete example:

1. Define the Generic Component Interface

Start by defining your component's props using generics:

interface DataListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
  emptyMessage?: string;
  loading?: boolean;
  onItemClick?: (item: T) => void;
}

2. Implement the Component

function DataList<T>({
  items,
  renderItem,
  keyExtractor,
  emptyMessage = "No items available",
  loading = false,
  onItemClick
}: DataListProps<T>) {
  const handleItemClick = (item: T) => {
    if (onItemClick) {
      onItemClick(item);
    }
  };

  if (loading) {
    return <div className="loading">Loading data...</div>;
  }

  if (items.length === 0) {
    return <div className="empty-message">{emptyMessage}</div>;
  }

  return (
    <div className="data-list">
      {items.map((item) => (
        <div 
          key={keyExtractor(item)} 
          className="list-item"
          onClick={() => handleItemClick(item)}
        >
          {renderItem(item)}
        </div>
      ))}
    </div>
  );
}

3. Usage with Different Data Types

The real power of generic components is that they maintain type safety across various data types:

// For users
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

const UserList = () => {
  const users: User[] = [...]; // Your user data
  
  return (
    <DataList<User>
      items={users}
      renderItem={(user) => (
        <div className="user-card">
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <span className="badge">{user.role}</span>
        </div>
      )}
      keyExtractor={(user) => user.id}
      onItemClick={(user) => console.log(`Clicked on user: ${user.name}`)}
    />
  );
};

// For products
interface Product {
  productId: number;
  title: string;
  price: number;
  inStock: boolean;
}

const ProductList = () => {
  const products: Product[] = [...]; // Your product data
  
  return (
    <DataList<Product>
      items={products}
      renderItem={(product) => (
        <div className="product-item">
          <h3>{product.title}</h3>
          <p>${product.price.toFixed(2)}</p>
          {product.inStock ? (
            <span className="in-stock">In Stock</span>
          ) : (
            <span className="out-of-stock">Out of Stock</span>
          )}
        </div>
      )}
      keyExtractor={(product) => product.productId.toString()}
      emptyMessage="No products found"
    />
  );
};

4. Extending with Constraints

You can add constraints to your generic type to ensure it has certain properties:

interface WithId {
  id: string | number;
}

function DataListWithId<T extends WithId>({
  items,
  renderItem,
  // No keyExtractor needed since we know T has an id
}: Omit<DataListProps<T>, 'keyExtractor'>) {
  return (
    <div className="data-list">
      {items.map((item) => (
        <div key={item.id.toString()}>
          {renderItem(item)}
        </div>
      ))}
    </div>
  );
}

Custom Hooks with Type Safety: Implementation Guide

Custom hooks are a powerful way to extract reusable logic in React components. TypeScript enhances them with type safety.

1. Building a Comprehensive useAsync Hook

This enhanced async hook handles loading, error, and success states with proper typing:

type AsyncStatus = 'idle' | 'pending' | 'success' | 'error';

interface AsyncState<T> {
  status: AsyncStatus;
  data: T | null;
  error: Error | null;
}

interface AsyncActions<T> {
  execute: (params?: any) => Promise<void>;
  reset: () => void;
  setData: (data: T) => void;
}

function useAsync<T>(
  asyncFn: (...args: any[]) => Promise<T>,
  immediate = false,
  initialData: T | null = null
): [AsyncState<T>, AsyncActions<T>] {
  const [state, setState] = useState<AsyncState<T>>({
    status: 'idle',
    data: initialData,
    error: null,
  });

  const execute = useCallback(
    async (params?: any) => {
      setState((prevState) => ({
        ...prevState,
        status: 'pending',
        error: null,
      }));
      
      try {
        const data = await asyncFn(params);
        setState({ status: 'success', data, error: null });
        return data;
      } catch (error) {
        setState({
          status: 'error',
          data: null,
          error: error instanceof Error ? error : new Error(String(error)),
        });
        throw error;
      }
    },
    [asyncFn]
  );

  const reset = useCallback(() => {
    setState({
      status: 'idle',
      data: initialData,
      error: null,
    });
  }, [initialData]);

  const setData = useCallback((data: T) => {
    setState((prevState) => ({
      ...prevState,
      data,
      status: 'success',
    }));
  }, []);

  useEffect(() => {
    if (immediate) {
      execute();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [immediate]);

  return [
    state,
    { execute, reset, setData },
  ];
}

2. Using the Hook in Components

Here's how to use this hook in a component:

interface User {
  id: string;
  name: string;
  email: string;
  profilePicture?: string;
}

async function fetchUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch user: ${response.statusText}`);
  }
  return response.json();
}

function UserProfile({ userId }: { userId: string }) {
  const [
    { status, data: user, error },
    { execute }
  ] = useAsync<User>(() => fetchUser(userId), true);

  const handleRefresh = () => {
    execute();
  };

  return (
    <div className="user-profile">
      {status === 'pending' && <Loading />}
      
      {status === 'error' && (
        <div className="error-container">
          <p>Error loading user: {error?.message}</p>
          <button onClick={handleRefresh}>Retry</button>
        </div>
      )}
      
      {status === 'success' && user && (
        <>
          <img
            src={user.profilePicture || '/default-avatar.png'}
            alt={`${user.name}'s profile`}
            className="profile-picture"
          />
          <h2>{user.name}</h2>
          <p>{user.email}</p>
          <button onClick={handleRefresh}>Refresh</button>
        </>
      )}
    </div>
  );
}

3. Creating a useLocalStorage Hook with TypeScript

Here's another custom hook example for type-safe localStorage usage:

function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
  // State to store our value
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') {
      return initialValue;
    }
    
    try {
      // Get from local storage by key
      const item = window.localStorage.getItem(key);
      // Parse stored json or if none return initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });
  
  // Return a wrapped version of useState's setter function that
  // persists the new value to localStorage
  const setValue = useCallback((value: T | ((val: T) => T)) => {
    try {
      // Allow value to be a function so we have same API as useState
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      
      // Save state
      setStoredValue(valueToStore);
      
      // Save to local storage
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue];
}

// Usage
function App() {
  const [user, setUser] = useLocalStorage<User | null>('user', null);
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
  
  // Type-safety in action!
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <div className={`app ${theme}`}>
      {/* Component implementation */}
    </div>
  );
}

Creating a Type-Safe Context System

Context is powerful but can be a source of runtime errors without proper typing. Let's build a complete type-safe context system:

1. Define Context Types and Create a Helper Function

// Helper function to create a type-safe context
function createCtx<ContextType>() {
  // Create the context with a default value that will never be used
  // but satisfies TypeScript's need for a default
  const Context = React.createContext<ContextType | undefined>(undefined);
  
  // Custom hook that ensures the context is used within a provider
  function useCtx() {
    const ctx = React.useContext(Context);
    if (ctx === undefined) {
      throw new Error('useCtx must be used within a Provider');
    }
    return ctx;
  }
  
  return [Context.Provider, useCtx] as const;
}

2. Create a Complete Authentication Context

// Types
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface Credentials {
  email: string;
  password: string;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
}

interface AuthContextType extends AuthState {
  login: (credentials: Credentials) => Promise<void>;
  logout: () => Promise<void>;
  signup: (userData: Omit<User, 'id'> & { password: string }) => Promise<void>;
  clearErrors: () => void;
}

// Create context and hook
const [AuthProvider, useAuth] = createCtx<AuthContextType>();

// Implement the provider component
function AuthProviderComponent({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<AuthState>({
    user: null,
    isAuthenticated: false,
    isLoading: true,
    error: null,
  });
  
  // Check for existing session when the provider mounts
  useEffect(() => {
    const checkAuthStatus = async () => {
      try {
        const response = await fetch('/api/auth/me');
        
        if (response.ok) {
          const user = await response.json();
          setState({
            user,
            isAuthenticated: true,
            isLoading: false,
            error: null,
          });
        } else {
          setState({
            user: null,
            isAuthenticated: false,
            isLoading: false,
            error: null,
          });
        }
      } catch (error) {
        setState({
          user: null,
          isAuthenticated: false,
          isLoading: false,
          error: 'Failed to check authentication status',
        });
      }
    };
    
    checkAuthStatus();
  }, []);
  
  // Login function
  const login = async (credentials: Credentials) => {
    try {
      setState(prev => ({ ...prev, isLoading: true, error: null }));
      
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });
      
      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Login failed');
      }
      
      const user = await response.json();
      
      setState({
        user,
        isAuthenticated: true,
        isLoading: false,
        error: null,
      });
    } catch (error) {
      setState(prev => ({
        ...prev,
        isLoading: false,
        error: error instanceof Error ? error.message : 'Login failed',
      }));
      throw error;
    }
  };
  
  // Logout function
  const logout = async () => {
    try {
      setState(prev => ({ ...prev, isLoading: true }));
      
      await fetch('/api/auth/logout', { method: 'POST' });
      
      setState({
        user: null,
        isAuthenticated: false,
        isLoading: false,
        error: null,
      });
    } catch (error) {
      setState(prev => ({
        ...prev,
        isLoading: false,
        error: 'Logout failed',
      }));
    }
  };
  
  // Signup function
  const signup = async (userData: Omit<User, 'id'> & { password: string }) => {
    try {
      setState(prev => ({ ...prev, isLoading: true, error: null }));
      
      const response = await fetch('/api/auth/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData),
      });
      
      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Signup failed');
      }
      
      const user = await response.json();
      
      setState({
        user,
        isAuthenticated: true,
        isLoading: false,
        error: null,
      });
    } catch (error) {
      setState(prev => ({
        ...prev,
        isLoading: false,
        error: error instanceof Error ? error.message : 'Signup failed',
      }));
      throw error;
    }
  };
  
  // Clear any error messages
  const clearErrors = () => {
    setState(prev => ({ ...prev, error: null }));
  };
  
  // Combine state and functions to create the context value
  const value: AuthContextType = {
    ...state,
    login,
    logout,
    signup,
    clearErrors,
  };
  
  return <AuthProvider value={value}>{children}</AuthProvider>;
}

// Export both the provider component and the hook
export { AuthProviderComponent as AuthProvider, useAuth };

3. Using the Auth Context in Components

function LoginPage() {
  const { login, isLoading, error, clearErrors } = useAuth();
  const [credentials, setCredentials] = useState<Credentials>({
    email: '',
    password: '',
  });
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await login(credentials);
      // Redirect on success
    } catch (error) {
      // Error is already handled in the context
    }
  };
  
  // Rest of the component...
}

function ProfilePage() {
  const { user, isAuthenticated, logout } = useAuth();
  
  // Redirect if not authenticated
  useEffect(() => {
    if (!isAuthenticated) {
      // Redirect to login
    }
  }, [isAuthenticated]);
  
  if (!user) return null;
  
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Role: {user.role}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Discriminated Unions for State Management

Discriminated unions are a powerful TypeScript feature that makes complex state management more type-safe.

1. Basic Implementation

type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

2. Implementing a Reducer with Discriminated Unions

// Define action types using discriminated unions
type RequestAction<T> =
  | { type: 'REQUEST_START' }
  | { type: 'REQUEST_SUCCESS'; payload: T }
  | { type: 'REQUEST_ERROR'; error: Error }
  | { type: 'REQUEST_RESET' };

// Reducer function with type-safety
function requestReducer<T>(
  state: RequestState<T>,
  action: RequestAction<T>
): RequestState<T> {
  switch (action.type) {
    case 'REQUEST_START':
      return { status: 'loading' };
    case 'REQUEST_SUCCESS':
      return { status: 'success', data: action.payload };
    case 'REQUEST_ERROR':
      return { status: 'error', error: action.error };
    case 'REQUEST_RESET':
      return { status: 'idle' };
    default:
      return state;
  }
}

// Hook that uses the reducer
function useRequest<T>(fetchFn: () => Promise<T>) {
  const [state, dispatch] = useReducer<React.Reducer<RequestState<T>, RequestAction<T>>>(
    requestReducer,
    { status: 'idle' }
  );
  
  const execute = useCallback(async () => {
    dispatch({ type: 'REQUEST_START' });
    
    try {
      const data = await fetchFn();
      dispatch({ type: 'REQUEST_SUCCESS', payload: data });
    } catch (error) {
      dispatch({
        type: 'REQUEST_ERROR',
        error: error instanceof Error ? error : new Error(String(error)),
      });
    }
  }, [fetchFn]);
  
  const reset = useCallback(() => {
    dispatch({ type: 'REQUEST_RESET' });
  }, []);
  
  return { state, execute, reset };
}

3. Using It in a Component

function UserData({ userId }: { userId: string }) {
  const fetchUserData = useCallback(async () => {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error('Failed to fetch user');
    return response.json();
  }, [userId]);
  
  const { state, execute, reset } = useRequest<User>(fetchUserData);
  
  useEffect(() => {
    execute();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userId]);
  
  // Type-safe rendering based on state
  switch (state.status) {
    case 'idle':
      return <p>Press the button to load user data.</p>;
      
    case 'loading':
      return <Spinner size="lg" />;
      
    case 'error':
      return (
        <div className="error">
          <p>Error: {state.error.message}</p>
          <button onClick={execute}>Retry</button>
        </div>
      );
      
    case 'success':
      return (
        <div className="user-profile">
          <h2>{state.data.name}</h2>
          <p>{state.data.email}</p>
          <span className="role">{state.data.role}</span>
          <button onClick={reset}>Reset</button>
        </div>
      );
  }
}

Advanced Utility Types and Type Guards

Let's explore some useful utility types and type guards for React applications.

1. Deep Partial Type for Form Data

// Utility type for deeply partial objects
type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

// Example usage with a complex form
interface Address {
  street: string;
  city: string;
  state: string;
  zipCode: string;
  country: string;
}

interface UserProfileForm {
  name: string;
  email: string;
  phone?: string;
  address: Address;
  preferences: {
    newsletter: boolean;
    notifications: {
      email: boolean;
      push: boolean;
      sms: boolean;
    };
  };
}

// Now we can update the form partially
function updateUserForm(
  formData: UserProfileForm,
  updates: DeepPartial<UserProfileForm>
): UserProfileForm {
  // Use a deep merge utility in practice
  return {...formData, ...updates} as UserProfileForm;
}

// Usage
const currentForm: UserProfileForm = {/*...*/};
const updatedForm = updateUserForm(currentForm, {
  address: {
    city: 'New York',
  },
  preferences: {
    notifications: {
      push: true,
    },
  },
});

2. Type Guards for Props Validation

// Helper function to check if props have certain properties
function hasOwnProperty<X extends {}, Y extends PropertyKey>(
  obj: X,
  prop: Y
): obj is X & Record<Y, unknown> {
  return Object.prototype.hasOwnProperty.call(obj, prop);
}

// Type guard to check if an object is of a specific interface
function isUserType(obj: any): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    hasOwnProperty(obj, 'id') &&
    hasOwnProperty(obj, 'name') &&
    hasOwnProperty(obj, 'email') &&
    hasOwnProperty(obj, 'role') &&
    typeof obj.id === 'string' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string' &&
    (obj.role === 'admin' || obj.role === 'user')
  );
}

// Usage in a component
function UserDisplay({ data }: { data: unknown }) {
  if (isUserType(data)) {
    // TypeScript now knows data is User
    return (
      <div>
        <h2>{data.name}</h2>
        <p>{data.email}</p>
      </div>
    );
  }
  
  return <p>Invalid user data</p>;
}

3. React Component Type Helpers

// Get prop types of a component
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

// Make certain props optional
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Make certain props required
type MakeRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Example usage
const Button = ({ variant = 'primary', size = 'md', children }: {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
}) => {
  // Button implementation
  return null;
};

// Make size required and variant optional
type CustomButtonProps = MakeRequired<ComponentProps<typeof Button>, 'size'>;

const CustomButton = (props: CustomButtonProps) => {
  return <Button {...props} />;
};

// This would error because size is now required
// <CustomButton>Click me</CustomButton>

// This works
<CustomButton size="lg">Click me</CustomButton>

Troubleshooting Common TypeScript Issues

1. Type Assertions vs. Type Guards

// Problem: Unsafe type assertion
function displayUser(user: any) {
  // Dangerous! Assumes the shape without checking
  return <div>{(user as User).name}</div>;
}

// Solution: Type guard
function displayUser(user: unknown) {
  if (isUserType(user)) {
    // Safe, TypeScript knows user is User here
    return <div>{user.name}</div>;
  }
  return <div>Invalid user</div>;
}

2. Fixing the "Object is possibly undefined" Error

// Problem
function UserProfile({ user }: { user?: User }) {
  // Error: Object is possibly undefined
  return <div>{user.name}</div>;
}

// Solutions:

// Option 1: Optional chaining
function UserProfile({ user }: { user?: User }) {
  return <div>{user?.name}</div>;
}

// Option 2: Default value
function UserProfile({ user = { id: '', name: 'Guest', email: '', role: 'user' } }: { user?: User }) {
  return <div>{user.name}</div>;
}

// Option 3: Early return
function UserProfile({ user }: { user?: User }) {
  if (!user) return <div>No user data</div>;
  return <div>{user.name}</div>;
}

// Option 4: Non-null assertion (use sparingly)
function UserProfile({ user }: { user?: User }) {
  return <div>{user!.name}</div>;
}

3. Working with Event Handlers

// Problem: Incorrect event type
function SearchInput() {
  const handleChange = (e) => { // Error: Parameter 'e' implicitly has 'any' type
    console.log(e.target.value);
  };
  
  return <input onChange={handleChange} />;
}

// Solution: Use the correct event type
function SearchInput() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
  
  return <input onChange={handleChange} />;
}

// For forms
function LoginForm() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Form handling logic
  };
  
  return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}

Best Practices: A Comprehensive Guide

1. Using React.FC vs. Function Components

// Avoid React.FC in most cases
type ButtonProps = {
  variant?: 'primary' | 'secondary';
  children: React.ReactNode;
};

// Prefer this:
function Button({ variant = 'primary', children }: ButtonProps) {
  return <button className={`btn-${variant}`}>{children}</button>;
}

// Over this:
const Button: React.FC<ButtonProps> = ({ variant = 'primary', children }) => {
  return <button className={`btn-${variant}`}>{children}</button>;
};

// The difference: React.FC implicitly includes children and
// adds certain static properties that aren't always needed

2. Organizing Types in Larger Applications

// src/types/index.ts - Central type definition file
export * from './user';
export * from './auth';
export * from './products';

// src/types/user.ts - Domain-specific types
export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  // ...
}

export type UserRole = 'admin' | 'user' | 'guest';

export interface UserProfile extends User {
  bio: string;
  avatar: string;
  // ...
}

// Usage in components
import { User, UserRole } from '../types';

3. Using Enums vs. Union Types

// Avoid enums in most cases
enum UserRole {
  Admin = 'ADMIN',
  User = 'USER',
  Guest = 'GUEST',
}

// Prefer union types
type UserRole = 'admin' | 'user' | 'guest';

// Why? Union types are more lightweight, easier to understand,
// and don't generate extra JavaScript code

4. Performance Optimization with TypeScript

// Use const assertions for better inference and optimization
const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  PROFILE: '/profile',
} as const;

// Now TypeScript knows the exact string literals
type Route = typeof ROUTES[keyof typeof ROUTES];
// Type is: '/' | '/about' | '/profile'

// Use `readonly` for arrays that shouldn't change
function Tabs({ items }: { items: readonly string[] }) {
  // items.push('new tab'); // Error: Property 'push' does not exist on type 'readonly string[]'
}

Conclusion

Advanced TypeScript patterns can dramatically improve the quality, maintainability, and developer experience of your React applications. In this article, we've covered:

  1. Generic Components for creating highly reusable type-safe components
  2. Custom Hooks with Type Safety to encapsulate and share logic
  3. Type-Safe Context for global state management
  4. Discriminated Unions for complex state handling
  5. Utility Types for advanced type transformations
  6. Type Guards for runtime type checking
  7. Troubleshooting Common Issues you might encounter

By applying these patterns, you'll reduce runtime errors, improve code readability, and make your React applications more robust and maintainable.

Remember, TypeScript is a tool to help you write better code, not a goal in itself. Start with simpler patterns and gradually adopt more advanced techniques as you become comfortable with them.

Happy coding!