|
| 1 | +import { useCallback, memo, useMemo, useEffect, useState } from "react"; |
| 2 | +import { faker } from "@faker-js/faker"; |
| 3 | + |
| 4 | +function createRandomPost() { |
| 5 | + return { |
| 6 | + title: `${faker.hacker.adjective()} ${faker.hacker.noun()}`, |
| 7 | + body: faker.hacker.phrase() |
| 8 | + }; |
| 9 | +} |
| 10 | + |
| 11 | +function App() { |
| 12 | + const [posts, setPosts] = useState(() => |
| 13 | + Array.from({ length: 30 }, () => createRandomPost()) |
| 14 | + ); |
| 15 | + const [searchQuery, setSearchQuery] = useState(""); |
| 16 | + const [isFakeDark, setIsFakeDark] = useState(false); |
| 17 | + |
| 18 | + // Derived state. These are the posts that will actually be displayed |
| 19 | + const searchedPosts = |
| 20 | + searchQuery.length > 0 |
| 21 | + ? posts.filter(post => |
| 22 | + `${post.title} ${post.body}` |
| 23 | + .toLowerCase() |
| 24 | + .includes(searchQuery.toLowerCase()) |
| 25 | + ) |
| 26 | + : posts; |
| 27 | + |
| 28 | + const handleAddPost = useCallback( |
| 29 | + function handleAddPost(post) { |
| 30 | + setPosts(posts => [post, ...posts]); |
| 31 | + console.log(posts); |
| 32 | + }, |
| 33 | + [posts] |
| 34 | + ); |
| 35 | + |
| 36 | + function handleClearPosts() { |
| 37 | + setPosts([]); |
| 38 | + } |
| 39 | + |
| 40 | + // Whenever `isFakeDark` changes, we toggle the `fake-dark-mode` class on the HTML element (see in "Elements" dev tool). |
| 41 | + useEffect( |
| 42 | + function () { |
| 43 | + document.documentElement.classList.toggle("fake-dark-mode"); |
| 44 | + }, |
| 45 | + [isFakeDark] |
| 46 | + ); |
| 47 | + |
| 48 | + const archiveOption = useMemo(() => { |
| 49 | + return { show: false, title: `Post archive ${posts.length}` }; |
| 50 | + }, [posts.length]); |
| 51 | + |
| 52 | + return ( |
| 53 | + <section> |
| 54 | + <button |
| 55 | + onClick={() => setIsFakeDark(isFakeDark => !isFakeDark)} |
| 56 | + className="btn-fake-dark-mode" |
| 57 | + > |
| 58 | + {isFakeDark ? "☀️" : "🌙"} |
| 59 | + </button> |
| 60 | + |
| 61 | + <Header |
| 62 | + posts={searchedPosts} |
| 63 | + onClearPosts={handleClearPosts} |
| 64 | + searchQuery={searchQuery} |
| 65 | + setSearchQuery={setSearchQuery} |
| 66 | + /> |
| 67 | + <Main posts={searchedPosts} onAddPost={handleAddPost} /> |
| 68 | + <Archive |
| 69 | + archiveOption={archiveOption} |
| 70 | + onAddPost={handleAddPost} |
| 71 | + setIsFakeDark={setIsFakeDark} |
| 72 | + /> |
| 73 | + <Footer /> |
| 74 | + </section> |
| 75 | + ); |
| 76 | +} |
| 77 | + |
| 78 | +function Header({ posts, onClearPosts, searchQuery, setSearchQuery }) { |
| 79 | + return ( |
| 80 | + <header> |
| 81 | + <h1> |
| 82 | + <span>⚛️</span>The Atomic Blog |
| 83 | + </h1> |
| 84 | + <div> |
| 85 | + <Results posts={posts} /> |
| 86 | + <SearchPosts |
| 87 | + searchQuery={searchQuery} |
| 88 | + setSearchQuery={setSearchQuery} |
| 89 | + /> |
| 90 | + <button onClick={onClearPosts}>Clear posts</button> |
| 91 | + </div> |
| 92 | + </header> |
| 93 | + ); |
| 94 | +} |
| 95 | + |
| 96 | +function SearchPosts({ searchQuery, setSearchQuery }) { |
| 97 | + return ( |
| 98 | + <input |
| 99 | + value={searchQuery} |
| 100 | + onChange={e => setSearchQuery(e.target.value)} |
| 101 | + placeholder="Search posts..." |
| 102 | + /> |
| 103 | + ); |
| 104 | +} |
| 105 | + |
| 106 | +function Results({ posts }) { |
| 107 | + return <p>🚀 {posts.length} atomic posts found</p>; |
| 108 | +} |
| 109 | + |
| 110 | +function Main({ posts, onAddPost }) { |
| 111 | + return ( |
| 112 | + <main> |
| 113 | + <FormAddPost onAddPost={onAddPost} /> |
| 114 | + <Posts posts={posts} /> |
| 115 | + </main> |
| 116 | + ); |
| 117 | +} |
| 118 | + |
| 119 | +function Posts({ posts }) { |
| 120 | + return ( |
| 121 | + <section> |
| 122 | + <List posts={posts} /> |
| 123 | + </section> |
| 124 | + ); |
| 125 | +} |
| 126 | + |
| 127 | +function FormAddPost({ onAddPost }) { |
| 128 | + const [title, setTitle] = useState(""); |
| 129 | + const [body, setBody] = useState(""); |
| 130 | + |
| 131 | + const handleSubmit = function (e) { |
| 132 | + e.preventDefault(); |
| 133 | + if (!body || !title) return; |
| 134 | + onAddPost({ title, body }); |
| 135 | + setTitle(""); |
| 136 | + setBody(""); |
| 137 | + }; |
| 138 | + |
| 139 | + return ( |
| 140 | + <form onSubmit={handleSubmit}> |
| 141 | + <input |
| 142 | + value={title} |
| 143 | + onChange={e => setTitle(e.target.value)} |
| 144 | + placeholder="Post title" |
| 145 | + /> |
| 146 | + <textarea |
| 147 | + value={body} |
| 148 | + onChange={e => setBody(e.target.value)} |
| 149 | + placeholder="Post body" |
| 150 | + /> |
| 151 | + <button>Add post</button> |
| 152 | + </form> |
| 153 | + ); |
| 154 | +} |
| 155 | + |
| 156 | +function List({ posts }) { |
| 157 | + return ( |
| 158 | + <ul> |
| 159 | + {posts.map((post, i) => ( |
| 160 | + <li key={i}> |
| 161 | + <h3>{post.title}</h3> |
| 162 | + <p>{post.body}</p> |
| 163 | + </li> |
| 164 | + ))} |
| 165 | + </ul> |
| 166 | + ); |
| 167 | +} |
| 168 | + |
| 169 | +const Archive = memo(function Archive({ archiveOption, onAddPost }) { |
| 170 | + // Here we don't need the setter function. We're only using state to store these posts because the callback function passed into useState (which generates the posts) is only called once, on the initial render. So we use this trick as an optimization technique, because if we just used a regular variable, these posts would be re-created on every render. We could also move the posts outside the components, but I wanted to show you this trick 😉 |
| 171 | + const [posts] = useState(() => |
| 172 | + // 💥 WARNING: This might make your computer slow! Try a smaller `length` first |
| 173 | + Array.from({ length: 10000 }, () => createRandomPost()) |
| 174 | + ); |
| 175 | + |
| 176 | + const [showArchive, setShowArchive] = useState(archiveOption.show); |
| 177 | + |
| 178 | + return ( |
| 179 | + <aside> |
| 180 | + <h2>{archiveOption.title}</h2> |
| 181 | + <button onClick={() => setShowArchive(s => !s)}> |
| 182 | + {showArchive ? "Hide archive posts" : "Show archive posts"} |
| 183 | + </button> |
| 184 | + |
| 185 | + {showArchive && ( |
| 186 | + <ul> |
| 187 | + {posts.map((post, i) => ( |
| 188 | + <li key={i}> |
| 189 | + <p> |
| 190 | + <strong>{post.title}:</strong> {post.body} |
| 191 | + </p> |
| 192 | + <button onClick={() => onAddPost(post)}> |
| 193 | + Add as new post |
| 194 | + </button> |
| 195 | + </li> |
| 196 | + ))} |
| 197 | + </ul> |
| 198 | + )} |
| 199 | + </aside> |
| 200 | + ); |
| 201 | +}); |
| 202 | + |
| 203 | +function Footer() { |
| 204 | + return <footer>© by The Atomic Blog ✌️</footer>; |
| 205 | +} |
| 206 | + |
| 207 | +export default App; |
0 commit comments