4.2 React Component State

# What is State?

Special variables for which changes in data should trigger the re-rendering of some part of the UI.

  • React Props are for passing data from one Component to another.
  • React State is intended to belong to a Component.
  • State has an initial value and then can be updated
  • State values can be displayed inside your Class Component render() method or inside the return object from your function Component.
  • When state is updated the Component will be re-rendered.

# Rules of State

State variables should not be updated by direct assignment. You must use the provided setState() method.

State variables, and the updating of them, should be encapsulated in the component where they are defined. However, the value of a state variable may be passed to another component via props.

# Component Lifecycle

React component lifecycle methods diagram (opens new window)

# LocalStorage with State

Sometimes you want to hold information in LocalStorage as well as in state. Doing this in React means that you have to put the data into LocalStorage and retrieve it at specific times. Remember that putting information into state will trigger a re-render of your component(s).

Typically we will use a user-driven event like click to call a function and update the value of state. This would be the same time to update LocalStorage.

Retrieving the value(s) from LocalStorage should be done when the page first loads. State will have a default value when the page loads. This default value will be used to render your component(s). Often this will be an empty array or empty object, so nothing actually gets rendered.

Then we will fetch the data from LocalStorage and update the value of our state which will trigger a re-render.

This video explains the process using a class-based Component and the setState method.

We can achieve the same effect with a click handler function plus the useState function, as described below.

# Synthetic Events

As you have been told numerous times by now, the JSX we are writing in our components is NOT HTML. It is a JavaScript object that reactDOM will convert into DOM elements.

This means when we add events to what will become the DOM we need to do things a bit differently.

React has a collection of Synthetic Events which are basically properties that you will be adding into your JSX elements. Their syntax makes them look like the old HTML style of inline event handlers.

<button onClick={someFunc}>click me</button>;
{
  /* OR */
}
<button onClick={(ev) => someFunc(ev)}>click me</button>;
1
2
3
4
5

This example shows a synthetic click event handler being added to our JSX button object. The code inside the parentheses will run when the user clicks the DOM version of this button object.

There are synthetic events that align with most events that we have in the browser. Read more about these Synthetic Events (opens new window)

# Hooks

Introduced in React v16.8, the hooks API provides a more declarative syntax for integrating component methods with React's lifecycle methods and rendering events.

# Rules of Hooks

  • top level - not inside loops or nested functions
  • only call from inside React Components

# useState()

The best way to explain it is to use it. Create a new component called ClickCounter. It will have a variable called count to keep track of how many times the user clicks a button.

import React from 'react';

