Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create hooks exercise for useState and useEffect #18

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book/book.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[book]
authors = ["Dylan Huynh", "Nicole Chun", "Jeremy Le", "Alex Lee"]
authors = ["Dylan Huynh", "Nicole Chun", "Jeremy Le", "Alex Lee", "Hanyuan Li"]
language = "en"
multilingual = false
src = "src"
Expand Down
17 changes: 17 additions & 0 deletions book/src/05_react-hooks/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# React Hooks

In the [introduction to React](../04_react-intro/index.md), we learned a bit about
how the overall *syntax* of React worked (with JSX), and how HTML and JavaScript
can be blended together to create reusable components that can be dynamically
generated. In these exercises, however, we will be covering **hooks** - a React
feature that makes webpages truly dynamic.

## What is a hook?

In React, a hook allows components to use React features, written as a function
call. We have the following exercises to help you familiarise yourself with what
these hooks actually do, and how to use them:

- [`useState` and `useEffect`](./state_and_effect/index.md)

For more information, you can have a look at the [official React documentation for hooks](https://react.dev/reference/react/hooks).
160 changes: 160 additions & 0 deletions book/src/05_react-hooks/state_and_effect/idle_game.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Exercise: `scrip.ts`
- Exercise directory: `/exercises/idle-game`
- Sample answer: `/exercise-solutions/idle-game`

With the power of `useState` and `useEffect`, we are going to make a small **idle game**.

An **idle game** (also known as an **incremental game**) is a game that slowly gets the
player to gain resources automatically - and thus, "idle" from the game and make progress.
Some examples of idle games are:
- [**Cookie Clicker**](https://orteil.dashnet.org/cookieclicker/) - perhaps the most famous
idle game of them all, players click cookies to buy grandmas to bake cookies, farms to dig
cookies out of the ground... all to get as many cookies as humanly possible.
- [**A Dark Room**](https://adarkroom.doublespeakgames.com/) - a minimal, text-based take on
the genre, where you slowly gain resources to advance further and further along an
overarching story.
- [**Clicker Heroes**](https://www.clickerheroes.com/) - increase your damage per second and
defeat stronger and stronger monsters.
- [**Asbury Pines**](https://store.steampowered.com/app/2212790/Asbury_Pines/) - a murder
mystery that uses the mechanics of idle games.

If you want to find more interesting games in the genre, have a look at [r/incremental_games](https://www.reddit.com/r/incremental_games/).

## Premise

You are a software engineer, who has been tasked to write thousands... nay,
*millions* of lines of code for the company's next big thing™️. Easier said than
done... the end is nowhere in sight. However, ever the optimiser, you decide to
code up some tools that can help you get closer and closer to your goal. Do
you have what it takes to be the next tech superstar?

This is... `scrip.ts`.

## Setting up

Before running the app, make sure you have NPM installed (preferably the latest
version).

To run the app itself, run `npm run dev`. Your game looks something like this: ![](images/start.png)

## Tasks

You will need to implement all of your functionality in the functional component
`App`. At the end of all of these exercises, your finished SPA should look something
like this: ![](images/game.png)

See the game in action [here](images/game.mp4).

> This exercise uses **TypeScript** instead of JavaScript. You do not need to worry
> too much about types - most of the code to do with types has already been set up
> for you. If any task does need TypeScript-specific syntax, it will be highlighted
> for you.

### Task 1: Counting lines

Let's first begin by implementing the line counter and the "Add lines" button.

Use `useState` to keep track of the number of lines we've gained, and wire that up to
the button such that every time we click the button, the number of lines increments.

### Task 2: Making it prettier

Making a number go up is cool and all, but it doesn't have enough pizzazz - our next
task is to make the "line of code display" at the very top change whenever we add
a new line of code.

> **NOTE**: We are *not* triggering a change whenever we press the button, that is
> not quite what we want! We want a change whenever the number of lines of code changes
> because in future tasks we're going to be implementing the "idle" part of our idle
> game - allowing the player to buy upgrades which "write lines of code" for them.

Some things have already been implemented for you:

- In the `components` folder, there is a component called `Highlight` - if you
give it a line of code and the programming language that line is in, a line with
correct syntax highlighting will be displayed.
- If you look carefully at `Highlight`, you'll see that the line and the programming
language are represented with something called a `Line`. This is a **type**, which
is a lot like a C `struct` definition. We use types so that if your text editor
is smart enough, it can catch errors before you even run your program.
- In `App.tsx`, `getRandomLine()` is a function that looks through all the files in
`public/code` (the details of which are defined in the constant `CODE_FILES`) and
spits out a random `Line`, to be used in `Highlight`.

It's your job to piece together all these provided functions and components to trigger
an update of `Highlight` every time we gain a new line of code.

### Task 3: Upgrades

We've got a button that makes a number go up, but we're missing the "idle" part
of our idle game. Now we need a way for our player to buy items that can "click"
our button for us.

In the sample solution, we have the following two **resources**:
- The *programmer* produces 0.1 lines of code per second, and costs 20 lines.
- The *forum answer* produces 0.3 lines of code per second, and costs 50 lines.

After each purchase, the price of an resource will go up by **10%** - so after we
purchase our first programmer, the next programmer will cost 22 lines.

For this task, you will need to keep track of the number of each type of resource,
as well as the price to buy another resource.

To encapsulate all the information a resource could contain, a `Resource` type has
been set up for you in `types.ts`:

```ts
export type Resource = {
// The name of the item we want to buy
name: string,
// How much it increases our lines per second (LPS) by
increase: number,
// The number of this type of item we have
amount: number,
// How much it costs
cost: number,
};
```

### Task 4 (extension): The "idle" part

Now comes the hard part - having the number *automatically* go up based on what
resources the player has purchased.

The easiest way to increment our "number of lines" is to calculate how much we
increment by every second (e.g. if we have two programmers and one forum answer,
we will get 0.5 lines every second), and to set up a function to add that increment
every second. This is where the `setInterval()` function in vanilla JS can come
in handy. From the [official docs](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) (emphasis mine):

> The `setInterval()` method... **repeatedly calls a function** or executes a code
> snippet, with a fixed time delay between each call.

Because this is an extension task, we will be covering an aspect of `useEffect`
that isn't on the [main explainer](index.md). Specifically, `useEffect` can
*return a function* - what happens is, whenever the dependency array of an `useEffect`
changes, before re-rendering, the return function gets called. For example:

```jsx
useEffect(() => {
/* ... */
return () => console.log("Changing!");
}, [foo]);
```

Whenever `foo` changes, we will see `Changing!` printed to the console.

This feature of `useEffect` is especially important for this task - whenever we
buy a new resource, we also want to change how much we increment our line counter
by every second. Depending on how you choose to update that increment, you may find
that you need to remove the old "loop" somehow - another built-in JS function,
[`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval),
may be useful.

> You may notice that floating-point arithmetic messes up the game by a fair bit -
> a *bonus challenge* would be to still increment the number of lines by the correct
> amount every second (which could be a decimal amount), but only display an integer
> number of lines, and only update the code block when a *whole* number of lines
> changes.
>
> You may find the type `LineState` can come in handy.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
195 changes: 195 additions & 0 deletions book/src/05_react-hooks/state_and_effect/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# `useState` and `useEffect`

React components, by themselves, are self-contained - they are basically "just
functions", when React decides to render a certain component the corresponding
function just gets called. This is great for representing complex, potentially
repetitive HTML into simpler, reusable parts, but by themselves React components
are not interactive.

Consider the scenario where we want to record how many times a button has been
clicked. We can set up the layout pretty easily within React:

```jsx
const App = () => {
return (
<>
<h1>Number of presses: 0</h1>
<button>Press me!</button>
</>
);
};
```

But how do we keep track of the number of button presses? If we try to put the
variable for the number of presses inside our `App` function itself:

```jsx
const App = () => {
let presses = 0;

return (
<>
<h1>Number of presses: {presses}</h1>
<button onClick={() => { presses += 1; }}>Press me!</button>
</>
);
};
```

But this doesn't work, because whenever React renders `App`, the function gets
called and `presses` gets reset back to 0! What do we do in this case, how do
we get React to "remember" certain variables as they get rendered over and over
again?

We can also try making `presses` a global variable:

```jsx
let presses = 0;

const App = () => {
// Exact same code from last example
};
```

While this would work, imagine if *every* variable that we want React to "remember"
was made into a global variable. It's not necessarily a problem if we have one or
two components, but large-scale apps often have hundreds, if not *thousands*, of
components that depend on each other. You would need tens of thousands of global
variables in total, which can cause headaches - you'd need to make sure the names
don't clash, and you'd need to keep track of which components are using which
variables.

Furthermore, making variables that anyone can change (in programmer-speak, **mutable**)
global is a generally bad idea™️. Because any piece of code in our codebase can access
these global variables by definition, it's very possible that we could accidentally
write code that changes a global variable we're not meant to touch, causing very
hard-to-detect bugs.

Fortunately, React has a solution that is much cleaner.

## Introducing `useState`

`useState` is React's answer. From the [official React guide](https://react.dev/reference/react/hooks):

> **State lets a component "remember" information like user input.** For example,
> a form component can use state to store the input value, while an image gallery
> component can use state to store the selected image index.

This is exactly what we want! Let us rewrite our original example using `useState`:

```jsx
import { useState } from "react";

const App = () => {
let [presses, setPresses] = useState(0);

return (
<>
<h1>Number of presses: {presses}</h1>
<button onClick={() => setPresses(presses + 1)}>Press me!</button>
</>
)
};
```

Notice a few things with `useState`:
- It takes in an argument, which is *the initial value of the variable* when the
component first renders.
- It returns an *array*, where the 0th element is a variable that contains the
value of `presses` itself, and the 1st element is a function that alters the
value of `presses` (which we refer to as `setPresses`).

Copy and paste the above code into a default React app and see it work! `presses`
is "remembered" between renders, and as we click the button the heading changes,
incrementing `presses` with every click.

## Another problem, and introducing `useEffect`

React components are *pure* by default - in other words, if we give a component
the same arguments, it will always return the same output. But what happens if
we don't want that, and we want our components to interact with the outside world
in some way?

For example, let's say we wanted to create a component that updated the webpage's
title:

```jsx
const App = () => {
let [name, setName] = useState("");

// We want to set the page's title to be `name` whenever we edit our input
return (
<>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)} />
</>
);
};
```

The problem here is that `App` doesn't have access to the webpage's title, since
React is only "painting" components onto the screen. Each page's title is actually
controlled by the *browser* itself, which is an external API.

The solution here is to use `useEffect`, which allows us to interact with the
outside world!

```jsx
import { useEffect, useState } from "react";

const App = () => {
let [name, setName] = useState("");

// Call this function every time our component renders or updates
useEffect(() => {
document.title = `Hello ${name}`;
});

return (
<>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)} />
</>
);
};
```

`useEffect` takes in a function, and runs that function every time the component
it's in needs to render or update. This works, but the function is called more
times than necessary - what if we had some other variable that wasn't `name` change?
Why do we need to update the webpage's title if `name` isn't changed?

`useEffect` actually takes in a *second* argument - the **dependency array**. This
is a list of variables that the function inside `useEffect` depends on - in other
words, the function should *only* run if any variable in the dependency array changes
since the last render. Because the function inside our `useEffect` call only depends
on `name`, we can put `[name]` as our second argument:

```jsx
const App = () => {
// ...
useEffect(() => {
document.title = `Hello, ${name}`;
}, [name]);
// ...
};
```

> When looking at React code in the future, you may encounter something like this, where
> an empty array is provided as the second argument to `useEffect`:
>
> ```jsx
> useEffect(() => { /* ... */ }, []);
> ```
>
> This is an example of what's known as a programming **idiom** - similar to an English
> idiom, where it's a piece of code that's *common* and understood to mean something
> that's *not immediately obvious* when looked at literally.
>
> In an empty array, nothing ever changes between renders because nothing is available
> inside the array to *be* changed. Thus, this "idiom" basically allows us to call a
> function *once*, at the *very first render of a component*.
3 changes: 3 additions & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
- [Character Card](04_react-intro/workshop_01/1-character-card.md)
- [Link's Cookbook](04_react-intro/workshop_01/2-links-cookbook.md)
- [Cooler Spotify](04_react-intro/workshop_01/3-cooler-spotify.md)
- [React Hooks](05_react-hooks/index.md)
- [`useState` and `useEffect`](05_react-hooks/state_and_effect/index.md)
- [Idle Game: `scrip.ts`](05_react-hooks/state_and_effect/idle_game.md)
- [Styling](06_styling/index.md)
- [DevWatch (TailwindCSS)](06_styling/devwatch.md)
- [Storage](07_storage/index.md)
Loading