📘 TypeScript with ReactJS All in One ⚛️

truongpx396

Truong Phung

Posted on November 12, 2024

📘 TypeScript with ReactJS All in One ⚛️

Following is an example that helps us quickly review common TypeScript features like Enums, Interfaces, Generics, Type Annotations, Intersection Type, Union Type, Type Guard....

1. TypeScript Page (TypeScriptPage.tsx)

import React, { useState, useEffect, useReducer } from 'react';
import styles from './TypeScriptPage.module.css';

// Enum for user roles
enum UserRole {
  Admin = 'Admin',
  User = 'User',
  Guest = 'Guest',
}

// Interface for base user data
interface User {
  id: number;
  name: string;
  email?: string; // Optional property example
  role: UserRole;
}

// AdminUser type using intersection to add permissions to User
type AdminUser = User & { permissions: string[] };
// Above code equivalent to following
// interface AdminUser extends User {
//     permissions: string[]; // Specific to AdminUser
// }

// Union type for users with different possible structures
type UserType = User | AdminUser;

// Type Guard to check if a user is an AdminUser
const isAdminUser = (user: UserType): user is AdminUser => {
  return (user as AdminUser).permissions !== undefined;
};

// Type alias for a list of users
type UserList = User[];

// Utility types examples
type UserWithoutEmail = Omit<User, 'email'>; // Omits the email property
type UserContactInfo = Pick<User, 'id' | 'name'>; // Picks only the id and name properties
type PartialUser = Partial<User>; // Makes all properties of User optional

// Generic function to fetch data, using generics for flexible return types with overloads and default parameter
async function fetchData<T>(url: string, options: RequestInit = {}): Promise<T> {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
}

// Action type for useReducer with different payloads
type UserAction =
  | { type: 'add'; payload: User }
  | { type: 'remove'; id: number }
  | { type: 'update'; payload: { id: number; updates: PartialUser } };

// Reducer function for managing a list of users, demonstrating typed reducer usage
const userReducer = (state: User[], action: UserAction): User[] => {
  switch (action.type) {
    case 'add':
      return [...state, action.payload];
    case 'remove':
      return state.filter(user => user.id !== action.id);
    case 'update':
      return state.map(user =>
        user.id === action.payload.id ? { ...user, ...action.payload.updates } : user
      );
    default:
      return state;
  }
};