function ClickCounter() {
  let count = 0;
  const setCount = (newValue) => (count = newValue);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

export default ClickCounter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Why does that not work?

React needs to know that it should watch the count variable and re-render when it changes.

Basically, what we did was this:

let num = 0;
let p = document.createElement('p');
//put num into the new paragraph
p.textContent = num;
p.addEventListener('click', () => {
  setCount(num + 1);
});
document.body.append(p);

function setCount(newNum) {
  //update the value of num
  num = newNum;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

There is nothing to connect the variable num to the paragraph. There is nothing telling the DOM that num was updated.

To fix it, you need to declare count as a new state variable using the useState() hook. Don't forget to import useState from React. The useState() function returns an array. The first element of the array holds the new state variable and the second element holds the "setter" function needed to assign a new value to the state variable.

You will want to use array assignment destructuring (opens new window) to extract these two elements into easier to use local variables.

 




 











import { useState } from 'react';

function ClickCounter() {
  // Declare a new state variable, which we'll call "count".
  // The initial value of count will be zero.
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

export default ClickCounter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Multiple state variables

Your component can have as many state variables managed with useState() as you need. Consider this example.

import { useState } from 'react';

function ClickCounter() {
  const [leftClicks, setLeftClicks] = useState(0);
  const [rightClicks, setRightClicks] = useState(0);

  return (
    <div>
      <p>You clicked a total of {leftClicks + rightClicks} times</p>
      <button onClick={() => setLeftClicks(leftClicks + 1)}>
        {leftClicks}
      </button>
      <button onClick={() => setRightClicks(rightClicks + 1)}>
        {rightClicks}
      </button>
    </div>
  );
}
export default ClickCounter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Dedicated event handler functions

As the logic for your event handlers gets more complex, it is a good idea to extract them to a declared function rather than defining them inline with the JSX.

import React, { useState } from 'react';

function ClickCounter() {
  const [leftClicks, setLeftClicks] = useState(0);
  const [rightClicks, setRightClicks] = useState(0);

  function handleLeftClick() {
    const increment = 1;
    setLeftClicks(leftClicks + increment);
  }

  function handleRightClick() {
    const increment = 1;
    setRightClicks(rightClicks + increment);
  }

  return (
    <div>
      <p>You clicked a total of {leftClicks + rightClicks} times</p>
      <button onClick={handleLeftClick}>{leftClicks}</button>
      <button onClick={handleRightClick}>{rightClicks}</button>
    </div>
  );
}

export default ClickCounter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# Batched Updates

React does not necessarily update your state variables immediately when you call the corresponding "setState" function. These updates may be done in batches to reduce the number of times components are re-rendered.

For this reason, if your "setState" operation depends on the previous value of state, then you should use the optional function syntax.









 




 













import { useState } from 'react';

function ClickCounter() {
  const [leftClicks, setLeftClicks] = useState(0);
  const [rightClicks, setRightClicks] = useState(0);

  function handleLeftClick() {
    const increment = 1;
    setLeftClicks((prevState) => prevState + increment);
  }

  function handleRightClick() {
    const increment = 1;
    setRightClicks((prevState) => prevState + increment);
  }

  return (
    <div>
      <p>You clicked a total of {leftClicks + rightClicks} times</p>
      <button onClick={handleLeftClick}>{leftClicks}</button>
      <button onClick={handleRightClick}>{rightClicks}</button>
    </div>
  );
}

export default ClickCounter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# Practice Exercise 10 minutes

Create a counter component with an increment button and a decrement button. Display the current value of the counter between the buttons.

The counter value should increase or decrease depending on which button was clicked.

Bonus: accept the increment and decrement values as props.

Solution

after class

# useEffect()

In general your React function components should mostly be "pure functions" — they should be deterministic without side effects.

Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects.

Deterministic means that the same input should always result in the same output.

In real applications you will most likely need to fetch data from some API or subscribe to a websocket. These asynchronous activities should be encapsulated inside a useEffect() hook.

# Github Profile Example

# Header Component

# AppHeader.js
import React from 'react';
import logo from './logo.svg';
import './AppHeader.css';

export default function AppHeader() {
  return (
    <header className="App-header">
      <img src={logo} className="App-logo" alt="React logo" />
      <h1>React Demo &mdash; GitHub Profiles</h1>
    </header>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
# AppHeader.css
.App-header {
  height: 80px;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  background-color: hsl(220, 15%, 18%);
  color: hsl(220, 0%, 98%);
}

.App-logo {
  height: 60px;
  padding-right: 0.5rem;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# ProfileCard Component

# ProfileCard.js
import React from 'react';
import './ProfileCard.css';

function ProfileCard(props) {
  return (
    <div className="ProfileCard">
      <div className="profile-avatar">
        <img src={props.profile.avatar_url} />
      </div>
      <div className="card-body">
        <p className="profile-name">
          {props.profile.name}
          <br />
          <span className="profile-login">{props.profile.login}</span>
        </p>
        <p className="profile-bio">{props.profile.bio}</p>
      </div>
    </div>
  );
}

export default ProfileCard;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ProfileCard.css
.ProfileCard {
  display: flex;
  margin: 0 0 1.25rem 0;
  height: 180px;
  /* border: 2px solid hsl(220, 15%, 46%); */
  border-radius: 0.5rem;
  overflow: hidden;
  box-shadow: 0 12px 24px 0 hsl(220, 15%, 88%), 0 3px 9px 0 hsl(220, 15%, 94%);
}

.profile-avatar {
  height: 180px;
  width: 180px;
  min-width: 180px;
  object-fit: cover;
}

.profile-avatar img {
  height: 100%;
  width: 100%;
}

.card-body {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.profile-name {
  font-size: 1.5rem;
  font-weight: 500;
  line-height: 1;
  margin: 0;
}
.profile-login {
  font-size: 0.85rem;
  font-weight: 400;
  color: hsl(220, 15%, 46%);
}
.profile-bio {
  color: hsl(220, 0%, 23%);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# App Component

# App.js
import React, { useState, useEffect } from 'react';
import AppHeader from './AppHeader';
import ProfileCard from './ProfileCard';
import './App.css';

function App() {
  const [profile, setProfile] = useState({});
  const [username, setUsername] = useState('prof3ssorSt3v3');

  useEffect(() => {
    getGitHubProfile(username)
      .then((githubProfile) => setProfile(githubProfile))
      .catch(console.log);
  }, [username]);

  async function getGitHubProfile(username) {
    const response = await fetch(`https://api.github.com/users/${username}`);
    if (!response.ok) throw new Error(response.statusText);

    return response.json();
  }

  return (
    <div className="App">
      <AppHeader />
      <main className="ProfileList" style={{ padding: '1rem' }}>
        <ProfileCard key={profile.id} profile={profile} />
      </main>
    </div>
  );
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# App.css
.App form {
  margin: 1rem;
}

.App input {
  font-size: 1.125rem;
  padding: 0.25rem 0.5rem;
}
.App button {
  font-size: 1.125rem;
  padding: 0.25rem 0.5rem;
  margin-left: 0.5rem;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Respond to user input

OK. That worked fine. We loaded the user profile from GitHub and updated the profile state variable, which triggered the <ProfileCard> component to re-render.

Now, lets go the next step and have the user be able to type in a GitHub username to trigger a new fetch call and display the new profile.

# Add a form

Between the header and the main elements add a form with an input and a button.

<form onSubmit={handleSubmit}>
  <input type="text" value={searchTerm} onChange={handleInput} />
  <button type="submit">Fetch</button>
</form>
1
2
3
4

We will make the input field a controlled component (opens new window). So, we will also need another state variable to track the contents of the input.

const [searchTerm, setSearchTerm] = useState('');
1

And, we will need an event handler for the input element's change event.

const handleInput = (e) => setSearchTerm(e.target.value);
1

Finally, we need an event handler for when the form is submitted. When the user is done typing and either presses enter to clicks the Fetch button, the form element's submit event will fire.

All we need to do now is update the username state variable to be the same as the searchTerm.

const handleSubmit = (event) => {
  event.preventDefault();
  setUsername(searchTerm);
};
1
2
3
4

Because username is included in the array of watched dependencies (second argument) of the useEffect() method, changing the value of username will cause useEffect to run the callback function and fetch the requested user profile, which will in turn update the profile variable which will cause the <ProfileCard> to re-render and display the results.

# Reference Video

Last Updated: : 10/8/2021, 3:27:56 PM