State

Topics

  • state basics
  • input state
  • immutable state - arrays and objects in the state

State

components may have an internal state

state can be referenced in the template

the view will automatically update if state entries change

State hook

state hook in function components:

import { useState } from 'react';

function Slideshow() {
  const [imgId, setImgId] = useState(0);
  const [imgWidth, setImgWidth] = useState(300);
  const [imgHeight, setImgHeight] = useState(200);

  return ...
};

State hook

const [imgId, setImgId] = useState(0);

useState may be called (repeatedly) inside the component function to declare state entries

parameter: initial state

array of return values: current state, setter function

Using the minimal state

guideline: use the minimal state (i.e. no redundant data)

other data can be computed based on the state

Examples:

  • slideshow: store the image ID - the image URL can be derived from it
  • todos: store an array of todos with their status - the number of completed or incomplete items can be derived from them
  • text input field: store its content - the validity status can be derived from it

Exercise: Slideshow (part 1)

Re-implement the slideshow component we saw before; try not to look at the old code

Exercise: Slideshow (part 2)

add some features to the slideshow, e.g.:

  • button for random image
  • buttons that let the user change the image width (e.g. switching between 300x200 and 400x200)
  • small thumbnail for the next and previous images
  • ... (your own ideas)

avoid this for now:

  • having arrays / objects in the state
  • using inputs (text fields)
  • using timers (setTimeout, setInterval)
  • styling

Input state

Input state

"uncontrolled" input in React:

<input />

React does not know the input's content and cannot change it

Input state

connecting an input to the state:

const [inputText, setInputText] = useState('');
<input
  value={inputText}
  onChange={(event) => setInputText(event.target.value)}
/>

Input state

explanation:

value={inputText}

binds from the state to the input value

onChange={(event) => setInputText(event.target.value)}

updates the state whenever the input value changes

Input state

why event.target.value?

  • event.target represents the input element
  • event.target.value is the new value of the input element

Input state

Example: Input that also displays the number of characters:

function App() {
  const [text, setText] = useState('');

  return (
    <div>
      <input
        value={text}
        onChange={(event) => setText(event.target.value)}
      />
      <button onClick={() => setText('')}>clear</button>
      <p>This string has {text.length} characters.</p>
    </div>
  );
}

Input state

other options for working with input state:

  • handle multiple inputs via their name (see React documentation)
  • only read input contents on a form submit event (via FormData)
  • use an external library (e.g. react-hook-form, formik)

Input types

Input types

  • text
  • textarea
  • checkbox
  • numeric input
  • ...

Input types

text and text area:

<input
  value={title}
  onChange={(event) => setTitle(event.target.value)}
/>

Input types

checkbox:

<input
  type="checkbox"
  checked={accept}
  onChange={(event) => setAccept(event.target.checked)}
/>

Numeric input fields

Basic advice for numeric input fields:

store the content as a string (not as a number)

Reasoning: possible contents of a numeric input field (while the user is typing):

""
"-"
"-3"
"-3."
"-3.0"

Numeric input fields

example: numeric input with direct "result":

https://codesandbox.io/s/numeric-input-direct-results-5vde88

Other input types

see https://reactjs.org/docs/forms.html

Exercise: newsletter form

Create a component called NewsletterSignup with three state entries:

email, repeatEmail, acceptTerms

demo of finished version: https://codesandbox.io/s/newsletter-form-pvgs6l

note: enabling / disabling a button in JSX: <button disabled={...}>...</button>

Exercise: newsletter form

extra: add a dropdown to choose a language for the newsletter

Immutable state

Immutable state

Immutability: important concept in functional programing and with React

Data is not modified directly - instead, new data is derived from existing data (and may replace the existing data)

Immutable state

if there are arrays or objects in the state we could try and mutate them directly

don't do this - React will not notice the changes and may not rerender the view

state should be considered as immutabe (unchangeable)

Immutable state

When a state setter is called:

React will compare the old state reference and the new state reference

If the reference is the same, the component will not be rerendered

Immutable state

demo: https://codesandbox.io/s/immutable-state-demo-r2x1i

Data management without mutations

Data management without mutations

options for getting updated data from previous data:

  • mutate the original data
  • create new - derived - data from the original data

Data management without mutations

topics:

  • adding properties to an object
  • changing individual properties of an object
  • adding elements to an array
  • changing elements in an array
  • removing elements from an array