const TypeScriptPage: React.FC = () => {
  const [users, dispatch] = useReducer(userReducer, []); // Using useReducer for state management
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);
  const [newUser, setNewUser] = useState<{ name: string; email: string; role: UserRole }>({
    name: '',
    email: '',
    role: UserRole.Guest,
  });
  const [validationError, setValidationError] = useState<string | null>(null);

  useEffect(() => {
    fetchData<UserList>('https://jsonplaceholder.typicode.com/users')
      .then(data => {
        // Simulate user roles and create AdminUser type users
        const usersWithRoles: UserType[] = data.map((user, index) =>
          index % 2 === 0
            ? { ...user, role: UserRole.User }
            : { ...user, role: UserRole.Admin, permissions: ['read', 'write'] }
        );
        usersWithRoles.forEach(user => dispatch({ type: 'add', payload: user })); // Adding users to the reducer
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

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

  if (error) {
    return <div className={styles.error}>Error: {error}</div>;
  }

  // Example of Partial: updating only a subset of user properties
  const updateUser = (id: number, updates: PartialUser) => {
    dispatch({
      type: 'update',
      payload: { id, updates },
    });
  };

  const handleAddUser = () => {
    if (!newUser.name || !newUser.email) {
      setValidationError('Name and email are required.');
      return;
    }

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(newUser.email)) {
      setValidationError('Please enter a valid email.');
      return;
    }

    const newUserId = users.length ? Math.max(...users.map(user => user.id)) + 1 : 1;
    const userToAdd: User = {
      id: newUserId,
      name: newUser.name,
      email: newUser.email,
      role: newUser.role,
    };

    dispatch({ type: 'add', payload: userToAdd });
    setNewUser({ name: '', email: '', role: UserRole.Guest });
    setValidationError(null);
  };

  return (
    <div className={styles.container}>
      <h1 className={styles.title}>TypeScript Advanced Features Page</h1>
      {/* Demonstrate Omit utility */}
      <div>
        <h2 className={styles.subtitle}>Omit User Example</h2>
        {/* Example: Email omitted from user */}
        <code>{JSON.stringify({ id: 1, name: "User without email", role: UserRole.User } as UserWithoutEmail)}</code>
      </div>
      <div className={styles.addUserForm}>
        <h2 className={styles.subtitle}>Add New User</h2>
        <input
          type="text"
          placeholder="Name"
          value={newUser.name}
          onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
          className={styles.input}
        />
        <input
          type="email"
          placeholder="Email"
          value={newUser.email}
          onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
          className={styles.input}
        />
        <select
          value={newUser.role}
          onChange={(e) => setNewUser({ ...newUser, role: e.target.value as UserRole })}
          className={styles.select}
        >
          <option value={UserRole.Guest}>Guest</option>
          <option value={UserRole.User}>User</option>
          <option value={UserRole.Admin}>Admin</option>
        </select>
        <button onClick={handleAddUser} className={styles.button}>
          Add User
        </button>
        {validationError && <div className={styles.error}>{validationError}</div>}
      </div>
      <ul className={styles.userList}>
        {users.map(user => (
          <li key={user.id} className={styles.userItem}>
            {user.name} ({user.email ?? 'N/A'}) - Role: {user.role}
            {isAdminUser(user) && (
              <div className={styles.permissions}>Permissions: {user.permissions.join(', ')}</div>
            )}
            {/* Example using Pick to show only selected properties */}
            <div>
              <strong>User Preview:</strong>{' '}
              {JSON.stringify({ id: user.id, name: user.name } as UserContactInfo)}
            </div>
            {/* Update user role using Partial example */}
            <button onClick={() => updateUser(user.id, { role: UserRole.Guest })}  className={`${styles.button} ${styles.changeRoleButton}`}>
              Change Role to Guest
            </button>
            <button onClick={() => dispatch({ type: 'remove', id: user.id })} className={`${styles.button} ${styles.removeUserButton}`}>
              Remove User
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TypeScriptPage;
Enter fullscreen mode Exit fullscreen mode

2. Style (TypeScriptPage.module.css)

Let's create a TypeScriptPage.module.css file for styling and then apply these styles within TypeScriptPage. Here’s how you can do it:

/* Main container for the page */
.container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

/* Title styling */
.title {
  color: #2c3e50;
  font-size: 2rem;
  margin-bottom: 20px;
  text-align: center;
}

/* Subtitle for section headings */
.subtitle {
  color: #34495e;
  font-size: 1.5rem;
  margin-top: 20px;
  margin-bottom: 10px;
}

/* User list container */
.userList {
  list-style-type: none;
  padding: 0;
  margin-top: 20px;
}

/* Individual user item */
.userItem {
  padding: 15px;
  margin: 10px 0;
  border: 1px solid #bdc3c7;
  border-radius: 8px;
  background-color: #ecf0f1;
}

/* Style for permissions for admin users */
.permissions {
  color: #16a085;
  font-weight: bold;
  margin-top: 5px;
}

/* Button styling */
.button {
  margin-right: 10px;
  padding: 8px 12px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
}

/* Button for changing role */
.changeRoleButton {
  background-color: #3498db;
  color: white;
}

.changeRoleButton:hover {
  background-color: #2980b9;
}

/* Button for removing user */
.removeUserButton {
  background-color: #e74c3c;
  color: white;
}

.removeUserButton:hover {
  background-color: #c0392b;
}

/* Loading and error messages */
.loading, .error {
  text-align: center;
  font-size: 1.2rem;
}

.loading {
  color: #3498db;
}

.error {
  color: #e74c3c;
}

/* Add User Form styling */
.addUserForm {
  margin-top: 20px;
  padding: 20px;
  border: 1px solid #bdc3c7;
  border-radius: 8px;
  background-color: #f8f9fa;
}

.addUserForm input,
.addUserForm select {
  width: 100%;
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #bdc3c7;
  border-radius: 4px;
}

.addUserForm button {
  background-color: #2ecc71;
  color: white;
  padding: 10px 15px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.addUserForm button:hover {
  background-color: #27ae60;
}
Enter fullscreen mode Exit fullscreen mode

3. Summary

Here’s a breakdown of what’s included

  1. Enums: Demonstrates using an enum to define user roles.
  2. Interfaces: Shows a clear example of defining an interface (User) for structured typing.
  3. Generics: The fetchData<T> function uses generics to make it reusable for various types.
  4. Type Annotations: Type annotations for state (users, loading, error) , component (React.FC)...
  5. Error Handling with Promises: Good use of error handling when fetching data.
  6. React with TypeScript: TypeScript integration in a React component.
  7. Optional Properties: The email field in User is optional, shown with email?: string.
  8. Intersection Type (AdminUser): Combines User with additional properties (permissions) for admin-specific features.
  9. Union Type and Type Guard (isAdminUser): UserType can be either User or AdminUser, with a type guard to check if a user has admin permissions.
  10. Utility Types:
    • Omit: UserWithoutEmail excludes email from User.
    • Pick: UserContactInfo selects only id and name from User.
    • Partial: PartialUser makes all User properties optional, used in the updateUser function to apply partial updates.
  11. useReducer and Typed Actions: userReducer manages the users state, allowing complex actions (add, remove,update) with a strongly-typed UserAction union.
  12. Type Assertions: In updateUser, payload is cast to User to match the userReducer requirements after partial updates.
  13. Conditional Rendering with Optional Chaining: Optional chaining is useful for handling deeply nested objects without worrying about null or undefined.
  14. Add User Form: a form is added to input the new user's details. The handleAddUser function validates the input and dispatches an action to add the new user to the state. The validationError state is used to display any validation errors.

This example demonstrates common TypeScript functionality and give you a broad showcase of the language’s features.

If you found this helpful, let me know by leaving a 👍 or a comment!, or if you think this post could help someone, feel free to share it! Thank you very much! 😃

💖 💪 🙅 🚩
truongpx396
Truong Phung

Posted on November 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related