Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand Solid.js reactive fundamentals ๐ฏ
- Apply Solid.js in real projects ๐๏ธ
- Debug common Solid.js issues ๐
- Write type-safe reactive code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Solid.js with TypeScript! ๐ In this guide, weโll explore the blazing-fast reactive framework thatโs changing how we think about frontend development.
Youโll discover how Solid.js can transform your TypeScript development experience with its fine-grained reactivity and excellent performance. Whether youโre building interactive UIs ๐, real-time dashboards ๐, or complex web applications ๐, understanding Solid.js is essential for modern reactive programming.
By the end of this tutorial, youโll feel confident building reactive applications with Solid.js and TypeScript! Letโs dive in! ๐โโ๏ธ
๐ Understanding Solid.js
๐ค What is Solid.js?
Solid.js is like having a Formula 1 racing car ๐๏ธ in the world of frontend frameworks. Think of it as a super-efficient reactive system that updates only what actually changes, just like how your brain only processes new information instead of re-analyzing everything you already know.
In TypeScript terms, Solid.js provides fine-grained reactivity with compile-time optimizations โก. This means you can:
- โจ Get React-like DX with better performance
- ๐ Build fast, responsive UIs
- ๐ก๏ธ Maintain full TypeScript type safety
- ๐ฏ Use familiar JSX syntax
๐ก Why Use Solid.js?
Hereโs why developers love Solid.js:
- Performance ๐: No virtual DOM overhead
- TypeScript First ๐ป: Excellent type safety and inference
- Fine-Grained Reactivity ๐: Updates only what changes
- Small Bundle Size ๐ง: Minimal runtime footprint
Real-world example: Imagine building a real-time trading dashboard ๐. With Solid.js, when a stock price updates, only that specific price component re-renders, not the entire dashboard!
๐ง Basic Syntax and Usage
๐ Setting Up Your First Solid App
Letโs start with installation and setup:
# ๐ Create a new Solid.js TypeScript project
npm create solid@latest my-solid-app
cd my-solid-app
# ๐ฆ Install dependencies
npm install
# ๐ฎ Start development server
npm run dev
๐จ Basic Component Structure
Hereโs how Solid.js components work with TypeScript:
// ๐ Hello, Solid.js with TypeScript!
import { Component, createSignal } from 'solid-js';
// ๐ฏ Define component props type
interface GreetingProps {
name: string;
emoji?: string; // ๐ญ Optional emoji prop
}
// ๐จ Creating a simple Solid component
const Greeting: Component<GreetingProps> = (props) => {
const [count, setCount] = createSignal(0); // ๐ Reactive state
return (
<div>
<h1>Hello {props.name}! {props.emoji || "๐"}</h1>
<p>Button clicked {count()} times</p>
<button onClick={() => setCount(count() + 1)}>
Click me! ๐ฏ
</button>
</div>
);
};
export default Greeting;
๐ก Explanation: Notice how we use createSignal()
for reactive state and access it with count()
- this is Solidโs fine-grained reactivity in action!
๐ฏ Reactive Primitives
Here are the core reactive patterns youโll use daily:
import { createSignal, createMemo, createEffect } from 'solid-js';
// ๐จ Signal: reactive state
const [temperature, setTemperature] = createSignal(20);
// โจ Memo: derived reactive value
const temperatureDisplay = createMemo(() => {
const temp = temperature();
return temp > 30 ? `๐ฅ Hot: ${temp}ยฐC` : `โ๏ธ Cool: ${temp}ยฐC`;
});
// ๐ Effect: side effects
createEffect(() => {
console.log(`๐ก๏ธ Temperature changed to: ${temperature()}ยฐC`);
});
// ๐ฎ Using in component
const WeatherApp: Component = () => {
return (
<div>
<h2>Weather Station ๐ค๏ธ</h2>
<p>{temperatureDisplay()}</p>
<button onClick={() => setTemperature(temperature() + 5)}>
Heat up! ๐ฅ
</button>
<button onClick={() => setTemperature(temperature() - 5)}>
Cool down! โ๏ธ
</button>
</div>
);
};
๐ก Practical Examples
๐ Example 1: Shopping Cart with Solid.js
Letโs build a reactive shopping cart:
// ๐๏ธ Define our product and cart types
interface Product {
id: string;
name: string;
price: number;
emoji: string;
}
interface CartItem extends Product {
quantity: number;
}
// ๐ Shopping cart store
import { createSignal, createMemo } from 'solid-js';
const [cartItems, setCartItems] = createSignal<CartItem[]>([]);
// ๐ฐ Calculate total with memo for performance
const cartTotal = createMemo(() => {
return cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0);
});
// ๐ Cart item count
const cartItemCount = createMemo(() => {
return cartItems().reduce((count, item) => count + item.quantity, 0);
});
// ๐ Shopping cart component
const ShoppingCart: Component = () => {
const products: Product[] = [
{ id: '1', name: 'TypeScript Book', price: 29.99, emoji: '๐' },
{ id: '2', name: 'Coffee', price: 4.99, emoji: 'โ' },
{ id: '3', name: 'Laptop', price: 999.99, emoji: '๐ป' }
];
// โ Add item to cart
const addToCart = (product: Product) => {
setCartItems(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
return [...prev, { ...product, quantity: 1 }];
}
});
};
// โ Remove item from cart
const removeFromCart = (productId: string) => {
setCartItems(prev => prev.filter(item => item.id !== productId));
};
return (
<div class="shopping-cart">
<h2>๐ Shopping Cart ({cartItemCount()} items)</h2>
{/* ๐ช Product list */}
<div class="products">
<h3>๐ฆ Available Products</h3>
{products.map(product => (
<div class="product-card">
<span>{product.emoji} {product.name}</span>
<span>${product.price}</span>
<button onClick={() => addToCart(product)}>
Add to Cart โ
</button>
</div>
))}
</div>
{/* ๐๏ธ Cart items */}
<div class="cart-items">
<h3>๐ Your Cart</h3>
{cartItems().map(item => (
<div class="cart-item">
<span>{item.emoji} {item.name}</span>
<span>Qty: {item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
<button onClick={() => removeFromCart(item.id)}>
Remove โ
</button>
</div>
))}
{cartItems().length === 0 && (
<p>Your cart is empty ๐ข</p>
)}
{cartItems().length > 0 && (
<div class="cart-total">
<strong>๐ฐ Total: ${cartTotal().toFixed(2)}</strong>
</div>
)}
</div>
</div>
);
};
๐ฏ Try it yourself: Add a quantity selector and implement a โclear cartโ feature!
๐ฎ Example 2: Real-Time Game Score Tracker
Letโs create an interactive game with reactive updates:
// ๐ Game types
interface Player {
id: string;
name: string;
score: number;
level: number;
avatar: string;
}
interface GameState {
players: Player[];
gameActive: boolean;
timeLeft: number;
}
// ๐ฎ Game tracker component
const GameTracker: Component = () => {
const [gameState, setGameState] = createSignal<GameState>({
players: [
{ id: '1', name: 'Alex', score: 0, level: 1, avatar: '๐ฆธโโ๏ธ' },
{ id: '2', name: 'Sarah', score: 0, level: 1, avatar: '๐ฆธโโ๏ธ' },
{ id: '3', name: 'Mike', score: 0, level: 1, avatar: '๐งโโ๏ธ' }
],
gameActive: false,
timeLeft: 60
});
// ๐ Leader (highest score)
const leader = createMemo(() => {
const players = gameState().players;
return players.reduce((prev, current) =>
current.score > prev.score ? current : prev
);
});
// ๐ Average score
const averageScore = createMemo(() => {
const players = gameState().players;
const total = players.reduce((sum, player) => sum + player.score, 0);
return Math.round(total / players.length);
});
// โก Add points to player
const addPoints = (playerId: string, points: number) => {
setGameState(prev => ({
...prev,
players: prev.players.map(player => {
if (player.id === playerId) {
const newScore = player.score + points;
const newLevel = Math.floor(newScore / 100) + 1;
return {
...player,
score: newScore,
level: newLevel
};
}
return player;
})
}));
};
// ๐ฏ Start/stop game
const toggleGame = () => {
setGameState(prev => ({
...prev,
gameActive: !prev.gameActive,
timeLeft: prev.gameActive ? 60 : prev.timeLeft
}));
};
// โฐ Game timer effect
createEffect(() => {
if (gameState().gameActive && gameState().timeLeft > 0) {
const timer = setTimeout(() => {
setGameState(prev => ({
...prev,
timeLeft: prev.timeLeft - 1
}));
}, 1000);
return () => clearTimeout(timer);
} else if (gameState().timeLeft === 0) {
setGameState(prev => ({ ...prev, gameActive: false }));
}
});
return (
<div class="game-tracker">
<h2>๐ฎ TypeScript Game Arena</h2>
{/* ๐ Game stats */}
<div class="game-stats">
<div class="stat">โฐ Time: {gameState().timeLeft}s</div>
<div class="stat">๐ Leader: {leader().avatar} {leader().name}</div>
<div class="stat">๐ Avg Score: {averageScore()}</div>
<button onClick={toggleGame}>
{gameState().gameActive ? 'โธ๏ธ Pause' : 'โถ๏ธ Start'} Game
</button>
</div>
{/* ๐ฅ Player list */}
<div class="players">
{gameState().players.map(player => (
<div class="player-card">
<div class="player-info">
<span class="avatar">{player.avatar}</span>
<span class="name">{player.name}</span>
<span class="level">Lv.{player.level}</span>
</div>
<div class="score">๐ {player.score}</div>
<div class="actions">
<button
onClick={() => addPoints(player.id, 10)}
disabled={!gameState().gameActive}
>
+10 โจ
</button>
<button
onClick={() => addPoints(player.id, 25)}
disabled={!gameState().gameActive}
>
+25 ๐
</button>
</div>
</div>
))}
</div>
</div>
);
};
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Stores and Context
When youโre ready to level up, try this advanced pattern for global state:
// ๐ช Creating a global store with TypeScript
import { createContext, useContext, createSignal, createMemo, ParentComponent } from 'solid-js';
interface User {
id: string;
name: string;
email: string;
avatar: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
interface AppStore {
user: User | null;
setUser: (user: User | null) => void;
isLoggedIn: () => boolean;
updatePreferences: (prefs: Partial<User['preferences']>) => void;
}
// ๐ฏ Create store context
const AppStoreContext = createContext<AppStore>();
// ๐๏ธ Store provider
export const AppStoreProvider: ParentComponent = (props) => {
const [user, setUser] = createSignal<User | null>(null);
// โจ Derived state
const isLoggedIn = createMemo(() => user() !== null);
// ๐ง Update user preferences
const updatePreferences = (prefs: Partial<User['preferences']>) => {
setUser(prev => {
if (!prev) return null;
return {
...prev,
preferences: { ...prev.preferences, ...prefs }
};
});
};
const store: AppStore = {
user,
setUser,
isLoggedIn,
updatePreferences
};
return (
<AppStoreContext.Provider value={store}>
{props.children}
</AppStoreContext.Provider>
);
};
// ๐ช Custom hook for using store
export const useAppStore = () => {
const context = useContext(AppStoreContext);
if (!context) {
throw new Error('useAppStore must be used within AppStoreProvider');
}
return context;
};
๐๏ธ Advanced Topic 2: Custom Reactive Primitives
For the brave developers, create your own reactive utilities:
// ๐ Custom reactive utilities
import { createSignal, createMemo, createEffect } from 'solid-js';
// ๐ก Custom hook for localStorage sync
export function createLocalStorage<T>(
key: string,
initialValue: T
): [() => T, (value: T) => void] {
const [storedValue, setStoredValue] = createSignal<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T) => {
setStoredValue(() => value);
localStorage.setItem(key, JSON.stringify(value));
};
return [storedValue, setValue];
}
// โฑ๏ธ Custom timer hook
export function createTimer(initialTime: number = 0) {
const [time, setTime] = createSignal(initialTime);
const [isRunning, setIsRunning] = createSignal(false);
createEffect(() => {
if (isRunning()) {
const interval = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}
});
return {
time,
isRunning,
start: () => setIsRunning(true),
stop: () => setIsRunning(false),
reset: () => {
setTime(initialTime);
setIsRunning(false);
}
};
}
// ๐ฏ Usage example
const TimerApp: Component = () => {
const timer = createTimer();
const [savedTime, setSavedTime] = createLocalStorage('timer-value', 0);
const formattedTime = createMemo(() => {
const minutes = Math.floor(timer.time() / 60);
const seconds = timer.time() % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
});
return (
<div>
<h2>โฑ๏ธ Custom Timer</h2>
<div class="timer-display">{formattedTime()}</div>
<button onClick={timer.start} disabled={timer.isRunning()}>
โถ๏ธ Start
</button>
<button onClick={timer.stop} disabled={!timer.isRunning()}>
โธ๏ธ Stop
</button>
<button onClick={timer.reset}>๐ Reset</button>
<button onClick={() => setSavedTime(timer.time())}>
๐พ Save Time
</button>
<p>๐พ Saved: {savedTime()}s</p>
</div>
);
};
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting to Call Signals
// โ Wrong way - treating signal like a variable!
const [count, setCount] = createSignal(0);
console.log(count); // ๐ฅ This logs the function, not the value!
// โ
Correct way - call the signal function!
const [count, setCount] = createSignal(0);
console.log(count()); // โ
This logs the actual value!
// ๐ฏ In JSX, both work but calling is more explicit
return (
<div>
<p>Count: {count()}</p> {/* โ
Explicit and clear */}
<p>Count: {count}</p> {/* โ
Also works in JSX */}
</div>
);
๐คฏ Pitfall 2: Creating Signals in Loops
// โ Dangerous - signals in loops!
const TodoList: Component = () => {
const [todos, setTodos] = createSignal(['Task 1', 'Task 2']);
return (
<div>
{todos().map(todo => {
// ๐ฅ DON'T create signals in render loops!
const [completed, setCompleted] = createSignal(false);
return <div>{todo}</div>;
})}
</div>
);
};
// โ
Safe - manage state properly!
interface Todo {
id: string;
text: string;
completed: boolean;
}
const TodoList: Component = () => {
const [todos, setTodos] = createSignal<Todo[]>([
{ id: '1', text: 'Task 1', completed: false },
{ id: '2', text: 'Task 2', completed: false }
]);
const toggleTodo = (id: string) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<div>
{todos().map(todo => (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</div>
))}
</div>
);
};
๐ ๏ธ Best Practices
- ๐ฏ Use TypeScript Everywhere: Define proper types for all props and state
- ๐ Leverage Memos: Use
createMemo
for expensive computations - ๐ก๏ธ Handle Loading States: Always account for async operations
- ๐จ Component Composition: Break down complex components
- โจ Optimize Reactivity: Only update what needs to change
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Real-Time Chat App
Create a type-safe chat application with Solid.js:
๐ Requirements:
- โ Message list with real-time updates
- ๐ท๏ธ User typing indicators
- ๐ค Multiple users support
- ๐ Message timestamps
- ๐จ Each message needs an emoji reaction system!
๐ Bonus Points:
- Add message search functionality
- Implement message editing
- Create user presence indicators
- Add emoji picker for reactions
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe chat system!
interface User {
id: string;
name: string;
avatar: string;
isTyping: boolean;
}
interface Message {
id: string;
userId: string;
content: string;
timestamp: Date;
reactions: Record<string, string[]>; // emoji -> userIds
}
interface ChatState {
messages: Message[];
users: User[];
currentUser: User;
}
const ChatApp: Component = () => {
const [chatState, setChatState] = createSignal<ChatState>({
messages: [],
users: [
{ id: '1', name: 'Alice', avatar: '๐ฉโ๐ป', isTyping: false },
{ id: '2', name: 'Bob', avatar: '๐จโ๐', isTyping: false },
{ id: '3', name: 'Charlie', avatar: '๐งโ๐จ', isTyping: false }
],
currentUser: { id: '1', name: 'Alice', avatar: '๐ฉโ๐ป', isTyping: false }
});
const [newMessage, setNewMessage] = createSignal('');
const [typingTimeout, setTypingTimeout] = createSignal<number | null>(null);
// ๐ฌ Send message
const sendMessage = () => {
const content = newMessage().trim();
if (!content) return;
const message: Message = {
id: Date.now().toString(),
userId: chatState().currentUser.id,
content,
timestamp: new Date(),
reactions: {}
};
setChatState(prev => ({
...prev,
messages: [...prev.messages, message]
}));
setNewMessage('');
stopTyping();
};
// โจ๏ธ Handle typing
const handleTyping = (content: string) => {
setNewMessage(content);
// ๐ฏ Update typing status
setChatState(prev => ({
...prev,
users: prev.users.map(user =>
user.id === prev.currentUser.id
? { ...user, isTyping: content.length > 0 }
: user
)
}));
// โฐ Clear typing after delay
if (typingTimeout()) {
clearTimeout(typingTimeout()!);
}
setTypingTimeout(setTimeout(() => {
stopTyping();
}, 2000) as unknown as number);
};
// ๐ Stop typing
const stopTyping = () => {
setChatState(prev => ({
...prev,
users: prev.users.map(user =>
user.id === prev.currentUser.id
? { ...user, isTyping: false }
: user
)
}));
};
// ๐ Add reaction
const addReaction = (messageId: string, emoji: string) => {
setChatState(prev => ({
...prev,
messages: prev.messages.map(message => {
if (message.id === messageId) {
const reactions = { ...message.reactions };
if (!reactions[emoji]) {
reactions[emoji] = [];
}
const userId = prev.currentUser.id;
if (!reactions[emoji].includes(userId)) {
reactions[emoji] = [...reactions[emoji], userId];
}
return { ...message, reactions };
}
return message;
})
}));
};
// ๐ฅ Get user by ID
const getUserById = (userId: string) => {
return chatState().users.find(user => user.id === userId);
};
// ๐ Typing users display
const typingUsers = createMemo(() => {
return chatState().users.filter(user =>
user.isTyping && user.id !== chatState().currentUser.id
);
});
return (
<div class="chat-app">
<h2>๐ฌ TypeScript Chat Room</h2>
{/* ๐ Messages list */}
<div class="messages">
{chatState().messages.map(message => {
const user = getUserById(message.userId);
return (
<div class="message">
<div class="message-header">
<span class="avatar">{user?.avatar}</span>
<span class="name">{user?.name}</span>
<span class="timestamp">
{message.timestamp.toLocaleTimeString()}
</span>
</div>
<div class="content">{message.content}</div>
{/* ๐ Reactions */}
<div class="reactions">
{Object.entries(message.reactions).map(([emoji, userIds]) => (
<button
class="reaction"
onClick={() => addReaction(message.id, emoji)}
>
{emoji} {userIds.length}
</button>
))}
{/* ๐ Quick reaction buttons */}
<button onClick={() => addReaction(message.id, '๐')}>๐</button>
<button onClick={() => addReaction(message.id, 'โค๏ธ')}>โค๏ธ</button>
<button onClick={() => addReaction(message.id, '๐')}>๐</button>
</div>
</div>
);
})}
{/* โจ๏ธ Typing indicator */}
{typingUsers().length > 0 && (
<div class="typing-indicator">
{typingUsers().map(user => (
<span>{user.avatar} {user.name}</span>
)).join(', ')} {typingUsers().length === 1 ? 'is' : 'are'} typing...
</div>
)}
</div>
{/* ๐ฌ Message input */}
<div class="message-input">
<input
type="text"
value={newMessage()}
onInput={(e) => handleTyping(e.currentTarget.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type a message... ๐ฌ"
/>
<button onClick={sendMessage} disabled={!newMessage().trim()}>
Send ๐
</button>
</div>
</div>
);
};
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create reactive Solid.js components with confidence ๐ช
- โ Avoid common mistakes that trip up beginners ๐ก๏ธ
- โ Apply best practices in real projects ๐ฏ
- โ Debug reactivity issues like a pro ๐
- โ Build performant apps with Solid.js and TypeScript! ๐
Remember: Solid.js is your performance-focused friend! It gives you React-like DX with better runtime performance. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Solid.js with TypeScript!
Hereโs what to do next:
- ๐ป Practice with the chat app exercise above
- ๐๏ธ Build a small project using Solid.js routing
- ๐ Move on to our next tutorial: SvelteKit with TypeScript
- ๐ Share your reactive apps with the community!
Remember: Every Solid.js expert was once a beginner. Keep building, keep learning, and most importantly, enjoy the blazing-fast performance! ๐
Happy coding! ๐๐โจ