Time to add interactivity to Kraken's Call! You'll learn to manage application state and create forms.
useState is a React hook that allows components to "remember" things.
1'use client';
2
3import { useState } from 'react';
4
5export default function Counter() {
6 // [value, setter function] = useState(initial value)
7 const [count, setCount] = useState(0);
8
9 return (
10 <div>
11 <p>Counter: {count}</p>
12 <button onClick={() => setCount(count + 1)}>
13 +1
14 </button>
15 </div>
16 );
17}How it works:
useState(0) - creates state with initial value 0count - current valuesetCount - function to change the valuesetCount changes count → component re-renders1const [name, setName] = useState('');
2
3<input
4 value={name}
5 onChange={(e) => setName(e.target.value)}
6/>1const [isOpen, setIsOpen] = useState(false);
2
3<button onClick={() => setIsOpen(!isOpen)}>
4 Toggle
5</button>1const [items, setItems] = useState<string[]>([]);
2
3const addItem = (item: string) => {
4 setItems([...items, item]);
5};1const [user, setUser] = useState({
2 name: 'Redbeard',
3 age: 35
4});
5
6setUser({ ...user, age: 36 });Let's create a form for adding new posts:
1'use client';
2
3import { useState } from 'react';
4
5export default function CreatePostForm() {
6 const [content, setContent] = useState('');
7 const [charCount, setCharCount] = useState(0);
8 const MAX_LENGTH = 280;
9
10 const handleSubmit = (e: React.FormEvent) => {
11 e.preventDefault(); // Prevents page reload
12
13 if (content.trim()) {
14 console.log('New post:', content);
15 setContent(''); // Clear the form
16 setCharCount(0);
17 }
18 };
19
20 const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
21 const text = e.target.value;
22 setContent(text);
23 setCharCount(text.length);
24 };
25
26 return (
27 <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow">
28 <textarea
29 value={content}
30 onChange={handleChange}
31 maxLength={MAX_LENGTH}
32 className="w-full p-4 border rounded-lg resize-none focus:ring-2 focus:ring-blue-500 outline-none"
33 rows={4}
34 />
35
36 <div className="flex items-center justify-between mt-4">
37 <span className={`text-sm ${
38 charCount > MAX_LENGTH * 0.9 ? 'text-red-500' : 'text-gray-500'
39 }`}>
40 {charCount}/{MAX_LENGTH}
41 </span>
42
43 <button
44 type="submit"
45 disabled={!content.trim() || charCount > MAX_LENGTH}
46 className="bg-blue-600 text-white px-6 py-2 rounded-lg font-semibold disabled:bg-gray-300 disabled:cursor-not-allowed hover:bg-blue-700 transition"
47 >
48 Send 🚀
49 </button>
50 </div>
51 </form>
52 );
53}Now let's connect the form with the list of posts:
1'use client';
2
3import { useState } from 'react';
4
5interface Post {
6 id: number;
7 author: string;
8 avatar: string;
9 content: string;
10 timestamp: Date;
11 likes: number;
12}
13
14export default function Feed() {
15 const [posts, setPosts] = useState<Post[]>([
16 {
17 id: 1,
18 author: 'Captain Redbeard',
19 avatar: '🏴☠️',
20 content: 'We just discovered a new island!',
21 timestamp: new Date(),
22 likes: 42,
23 },
24 ]);
25
26 const [newPostContent, setNewPostContent] = useState('');
27
28 const handleSubmit = (e: React.FormEvent) => {
29 e.preventDefault();
30
31 if (!newPostContent.trim()) return;
32
33 const newPost: Post = {
34 id: Date.now(),
35 author: 'Morgan Blackheart',
36 avatar: '⚓',
37 content: newPostContent,
38 timestamp: new Date(),
39 likes: 0,
40 };
41
42 setPosts([newPost, ...posts]); // Add to the beginning
43 setNewPostContent('');
44 };
45
46 return (
47 <div className="max-w-2xl mx-auto p-8">
48 {/* Form */}
49 <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow mb-6">
50 <textarea
51 value={newPostContent}
52 onChange={(e) => setNewPostContent(e.target.value)}
53 className="w-full p-4 border rounded-lg"
54 rows={3}
55 />
56 <button
57 type="submit"
58 className="mt-4 bg-blue-600 text-white px-6 py-2 rounded-lg"
59 >
60 Send
61 </button>
62 </form>
63
64 {/* Post list */}
65 <div className="space-y-4">
66 {posts.map((post) => (
67 <div key={post.id} className="bg-white p-6 rounded-lg shadow">
68 <div className="flex items-center gap-3 mb-4">
69 <div className="text-3xl">{post.avatar}</div>
70 <div>
71 <h3 className="font-bold">{post.author}</h3>
72 <p className="text-sm text-gray-500">
73 {post.timestamp.toLocaleTimeString('en-US')}
74 </p>
75 </div>
76 </div>
77 <p className="text-gray-800">{post.content}</p>
78 <div className="mt-4 text-gray-500">
79 ❤️ {post.likes}
80 </div>
81 </div>
82 ))}
83 </div>
84 </div>
85 );
86}When two components need the same state, we move it to their common parent.
1'use client';
2
3import { useState } from 'react';
4
5// Like button component
6function LikeButton({
7 likes,
8 onLike
9}: {
10 likes: number;
11 onLike: () => void;
12}) {
13 return (
14 <button onClick={onLike} className="flex items-center gap-2">
15 ❤️ {likes}
16 </button>
17 );
18}
19
20// Like counter component
21function LikeCounter({ likes }: { likes: number }) {
22 return (
23 <div className="text-sm text-gray-500">
24 Total likes on page: {likes}
25 </div>
26 );
27}
28
29// Parent holds the state
30export default function Post() {
31 const [likes, setLikes] = useState(42);
32
33 const handleLike = () => {
34 setLikes(likes + 1);
35 };
36
37 return (
38 <div className="bg-white p-6 rounded-lg">
39 <p className="mb-4">Post content...</p>
40
41 <LikeButton likes={likes} onLike={handleLike} />
42 <LikeCounter likes={likes} />
43 </div>
44 );
45}useEffect runs code after render (e.g. fetching data, timers).1'use client';
2
3import { useState, useEffect } from 'react';
4
5export default function Timer() {
6 const [seconds, setSeconds] = useState(0);
7
8 useEffect(() => {
9 // Run after each render
10 const interval = setInterval(() => {
11 setSeconds((s) => s + 1);
12 }, 1000);
13
14 // Cleanup - remove timer when component disappears
15 return () => clearInterval(interval);
16 }, []); // [] = run only once
17
18 return <div>Elapsed: {seconds}s</div>;
19}1// Run only once (on mount)
2useEffect(() => {
3 console.log('Component mounted');
4}, []);
5
6// Run when count changes
7useEffect(() => {
8 console.log('Count changed:', count);
9}, [count]);
10
11// Run after every render
12useEffect(() => {
13 console.log('Every render');
14}); // No []Save posts in the browser so they don't disappear after refresh:
1'use client';
2
3import { useState, useEffect } from 'react';
4
5export default function Feed() {
6 const [posts, setPosts] = useState<Post[]>([]);
7
8 // Load from localStorage on start
9 useEffect(() => {
10 const saved = localStorage.getItem('kraken-posts');
11 if (saved) {
12 setPosts(JSON.parse(saved));
13 }
14 }, []);
15
16 // Save to localStorage when posts change
17 useEffect(() => {
18 localStorage.setItem('kraken-posts', JSON.stringify(posts));
19 }, [posts]);
20
21 // Rest of the code...
22}✅ useState - hook for state management ✅ Forms -
value, onChange, onSubmit
✅ e.preventDefault() - prevents page reload
✅ Dynamic lists - adding/removing elements
✅ Lifting state up - sharing state between components
✅ useEffect - side effects (timers, fetch, localStorage)
✅ Dependency array - controls when the effect runs
✅ localStorage - saving data in the browserNow you have a complete, interactive Kraken's Call application! 🎉
See you there! 🚀