COMP_SCI 392 - Fall 2024 - Northwestern University
- OpenHands - Red Team
This project uses a component-based structure with a focus on clear separation of concerns. Key files and folders:
.
├── LICENSE
├── README.md # Project documentation and usage guide
├── vite.config.ts # Vite configuration file
├── tsconfig.json # Typescript config file
├── firebase.json # Firebase configuration for hosting
├── package.json # Dependencies
└── src # Source code
├── components # Shared components and features
│ ├── common # Common components used across the app
│ ├── Home # Home page components
│ ├── Me # Me page components
│ └── ... # Other pages' components
├── stores # Zustand related global state management
├── hooks # Custom hooks for specialized logic
├── pages # Application pages
├── utils # Utility functions and Firebase configurations
├── styles # Styles.
└── types # Global types define here (such as schemas).
The main components and utilities are organized under src/components
and src/utils
.
-
Why: Avoid developing directly on
main
. Keepingmain
in sync withorigin/main
makes it easier to update and manage changes. -
How: Create and switch to a new branch for your feature, and remember to push it to
origin
:git switch -c feat/new-feature-name git push -u origin feat/new-feature-name
-
Switch Back to
main
: Ensure you're onmain
before updating:git switch main
-
Stash Your Work(if needed): If you have uncommitted changes, stash them to avoid conflicts while pulling:
git stash
-
Pull Latest Changes: Bring in the latest updates from
origin/main
:git pull origin main
-
Switch Back to Your Feature Branch:
git switch feat/new-feature-name
-
Rebase: Apply your feature branch changes on top of the latest
main
:git rebase main
-
Apply Stash(if you stashed changes): Reapply your saved changes once main is updated:
git stash pop
-
Resolve Conflicts (if any): If conflicts occur, Git will prompt you to resolve them. After resolving, use:
git add <conflicted-files> git rebase --continue
-
Push Changes:
-
If you have NOT previously pushed code to the remote:
git push
-
If you HAVE previously pushed code (with conflicting changes), you may need to force-push to align with the rebased history. (Do NOT use this on
main
)git push --force-with-lease
-
By following these steps, you ensure that main
remains in sync with origin/main
, while your feature branch incorporates the latest updates without directly modifying main
. This keeps your work organized and minimizes conflict risks.
In this project, TypeScript types and interfaces are used to ensure clarity and catch errors early in the development process. Defining types helps TypeScript provide hints and checks, reducing potential bugs by catching type mismatches before runtime.
For types shared across multiple components or features, define them globally in types.d.ts
. This allows all components to access these types without needing to import them explicitly, keeping the code DRY.
Example:
// types.d.ts
interface UserContext = {
user: User | null;
loading: boolean;
};
type User {
uid: string;
username: string;
email: string;
avatar: string;
}
With these types defined in types.d.ts
, all components can directly use UserContext
and User
without importing them, ensuring consistency across the project.
For types only relevant to a specific component or file, define them locally within that file to avoid cluttering global types.
Example:
// src/components/Profile.tsx
type ProfileStats = {
posts: number;
followers: number;
following: number;
};
// Since the `User` type is defined in the global scope, no need to import here.
const Profile = ({ user }: { user: User }) => {
const stats: ProfileStats = { posts: 50, followers: 100, following: 20 };
return (
<div>
<h1>{user.username}</h1>
<p>Posts: {stats.posts}</p>
<p>Followers: {stats.followers}</p>
<p>Following: {stats.following}</p>
</div>
);
};
Here, ProfileStats
is defined locally within Profile.tsx
because it’s specific to this component.
Interfaces are primarily used for objects that might be extended or combined with other types in the future. They are especially useful for defining the structure of props
passed into components. This helps TypeScript enforce the correct structure and prevent errors when using the component.
Example:
// types.d.ts
interface UserProfile {
id: string;
username: string;
email: string;
avatarUrl?: string;
bio?: string;
}
interface ProfileProps {
profile: UserProfile;
onFollow: (userId: string) => void;
}
In this example, UserProfile
defines the structure for a user's profile, and ProfileProps
specifies the props
expected by the Profile
component, including the profile data and a follow function.
Usage in a Component:
// src/components/Profile.tsx
const Profile = ({ profile, onFollow }: ProfileProps) => {
return (
<div>
<h1>{profile.username}</h1>
<p>{profile.bio}</p>
{profile.avatarUrl && <img src={profile.avatarUrl} alt={`${profile.username}'s avatar`} />}
<button onClick={() => onFollow(profile.id)}>Follow</button>
</div>
);
};
Here, Profile
receives profile
and onFollow
as props
. TypeScript ensures that profile
matches the UserProfile
structure and onFollow
is a function that takes a userId
string as an argument. This makes the component’s expectations clear and prevents type-related bugs during development.
Using import type
for importing types makes it clear that the import is used only for TypeScript type checking and won’t be included in the compiled JavaScript code. This can improve bundling and reduce unnecessary imports.
Example:
// src/hooks/useUser.ts
import type { UserContextType } from '@/types';
const useUser = () => {
// usage of UserContextType in the hook
};
Here, import type { UserContextType }
indicates that UserContextType
is used only for type checking, not at runtime. This avoids additional runtime imports and helps with bundling efficiency.
Zustand
is a lightweight state management library for React that uses a centralized store to manage global state. Unlike Context
, which triggers a re-render of all components that consume it whenever the state changes, zustand
only updates the components that subscribe to the specific state slice. This makes it more efficient and scalable for managing complex or frequently changing global states, such as user authentication data.
-
Enable Zustand Store: Zustand stores are imported and initialized once for the entire application. For example, the
user
store:import { useUserStore } from '@/stores'; const App = () => { const initializeAuthListener = useUserStore( (state) => state.initializeAuthListener ); useEffect(() => { const unsubscribe = initializeAuthListener(); return () => unsubscribe(); // Cleanup listener on unmount }, [initializeAuthListener]); return <>{/* App Components */}</>; };
-
Access State in Components: Use zustand hooks to fetch only the state you need. This ensures efficient re-rendering.
import { useUserStore } from '@/stores'; const MyComponent = () => { const user = useUserStore((state) => state.user); const loading = useUserStore((state) => state.loading); return user ? ( <div> <h1>Welcome, {user.displayName}</h1> <button onClick={() => useUserStore.getState().logout()}> Sign Out </button> </div> ) : loading ? ( <p>Loading...</p> ) : ( <button onClick={() => useUserStore .getState() .login('donor', () => console.log('Logged in')) } > Sign In </button> ); };
To update the user's profile or other states, zustand provides centralized actions that automatically update Firebase and sync the changes with the store:
import useUserStore from '@/stores/useUserStore';
const MyComponent = () => {
const updateProfile = useUserStore((state) => state.updateProfile);
const handleUpdate = () => {
updateProfile({ displayName: 'New Name' }); // Updates profile in Firebase and zustand
};
return <button onClick={handleUpdate}>Update Profile</button>;
};
- Better Performance: Only components subscribing to specific state slices re-render, unlike
Context
, where all consumers re-render. - Cleaner Code: Real-time Firebase listeners (
onSnapshot
,onAuthStateChanged
) are centralized in zustand, simplifying the components. - Easy Debugging: Zustand provides utilities like
zustand/middleware
to inspect and persist state. - Efficient State Sharing: State is directly accessible anywhere in the app without additional setup (e.g., no need for custom hooks like
useUser
).