How to ship fast using code passes.
Don't bother with go-to best-practice code from the very start; it slows down development and stops you delivering quickly. Instead of aiming for perfection from day one, split your coding sessions into three distinct passes. When you first write code, it should be with an aim to get it done quickly; a working prototype. There's plenty of time to fix your mistakes and make your code 'acceptable' after this, but right now, relax.
Pass One: Get it done
When? Immediately.
Objective: It works. That's it.
The aim here is to get started. Pen on paper mode, let's go. Create your new component file (kebab-case.tsx
) and get to work scaffolding everything you need. Move fast, stay native, and get to a point that things work.
Keep types to a minimum, don't be afraid of [key] : any
and definitely work from one single file. useState
, useEffect
etc are more than enough at this stage - You can always refactor that messy logic to custom hook later on if needed. The objective is to get your code working so you can move onto Pass two.
// Pass One: user-profile.tsx
import { useState, useEffect } from 'react';
const UserProfile = () => {
const [user, setUser] = useState<any>(null);
useEffect(() => {
fetch(`https://api.example.com/user/1`)
.then(response => response.json())
.then(data => setUser(data));
}, []);
if (!user) {
return <div>Loading...</div>;
}
return (
<div className="p-4">
<h2 className="text-2xl font-bold mb-2">{user.name}</h2>
<p className="text-gray-700">{user.email}</p>
</div>
);
};
export default UserProfile;
Pass Two: Make it right
Objective: It works as expected and is understandable.
Now that your code is working, start adding more specific types to replace any [key]: any
you might have used. Break down large functions into smaller, named functions. This will make your code easier to understand and debug. Start introducing some testing at this stage to make sure your code not only works, but it works as expected.
// Pass Two: user-profile.tsx
interface User {
id: number;
name: string;
email: string;
// Other fields...
}
export const UserProfile = () => {
const [user, setUser] = useState<User | null>(null);
const fetchUser = async () => {
const res = await fetch(`https://.../users`);
const user = await res.json() as User;
setUser(user);
};
useEffect(() => {
fetchUser();
}, []);
return (
// JSX
);
};
Pass Three: Make it good
Objective: It's optimized and maintainable.
This is the stage where you start thinking about optimizing your code. Can some parts of your code be memoized to improve performance? Can some state and functions be moved into custom hooks for better organization and reusability? You should also consider splitting your files if they've grown too large at this stage.
// Pass Three: useUser.ts
export const useUser = () => {
const [user, setUser] = useState<User | null>(null);
const fetchUser = async () => {
const res = await fetch(`https://.../users`);
const user = await res.json() as User;
setUser(user);
};
useEffect(() => {
fetchUser();
}, []);
return user;
};
// Pass Three: user-profile.tsx
import { useUser } from './hooks/useUser';
export const UserProfile = () => {
const user = useUser();
return (
// JSX
);
};
Pass Four: Make it resilient
Objective: It's robust and can handle failure gracefully. This pass involves thinking about error handling, edge cases, and the overall user experience when things don't go as expected. In the context of our example, this might involve handling scenarios where the user data couldn't be fetched, or perhaps the data comes back in an unexpected format.
Here's what this might look like in code:
// Pass Four: useUser.ts
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
// Other fields...
}
export const useUser = () => {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
const fetchUser = async () => {
try {
const res = await fetch(`https://.../users`);
if (!res.ok) {
throw new Error('Failed to fetch user');
}
const user = await res.json() as User;
setUser(user);
} catch (err) {
setError(err.message);
}
};
useEffect(() => {
fetchUser();
}, []);
return { user, error };
};
// Pass Four: user-profile.tsx
import { useUser } from './hooks/useUser';
export const UserProfile = () => {
const { user, error } = useUser();
if (error) {
return <div>Error: {error}</div>;
}
if (!user) {
return <div>Loading...</div>;
}
return (
// JSX
);
};
In this example, we have added error handling to the fetchUser
function in our custom hook, and the hook now returns an error
state alongside the user
state. The UserProfile
component can then display an error message to the user when something goes wrong. This makes our application more resilient and provides a better user experience.
Remember, you don't necessarily have to follow these passes in order or only once. It's more of an iterative process where you continually refine and improve your code over time. You might go through several cycles of "making it work", then "making it right", then "making it good", then "making it resilient" as you develop an application.