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 Component render() method.
  • 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

# 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 from loops or nested functions
  • only call from React component

# 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

Why does that not work?

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

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 to extract these two elements into easier to use local variables.

 




 











import React, { 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

# Multiple state variables

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

import React, { 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

# 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

# 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 React, { 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

# 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" &mdash 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.

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'

function AppHeader () {
  return (
    <header className='App-header'>
      <img src={logo} className='App-logo' alt='React logo' />
      <h1>React Demo &mdash; GitHub Profiles</h1>
    </header>
  )
}

export default AppHeader
# 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;
}

# 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
# 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%);
}

# 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('rlmckenney')

  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
# 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;
}

# 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>

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

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

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

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

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)
}

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.

Last Updated: : 10/3/2020, 4:04:14 PM