diff --git a/exercises/01.managing-ui-state/05.problem.cb/README.mdx b/exercises/01.managing-ui-state/05.problem.cb/README.mdx
new file mode 100644
index 000000000..e7dccc16e
--- /dev/null
+++ b/exercises/01.managing-ui-state/05.problem.cb/README.mdx
@@ -0,0 +1,43 @@
+# Init Callback
+
+π¦ There's one more thing you should know about `useState` initialization and
+that is a small performance optimization. `useState` can accept a function.
+
+You may recall from earlier we mentioned that the first argument to `useState`
+is only used during the initial render. It's not used on subsequent renders.
+This is because the initial value is only used when the component is first
+rendered. After that, the value is managed by React and you use the updater
+function to update it.
+
+But imagine a situation where calculating that initial value were
+computationally expensive. It would be a waste to compute the initial value for
+all but the initial render right? That's where the function form of `useState`
+comes in.
+
+Let's imagine we have a function that calculates the initial value and it's
+computationally expensive:
+
+```tsx
+const [val, setVal] = useState(calculateInitialValue())
+```
+
+This will work just fine, but it's not ideal. The `calculateInitialValue` will
+be called on every render, even though it's only needed for the initial render.
+So instead of calling the function, we can just pass it:
+
+```tsx
+const [val, setVal] = useState(calculateInitialValue)
+```
+
+Typically doing this is unnecessary, but it's good to know about in case you
+need it.
+
+So
+
+```tsx
+// both of these work just fine:
+const [query, setQuery] = useState(getQueryParam())
+const [query, setQuery] = useState(getQueryParam)
+```
+
+You're going to be making the `getQueryParam` function. Got it? Great, let's go!
diff --git a/exercises/01.managing-ui-state/05.problem.cb/index.css b/exercises/01.managing-ui-state/05.problem.cb/index.css
new file mode 100644
index 000000000..0cdfd44a3
--- /dev/null
+++ b/exercises/01.managing-ui-state/05.problem.cb/index.css
@@ -0,0 +1,89 @@
+html,
+body {
+ margin: 0;
+}
+
+.app {
+ margin: 40px auto;
+ max-width: 1024px;
+ form {
+ text-align: center;
+ }
+}
+
+.post-list {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+ justify-content: center;
+ li {
+ position: relative;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ border: 1px solid #ddd;
+ width: 320px;
+ transition: transform 0.2s ease-in-out;
+ a {
+ text-decoration: none;
+ color: unset;
+ }
+
+ &:hover,
+ &:has(*:focus),
+ &:has(*:active) {
+ transform: translate(0px, -6px);
+ }
+
+ .post-image {
+ display: block;
+ width: 100%;
+ height: 200px;
+ }
+
+ button {
+ position: absolute;
+ font-size: 1.5rem;
+ top: 20px;
+ right: 20px;
+ background: transparent;
+ border: none;
+ outline: none;
+ &:hover,
+ &:focus,
+ &:active {
+ animation: pulse 1.5s infinite;
+ }
+ }
+
+ a {
+ padding: 10px 10px;
+ display: flex;
+ gap: 8px;
+ flex-direction: column;
+ h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: bold;
+ }
+ p {
+ margin: 0;
+ font-size: 1rem;
+ color: #666;
+ }
+ }
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.3);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
diff --git a/exercises/01.managing-ui-state/05.problem.cb/index.tsx b/exercises/01.managing-ui-state/05.problem.cb/index.tsx
new file mode 100644
index 000000000..54d77ed50
--- /dev/null
+++ b/exercises/01.managing-ui-state/05.problem.cb/index.tsx
@@ -0,0 +1,103 @@
+import { useState } from 'react'
+import * as ReactDOM from 'react-dom/client'
+import { generateGradient, getMatchingPosts } from '#shared/blog-posts'
+
+// π¨ make a function here called getQueryParam
+
+function App() {
+ // π¨ move π up to getQueryParam
+ const params = new URLSearchParams(window.location.search)
+ const initialQuery = params.get('query') ?? ''
+ // π¨ move π up to getQueryParam and return the initialQuery
+
+ // π¨ pass getQueryParam into useState
+ const [query, setQuery] = useState(initialQuery)
+ const words = query.split(' ')
+
+ const dogChecked = words.includes('dog')
+ const catChecked = words.includes('cat')
+ const caterpillarChecked = words.includes('caterpillar')
+
+ function handleCheck(tag: string, checked: boolean) {
+ const newWords = checked ? [...words, tag] : words.filter(w => w !== tag)
+ setQuery(newWords.filter(Boolean).join(' ').trim())
+ }
+
+ return (
+
+ )
+}
+
+const rootEl = document.createElement('div')
+document.body.append(rootEl)
+ReactDOM.createRoot(rootEl).render()
diff --git a/exercises/02.side-effects/01.problem.effects/README.mdx b/exercises/02.side-effects/01.problem.effects/README.mdx
new file mode 100644
index 000000000..c9cd6d072
--- /dev/null
+++ b/exercises/02.side-effects/01.problem.effects/README.mdx
@@ -0,0 +1,74 @@
+# useEffect
+
+π¨βπΌ We want the query to update as the query params change through a `popstate`
+event. Once you added the `useEffect` hook, you then have to prevent the default
+full refresh of the page when the user clicks the back/forward button.
+
+π§ββοΈ I added a new utility called `setGlobalSearchParams` which allows us to set
+the URL search params without triggering a full-page refresh. So whenever the
+user clicks "submit," we update the URL search params. The trouble is, when they
+hit the back button, the search doesn't stay synchronoized with the URL (yet).
+
+You can take a look at my changes for
+details on what I did.
+
+π¨βπΌ Thanks Kellie. So what we you to do is make it so when the user hits the back
+button in their browser and the search params are updated, we update the input
+value.
+
+The only way to be notified of a back/forward event is to listen for the
+`popstate` event. You can do this by adding an event listener to the `window`
+object.
+
+```javascript
+window.addEventListener('popstate', () => {
+ // update the input value
+})
+```
+
+To test whether you got this right follow these steps:
+
+- Go to (in a new tab)
+- Add " cat" in the input
+- Click "submit"
+- Click the back button in your browser
+- The input value should now be "dog"
+
+That last step will be broken and that's what you should fix in this step.
+
+
+ Spoiler alert: there's going to be a bug with our implementation that we'll
+ fix in the next step, so if you notice a bug... we'll get to it π
+
+
+One more thing, we're going to need to retrieve the query param in two places.
+Right now we get it to initialize our state, but we'll also want to get it when
+the `popstate` event fires. So you'll be extracting that logic to a small
+function. This will introduce another feature of React's `useState` hook that
+we'll take advantage of for the first time and that is the callback form of
+`useState`. So you should know that these are equivalent:
+
+```tsx
+const [val, setVal] = useState('hello')
+// will result in the same thing as:
+const [val, setVal] = useState(function getHello() {
+ return 'hello'
+})
+```
+
+What's the difference? The second form is useful when you creating the initial
+state is computationally expensive. So it's a performance optimization because
+it allows you to place the expensive stuff inside the function and then it will
+only be called during the initial render. This is a bit of a tangent, but I
+thought it was worth mentioning.
+
+Typically it's unnecessary, but since we're making a function anyway, you can
+simply pass the function to `useState` directly:
+
+```tsx
+// both of these work just fine:
+const [query, setQuery] = useState(getQueryParam())
+const [query, setQuery] = useState(getQueryParam)
+```
+
+You're going to be making the `getQueryParam` function. Got it? Great, let's go!
diff --git a/exercises/02.side-effects/01.problem.effects/index.css b/exercises/02.side-effects/01.problem.effects/index.css
new file mode 100644
index 000000000..0cdfd44a3
--- /dev/null
+++ b/exercises/02.side-effects/01.problem.effects/index.css
@@ -0,0 +1,89 @@
+html,
+body {
+ margin: 0;
+}
+
+.app {
+ margin: 40px auto;
+ max-width: 1024px;
+ form {
+ text-align: center;
+ }
+}
+
+.post-list {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+ justify-content: center;
+ li {
+ position: relative;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ border: 1px solid #ddd;
+ width: 320px;
+ transition: transform 0.2s ease-in-out;
+ a {
+ text-decoration: none;
+ color: unset;
+ }
+
+ &:hover,
+ &:has(*:focus),
+ &:has(*:active) {
+ transform: translate(0px, -6px);
+ }
+
+ .post-image {
+ display: block;
+ width: 100%;
+ height: 200px;
+ }
+
+ button {
+ position: absolute;
+ font-size: 1.5rem;
+ top: 20px;
+ right: 20px;
+ background: transparent;
+ border: none;
+ outline: none;
+ &:hover,
+ &:focus,
+ &:active {
+ animation: pulse 1.5s infinite;
+ }
+ }
+
+ a {
+ padding: 10px 10px;
+ display: flex;
+ gap: 8px;
+ flex-direction: column;
+ h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: bold;
+ }
+ p {
+ margin: 0;
+ font-size: 1rem;
+ color: #666;
+ }
+ }
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.3);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
diff --git a/exercises/02.side-effects/01.problem.effects/index.tsx b/exercises/02.side-effects/01.problem.effects/index.tsx
new file mode 100644
index 000000000..f60bd834f
--- /dev/null
+++ b/exercises/02.side-effects/01.problem.effects/index.tsx
@@ -0,0 +1,112 @@
+import { useState } from 'react'
+import * as ReactDOM from 'react-dom/client'
+import { generateGradient, getMatchingPosts } from '#shared/blog-posts'
+import { setGlobalSearchParams } from '#shared/utils'
+
+function getQueryParam() {
+ const params = new URLSearchParams(window.location.search)
+ return params.get('query') ?? ''
+}
+
+function App() {
+ const [query, setQuery] = useState(getQueryParam)
+
+ const words = query.split(' ')
+
+ const dogChecked = words.includes('dog')
+ const catChecked = words.includes('cat')
+ const caterpillarChecked = words.includes('caterpillar')
+
+ // π¨ add a useEffect(() => {}, []) call here (we'll talk about that empty array later)
+ // π¨ in the useEffect callback, subscribe to window's popstate event
+ // π¨ your event handler should call setQuery to getQueryParam()
+ // π https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
+
+ function handleCheck(tag: string, checked: boolean) {
+ const newWords = checked ? [...words, tag] : words.filter(w => w !== tag)
+ setQuery(newWords.filter(Boolean).join(' ').trim())
+ }
+
+ return (
+
+ )
+}
+
+const rootEl = document.createElement('div')
+document.body.append(rootEl)
+ReactDOM.createRoot(rootEl).render()
diff --git a/exercises/02.side-effects/02.problem.cleanup/README.mdx b/exercises/02.side-effects/02.problem.cleanup/README.mdx
new file mode 100644
index 000000000..cbaa147e7
--- /dev/null
+++ b/exercises/02.side-effects/02.problem.cleanup/README.mdx
@@ -0,0 +1,67 @@
+# Effect Cleanup
+
+π¨βπΌ We've got an issue with our `useEffect` callback here that needs some
+attention. You won't be able to tell without a little bit extra in the app, so
+Kellie (π§ββοΈ) has put together a demo.
+
+π§ββοΈ Yep, so now we have a checkbox that says "show form." When you check it,
+it'll show the form and the results, when you uncheck it, those will be removed.
+In dynamic applications we have components that are added and removed from the
+page all the time, so you definitelly will have situations like this.
+
+π¨βπΌ Thanks Kellie. Olivia (π¦) would like to talk to you about memory leaks.
+
+π¦ Thanks Peter. So, let's review what's going on. When our component is
+rendered, we subscribe to the `popstate` event. The callback we pass to the
+`addEventListener` method creates a closure over all the variables in the
+function's scope. This means that when the callback is called, it has access to
+those values. What that means is that as long as that function exists and is
+referenced by something else in the application, those values will be kept in
+memory as well just in case the callback is called again.
+
+As a result, when the component is removed from the page, the callback is still
+referenced by the `popstate` event, and so the values are kept in memory. So
+imagine if you have a component that is added and removed from the page many
+times, and each time it's added, it subscribes to an event and adds more to the
+memory, but that memory is never released because even when the component is
+removed from the page the event still has a reference to the callback which is
+hanging on to all the values!
+
+This is called a memory leak and will make your application slower and use more
+memory than it needs to (leading to a bad user experience). Whether you're using
+React or anything else, you should always be aware of memory leaks and how to
+avoid them. In general, whenever you find yourself adding an event listener or
+subscribing to something, you should always make sure to remove that listener or
+subscription when you're finished with it.
+
+So in a React context, this means that you should always clean up your effects
+when the component is removed from the page. The way to do this is to return a
+function from the effect that removes the listener or subscription:
+
+```tsx
+useEffect(() => {
+ function handleEvent() {
+ // some-event happened!
+ }
+ window.addEventListener('some-event', handleEvent)
+ return () => {
+ window.removeEventListener('some-event', handleEvent)
+ }
+}, [])
+```
+
+This way, when the component is removed from the page, React will call the
+cleanup function and remove the listener or subscription.
+
+π¨βπΌ Great. Now that we've got that out of the way, let's handle this in our app.
+
+You can add `console.log` statements to make sure things are being called (unless
+you want to open up the memory profiling tab in your dev tools and click the
+checkbox a bunch of times to see the memory usage go up π ).
+
+π― If you really do want to test this out, I've added a couple lines you can
+uncomment to allocate huge amounts of memory to huge arrays. If you uncomment
+those, then watch the quick climb of the memory in the
+[Memory tab of dev tools](https://developer.chrome.com/docs/devtools/memory) or
+[Browser Task manager](https://developer.chrome.com/docs/devtools/memory-problems#monitor_memory_use_in_realtime_with_the_chrome_task_manager)
+every time you check and uncheck the box.
diff --git a/exercises/02.side-effects/02.problem.cleanup/index.css b/exercises/02.side-effects/02.problem.cleanup/index.css
new file mode 100644
index 000000000..0cdfd44a3
--- /dev/null
+++ b/exercises/02.side-effects/02.problem.cleanup/index.css
@@ -0,0 +1,89 @@
+html,
+body {
+ margin: 0;
+}
+
+.app {
+ margin: 40px auto;
+ max-width: 1024px;
+ form {
+ text-align: center;
+ }
+}
+
+.post-list {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+ justify-content: center;
+ li {
+ position: relative;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ border: 1px solid #ddd;
+ width: 320px;
+ transition: transform 0.2s ease-in-out;
+ a {
+ text-decoration: none;
+ color: unset;
+ }
+
+ &:hover,
+ &:has(*:focus),
+ &:has(*:active) {
+ transform: translate(0px, -6px);
+ }
+
+ .post-image {
+ display: block;
+ width: 100%;
+ height: 200px;
+ }
+
+ button {
+ position: absolute;
+ font-size: 1.5rem;
+ top: 20px;
+ right: 20px;
+ background: transparent;
+ border: none;
+ outline: none;
+ &:hover,
+ &:focus,
+ &:active {
+ animation: pulse 1.5s infinite;
+ }
+ }
+
+ a {
+ padding: 10px 10px;
+ display: flex;
+ gap: 8px;
+ flex-direction: column;
+ h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: bold;
+ }
+ p {
+ margin: 0;
+ font-size: 1rem;
+ color: #666;
+ }
+ }
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.3);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
diff --git a/exercises/02.side-effects/02.problem.cleanup/index.tsx b/exercises/02.side-effects/02.problem.cleanup/index.tsx
new file mode 100644
index 000000000..ca9fdf2e7
--- /dev/null
+++ b/exercises/02.side-effects/02.problem.cleanup/index.tsx
@@ -0,0 +1,141 @@
+import { useEffect, useState } from 'react'
+import * as ReactDOM from 'react-dom/client'
+import { generateGradient, getMatchingPosts } from '#shared/blog-posts'
+import { setGlobalSearchParams } from '#shared/utils'
+
+function getQueryParam() {
+ const params = new URLSearchParams(window.location.search)
+ return params.get('query') ?? ''
+}
+
+function App() {
+ const [query, setQuery] = useState(getQueryParam)
+
+ const words = query.split(' ')
+
+ const dogChecked = words.includes('dog')
+ const catChecked = words.includes('cat')
+ const caterpillarChecked = words.includes('caterpillar')
+
+ useEffect(() => {
+ // π― you can use this to test whether your cleanup is working (make sure to include the console.log below as well)
+ // const hugeData = new Array(1_000_000).fill(new Array(1_000_000).fill('πΆπ±π'))
+
+ // π¨ extract your event handler here into a function called updateQuery
+ window.addEventListener('popstate', () => {
+ // π― you can use this to test whether your cleanup is freeing up memory
+ // console.log(hugeData)
+
+ console.log('popstate event listener called')
+ setQuery(getQueryParam())
+ })
+ // π¨ return a function which removes the popstate event listener
+ // π https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
+ }, [])
+
+ function handleCheck(tag: string, checked: boolean) {
+ const newWords = checked ? [...words, tag] : words.filter(w => w !== tag)
+ setQuery(newWords.filter(Boolean).join(' ').trim())
+ }
+
+ return (
+
+ )
+}
+
+const rootEl = document.createElement('div')
+document.body.append(rootEl)
+ReactDOM.createRoot(rootEl).render()
diff --git a/exercises/02.side-effects/FINISHED.mdx b/exercises/02.side-effects/FINISHED.mdx
new file mode 100644
index 000000000..134c6685a
--- /dev/null
+++ b/exercises/02.side-effects/FINISHED.mdx
@@ -0,0 +1,7 @@
+# Side-Effects
+
+π¨βπΌ Great work! You've learned the basics of how to trigger side-effects when our
+component is added to the page and how to properly clean up when it's through.
+You've not learned everything there is to know about `useEffect` yet (that
+dependency array is still a topic we need to get into), but we'll get into the
+rest of that soon.
diff --git a/exercises/02.side-effects/README.mdx b/exercises/02.side-effects/README.mdx
new file mode 100644
index 000000000..a13e8b984
--- /dev/null
+++ b/exercises/02.side-effects/README.mdx
@@ -0,0 +1,105 @@
+# Side-Effects
+
+`useEffect` is a built-in hook that allows you to run some custom code
+after React renders (and re-renders) your component to the DOM. It accepts a
+callback function which React will call after the DOM has been updated:
+
+```javascript
+useEffect(() => {
+ // your side-effect code here.
+ // this is where you can interact with browser APIs for example
+ doSomeThing()
+ return function cleanup() {
+ // if you need to clean up after your side-effect (like unsubscribe from an
+ // event), you can do it here
+ doSomeCleanup()
+ }
+}, [
+ // this is where dependencies of your useEffect callback go
+ // we'll talk about this in depth in a future exercise.
+ // In this exercise, we'll just leave it as an empty array
+ dep1,
+ dep2,
+])
+```
+
+`useState` is for managing our react component state and `useEffect` is for
+managing side-effects. Side-effects are things that happen outside our react
+components.
+
+For example, things outside our react components include:
+
+- Browser APIs like local storage, geolocation, media devices, etc.
+- Integrations with third-party libraries like D3, Chart.js, etc.
+
+Check out [the React Flow diagram](https://github.com/donavon/hook-flow) below:
+
+![React Flow diagram showing mount, update, unmount](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/295689283-b9ecdd1d-ce28-446b-84ad-6b264d4be8e4.png)
+
+The graphic illustrates the lifecycle of a React component, focusing on how
+hooks behave during different phases: Mount, Update, and Unmount. It's
+structured into different sections, each representing a stage in the component
+lifecycle, and provides a visual flow of the order in which React hooks and
+other operations are executed. Here's a breakdown:
+
+1. **Mount Phase**:
+ - **Run lazy initializers**: This step involves executing any lazy
+ initialization functions provided to hooks like `useState` or `useReducer`.
+ These functions are only run during the initial render.
+2. **Update Phase** (triggered by a parent re-render, state change, or context
+ change):
+ - **Render**: The component re-renders, evaluating its function body.
+ - **React updates DOM**: React applies any changes from the render phase to
+ the DOM.
+ - **Cleanup LayoutEffects**: Before running any new layout effects, React
+ cleans up the previous ones defined in `useLayoutEffect`.
+ - **Run LayoutEffects**: Runs the effects defined in `useLayoutEffect`
+ immediately after DOM updates, blocking the browser painting until
+ complete.
+ - **Browser paints screen**: The browser updates the visual representation of
+ the page.
+ - **Cleanup Effects**: Cleans up any effects defined in `useEffect` from the
+ previous render.
+ - **Run Effects**: Runs the effects defined in `useEffect`. These are
+ scheduled to run after the paint, so they don't block the browser from
+ updating the screen.
+3. **Unmount Phase**:
+ - React performs cleanup for both `useEffect` and `useLayoutEffect` hooks,
+ preventing memory leaks by removing event listeners, canceling network
+ requests, or invalidating timers set up by the component.
+
+**Notes** at the bottom highlight key concepts:
+
+- **Updates** are triggered by re-renders from parent components, state changes,
+ or context changes.
+- **Lazy initializers** are functions that initialize state lazily, meaning the
+ initial state is computed only on the initial render, potentially optimizing
+ performance.
+
+The different colors in the graphic signify various stages and types of
+operations within the React component lifecycle, specifically relating to the
+execution of hooks and rendering processes. Each color represents a distinct
+group of operations:
+
+1. **Green (Top section)**: This color is associated with the initial setup
+ phase of a component, including running lazy initializers which are functions
+ provided to hooks like `useState` and `useReducer` for setting the initial
+ state.
+2. **Red (Middle section)**: Represents operations related to the
+ DOM updates and the pre-paint phase. This includes the rendering process,
+ where React evaluates the component and updates the DOM, followed by the
+ cleanup and execution of layout effects (`useLayoutEffect`). These operations
+ are crucial for ensuring that any DOM manipulations or measurements happen
+ synchronously before the browser paints.
+3. **Yellow (Bottom section)**: Focuses on post-paint effects, encapsulating the
+ cleanup and execution of side effects (`useEffect`). These operations are
+ scheduled after painting, allowing for non-blocking operations like data
+ fetching, subscriptions, or manually triggering DOM updates. These effects
+ run asynchronously to avoid delaying the visual update of the page.
+
+This diagram is a helpful reference for understanding the sequence and timing of
+React's hook-based lifecycle methods, which is crucial for correctly managing
+side effects, subscriptions, and manual DOM manipulations in functional
+components.
+
+This will make more sense after finishing the exercise. So come back!
diff --git a/exercises/03.lifting-state/01.solution.lift/index.tsx b/exercises/03.lifting-state/01.solution.lift/index.tsx
index b2bb455c2..9b6a6b9bf 100644
--- a/exercises/03.lifting-state/01.solution.lift/index.tsx
+++ b/exercises/03.lifting-state/01.solution.lift/index.tsx
@@ -21,7 +21,7 @@ function App() {
return () => {
window.removeEventListener('popstate', updateQuery)
}
- }, [setQuery])
+ }, [])
return (