Data management without mutations

mechanisms:

  • spread syntax (...)
  • special array methods (.map, .filter)

Data management without mutations

spread syntax for arrays:

const numbers = [1, 2, 3];
const moreNumbers = [...numbers, 4, 5, 6];
// moreNumbers: [1, 2, 3, 4, 5, 6]

Data management without mutations

spread syntax for objects:

const person = {
  firstName: 'Joe',
  lastName: 'Doe',
  age: 31,
};
const newPerson = { ...person, email: 'j@d.com', age: 32 };
// {firstName: 'Joe', lastName: 'Doe', email: 'j@d.com', age: 32}

Data management without mutations

the .map method:

  • creates a new value for each entry in an array
  • uses a function to specify the "transformation" of each entry
  • returns a new array with those values
const myNumbers = [1, 2, 3, 4];

const tripledNumbers = myNumbers.map((n) => 3 * n);
// [3, 6, 9, 12]
const squareRoots = myNumbers.map(Math.sqrt);
// [1, 1.41, 1.73, 2]

Data management without mutations

the .filter method:

  • creates a new array where only specific entries are kept in
  • uses a function to specify the criterion
  • returns a new array
const myNumbers = [1, 2, 3, 4];

const evenNumbers = myNumbers.filter((n) => n % 2 === 0);
// [2, 4]

Data management without mutations

mechanisms:

  • adding properties to a an object: spread syntax
  • changing individual properties of an object: spread syntax
  • adding elements to an array: spread syntax
  • changing elements in an array: map
  • removing elements from an array: filter

Exercises

Working with todos

Task: Complete some functions that manage todos without mutating them

Codesandbox starting point

Exercises

exercise: create the following pure function that handles a todo item:

const todo1 = { id: 1, title: 'foo', completed: false };

function changeTitle(todo, newTitle) {
  // TODO: FINISH IMPLEMENTATION HERE
}

const todo2 = changeTitle(todo1, 'bar');
console.log(todo2);
// { id: 1, title: 'bar', completed: false}

Exercises

solution:

function changeTitle(todo, newTitle) {
  return { ...todo, title: newTitle };
}

Exercises

exercises: create pure functions that handle an array of todos

const todos = [
  { id: 1, title: 'foo', completed: false },
  { id: 5, title: 'bar', completed: true },
  { id: 7, title: 'baz', completed: false },
];

Exercises

Complete this code so it adds a new incomplete todo with a given title and a new id:

function addTodo(todos, newTitle) {
  let maxId = 0;
  for (let todo of todos) {
    maxId = Math.max(maxId, todo.id);
  }
  // TODO: FINISH IMPLEMENTATION HERE
}

// add a todo with title 'qux'
const todosA = addTodo(todos, 'qux');
console.log(todosA);

Exercises

possible solution:

function addTodo(todos, newTitle) {
  let maxId = 0;
  for (let todo of todos) {
    maxId = Math.max(maxId, todo.id);
  }
  return [
    ...todos,
    { id: maxId + 1, title: newTitle, completed: false },
  ];
}

Exercises

Complete this code so it removes a specific todo by its id:

function removeTodo(todos, id) {
  // TODO: FINISH IMPLEMENTATION HERE
}

const todosB = removeTodo(todos, 1);
console.log(todosB);

Exercises

possible solution:

function removeTodo(todos, id) {
  return todos.filter((todo) => todo.id !== id);
}

Exercises

Complete this code so it sets the completed status of a specific todo

function changeTodoCompleted(todos, id, completed) {
  // TODO: FINISH IMPLEMENTATION HERE
}

// change the completed status of todo 1 to true
const todosC = changeTodoCompleted(todos, 1, true);
console.log(todosC);

Exercises

possible solution:

function changeTodoCompleted(todos, id, completed) {
  return todos.map((todo) => {
    if todo.id === id {
      return {...todo, completed: completed}
    } else {
      return todo;
    }
  })
}

Exercises

newsletter signup: update the newsletter signup to use one state object instead of three separate state entries:

const [data, setData] = useState({
  email: '',
  repeatEmail: '',
  acceptTerms: false,
});

todos: write more pure functions that handle todos:

  • changeTodoTitle
  • toggleTodo (switch between completed and not completed)
  • updateTodo (change title and completed in one go)
  • removeAllCompoletedTodos
  • ...