Overview

A hodge-podge of material for getting up to speed on the technologies used to build the DT Heart & Soul app: Javascript ES6, React, React Native, and Redux, to name a few.

Some sources for this material:

  • https://www.taniarascia.com/es6-syntax-and-feature-overview/
  • https://www.javatpoint.com/es6
  • https://www.w3schools.com/Js

Overview

Some sources for this material:

  • https://www.taniarascia.com/es6-syntax-and-feature-overview/
  • https://www.javatpoint.com/es6
  • https://www.w3schools.com/Js

ES6 or ECMAScript 6 - scripting language spec, standardized by ECMAScript International.

This spec governs some languages such as JavaScript, ActionScript, and Jscript.

ECMAScript is generally used for client-side scripting; also used for writing server applications and services, using Node.js.

ES6 allows writing more readable code. With ES6 features, we write less and do more.

This tutorial introduces you to the implementation of ES6 in JavaScript.

Some History

8th Edition - ECMAScript 2017.

The 8th edition was officially known as ECMAScript 2017, which was finalized in June 2017. It includes the async/await constructions which work using promises (In CS future, promise, deferred, and delay refers to the constructs which are used to synchronize the execution of the program in some concurrent programming languages) and generators.

9th Edition - ECMAScript 2018.

The 9th edition was officially known as ECMAScript 2018, which was finalized in June 2018. It includes the new features like the rest/spread operators for variables (three dots: …identifier), asynchronous iteration, etc.

Functions

Declaring functions

The function keyword can be omitted when assigning methods on an object.

let obj = {
  a(c, d) {},
  b(e, f) {},
}

obj.a() // call method a

Functions can be initialized with default parameters, which will be used only if an argument is not invoked through the function.

let func = (a, b = 2) => {
  return a + b
}

func(10) // returns 12
func(10, 5) // returns 15

Spread syntax can be used for function arguments.

let arr1 = [1, 2, 3]
let func = (a, b, c) => a + b + c

console.log(func(...arr1)) // 6

Classes

ES6 introducess the class syntax on top of the prototype-based constructor function.

class Func {
  constructor(a, b) {
    this.a = a
    this.b = b
  }

  getSum() {
    return this.a + this.b
  }
}

let x = new Func(3, 4)

x.getSum() // returns 7

Inheritance

The extends keyword creates a subclass.

class Inheritance extends Func {
  constructor(a, b, c) {
    super(a, b)  // Calls Func constructor

    this.c = c
  }

  getProduct() {
    return this.a * this.b * this.c
  }
}

let y = new Inheritance(3, 4, 5)

y.getProduct() // 60

Spread operator

var variablename1 = [...value];

Spread syntax can be used to expand an array.

let arr1 = [1, 2, 3]
let arr2 = ['a', 'b', 'c']
let arr3 = [...arr1, ...arr2]

console.log(arr3) // [1, 2, 3, "a", "b", "c"]

Rest parameter

Similar to the "splat" in Ruby

https://www.javatpoint.com/es6-rest-parameter

A short notation for assigning properties to variables of the same name...

From https://www.taniarascia.com/es6-syntax-and-feature-overview/

let obj = {
  a,
  b,
}

Results in this structure:

{
  a: a,
  b: b
}

Variables

From W3Schools.

Declare with var, let, and const (or nothing)

  • The var keyword was used in all JavaScript code from 1995 to 2015.
  • The let and const keywords were added to JavaScript in 2015.
  • You cannot re-declare a variable declared with let or const.
  • You cannot re-assign a variable declared with const.
  • Variables defined with let must be declared before use.
  • Variables defined with let have Block Scope.
let person = "John Doe", carName = "Volvo", price = 200;
let carName;  // Will have undefined as value
  • $ is a valid character in a variable name. Programmers often use it as an alias for the main function in a JavaScript library.
  • "" is a valid character in a variable name. A convention among programmers is to prefix private/hidden variables with "".

Block Scope and variables

  • Before ES6 (2015), JavaScript had only Global Scope and Function Scope.
  • let and const provide Block Scope in JavaScript.

Variables declared inside a { } block cannot be accessed from outside the block: { let x = 2; } // x can NOT be used here //

Language Features

Export - Exposing to other files

Modules can be created to export and import code between files.

From https://www.taniarascia.com/es6-syntax-and-feature-overview...

index.html

<script src="export.js"></script>
<script type="module" src="import.js"></script>

export.js

let func = (a) => a + a
let obj = {}
let x = 0

export {func, obj, x}

import.js

import {func, obj, x} from './export.js'

console.log(func(3), obj, x)

From Exercism.org

To make functions, constants, or variables available in other files, they need to be exported using the export keyword. Another file may then import these using the import keyword. This is also known as the module system.

A great example is how all the tests work. Each exercise has at least one file, for example lasagna.js, which contains the implementation. Additionally, there is at least one other file, for example lasagna.spec.js, that contains the tests.

This file imports the public (i.e. exported) entities to test the implementation.

Using the default export

From Developer.Mozilla.org.

If we want to export a single value or to have a fallback value for your module, you could use a default export:

// module "my-module.js"

export default function cube(x) {
  return x * x * x;
}

Then, in another script, it is straightforward to import the default export:

import cube from './my-module.js';
console.log(cube(3)); // 27

Import - static or dynamic

static import - a declaration

The static import declaration is used to import read-only live bindings which are exported by another module. The imported bindings are called live bindings because they are updated by the module that exported the binding, but cannot be modified by the importing module.

import declarations can only be present in modules, and only at the top-level (i.e. not inside blocks, functions, etc.). If an import declaration is encountered in non-module contexts (for example, <script> tags without type="module", eval, new Function, which all have "script" or "function body" as parsing goals), a SyntaxError is thrown. To load modules in non-module contexts, use the dynamic import syntax instead.

static import syntax

import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { default as alias } from "module-name";
import { export1, export2 } from "module-name";
import { export1, export2 as alias2, /* … */ } from "module-name";
import { "string name" as alias } from "module-name";
import defaultExport, { export1, /* … */ } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";

dynamic import() - a function call

Other Language Features

Iteration

A more concise syntax has been introduced for iteration through arrays and other iterable objects.

var arr = ['a', 'b', 'c']
for (let i of arr) {
  console.log(i)
}

Strict Mode

Add this to the very top of a function or a script:

"use strict";

Strict mode makes several changes to normal JavaScript semantics:

  • Prohibits some syntax likely to be defined within future versions of ECMAScript.
  • Removes some of JavaScript's silent errors by changing them to throw errors.
  • Modifies code that makes it difficult for JavaScript engines to optimize, with the result that strict mode sometimes runs faster than non-strict.
  • In React, it's not necessary to declare strict mode.

ES6 and Hoisting

Hoisting is the default behavior of moving all declarations (but not initializations) to the top of the scope before executing the code. It applies to functions and variables. It allows JavaScript to use the component before its declaration.

  • Hoisting does not apply to scripts that run in strict mode.
  • JavaScript only hoists the variable declaration, not variable initialization.
  • Variables declared with let cannot be hoisted.

Destructuring

Destructuring is an efficient means of extracting multiple fragments from data stored in arrays or objects.

The syntax of destructuring supports variable declaration and assignment.

Array destructuring

When destructuring an array, values are extracted based on position (or index) in an assignment:

> const a = ['a','b','c','d','e'];
> let [f,g,h] = a
> f
'a'
> h
'c'
> let therest
> [f,g,...therest] = a
> therest
[ 'c', 'd', 'e' ]

For more examples, see MDN web docs, destructuring assignment.

Object destructuring

In object destructuring, values are extracted based on the keys.

Syntax shorthand: Use curly brackets on left-hand side to assign an object's properties to variables of the same name as the corresponding key.

> const num = {x: 100, y: 200, z: 300}
// Must use key names on LHS... {a,b} will not work here!
> const {x, z} = num
> z
300

This is an example of a "binding" pattern, in which the pattern starts with a declaration keyword (let, var, or const). Here is another:

const obj = { a: 1, b: { c: 2 } };
const {
  a,
  b: { c: d },
} = obj;
// Two variables are bound: `a` and `d`

There are also "assignment" patterns, in which the pattern does NOT start with a declaration keyword. For instance:

const numbers = [];
const obj = { a: 1, b: 2 };
({ a: numbers[0], b: numbers[1] } = obj);
// The properties `a` and `b` are assigned to properties of `numbers`

For more examples, see MDN web docs, destructuring assignment.

My encounter with inline destructuring of component props passed to a function

I ran across the above syntax shorthand notation when I neglected curlies around the name parameter in a component function definition:

export default function ProductCategoryRow(name) {  // This is wrong!
  ...
  return <div>
    {name}
  </div>
}

Webpack happily compiled it, but a runtime error appeared on the console:

Uncaught Error: Objects are not valid as a React child
(found: object with keys {name}). If you meant to render
a collection of children, use an array instead

I was calling it from another component thusly:

<ProductCategoryRow name="Fruits" />

What happened? The name="Fruits" syntax passed a {name: "Fruits"} prop object. So without curlies around the function definition, name was assigned {name: "Fruits"}, and not "Fruits" as I intended. Subsequently, name in the function body returned an object, not a string. Indeed, it returned an "object with keys {name}", exactly as the error indicated. React can render a string like Fruits, but it has no way to render an object.

Were we to stick with name in the function body as the object {name: "Fruits"}, we could de-reference it, as {name.name}, which is valid but odd. Or, we could call our param props instead of name, and say {props.name} -- more readable, but still wordy.

So, since ES6 supports destructuring (and unpacking) inline in the parameter list, by means of the {} syntax in that context, I can pick out just the name property from the passed object, using the {name} shorthand -- way more elegant, assuming I speak ES6 destructuring.

export default function ProductCategoryRow( {name} ) {  // Nice!
  ...

Now name in the function body is the string "Fruits", because inline-destructuring {...} syntax extracted it from the {name: "Fruits"} props object passed from the component.

These two links help with understanding inline destructuring (and unpacking) of function parameters:

https://bobbyhadz.com/blog/react-objects-are-not-valid-as-react-child https://stackoverflow.com/questions/58574905/destructured-object-as-function-parameter

'Destructuring' with import statements

What do these curlies mean?

import React, { useState } from 'react';

The curly braces {} are used to import named bindings from a module, using syntax that looks and acts a lot like destructuring (but isn't actually).

There is a good answer here (even though they mistakenly call it 'destructuring'): https://stackoverflow.com/questions/51701042/when-do-we-use-in-javascript-imports/51701099#51701099.

Promises/Callbacks

Promises represent the completion of an asynchronous function. They can be used as an alternative to chaining functions.

let doSecond = () => {
  console.log('Do second.')
}

let doFirst = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('Do first.')

    resolve()
  }, 500)
})

doFirst.then(doSecond)

Example using promises

function makeRequest(method, url) {
  return new Promise((resolve, reject) => {
    let request = new XMLHttpRequest()

    request.open(method, url)
    request.onload = resolve
    request.onerror = reject
    request.send()
  })
}

makeRequest('GET', 'https://url.json')
  .then((event) => {
    console.log(event.target.response)
  })
  .catch((err) => {
    throw new Error(err)
  })

Async and Await

From https://www.w3schools.com/Js/js_async.asp

"async and await make promises easier to write"

async makes a function return a Promise

async function myFunction() {
  return "Hello";
}

...is the same as...

function myFunction() {
  return Promise.resolve("Hello");
}

await makes a function wait for a Promise

await can only be used inside an async function.

Example

The resolve and reject args to the new Promise() are pre-defined by JavaScript.

Often we will not need reject.

async function myDisplay() {
  let myPromise = new Promise(function(resolve, reject) {
    resolve("I love You !!");
  });
  document.getElementById("demo").innerHTML = await myPromise;
}

myDisplay();

Generator Functions and Yield

From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield.

The function* declaration defines a generator function, which returns a Generator object, which conforms to both the iterable protocol and the iterator protocol.

A generator function is a normal function, except that it uses the yield keyword rather than return. yield pauses the function and returns a value to the caller. When called again, the generator function resumes execution from where it was last paused.

function* foo(index) {
  yield index;
  index++;
  yield index;
  index++;
}

const iterator = foo(0);

console.log(iterator.next().value);
// expected output: 0

console.log(iterator.next().value);
// expected output: 1

console.log(iterator.next().value);
// expected output: undefined

console.log(iterator.next().value);
// expected output: undefined

React

From https://beta.reactjs.org/learn

React components are JavaScript functions that return markup:

function MyButton() {
  return (
    <button>I'm a button</button>
  );
}

// The export default keywords specify the main component in the file.
export default function MyApp() {
  return (
    <div>
      <h1>Welcome to my app</h1>
      <MyButton />
    </div>
  );
}

React component names must always start with a capital letter, while HTML tags must be lowercase.

JSX

NOTE, the above is JSX--a mix of Javascript and Markup.

Your component also can’t return multiple JSX tags. You have to wrap them into a shared parent, like a <div>...</div> or an empty <>...</> wrapper:

function AboutPage() {
  return (
    <>
      <h1>About</h1>
      <p>Hello there.<br />How do you do?</p>
    </>
  );
}

In React, you specify a CSS class with className. It works the same way as the HTML class attribute:

<img className="avatar" />

JSX lets you put markup into JavaScript. Curly braces let you “escape back” into JavaScript so that you can embed some variable from your code and display it to the user. For example, this will display user.name:

return (
  <h1>
    {user.name}
    <img
      className="avatar"
      src={user.imageUrl}
      style={{
        width: user.imageSize,
        height: user.imageSize
      }}
    />
  </h1>
);

NOTE the above: where Javascript supplies a JSX attribute value, quotes are not used.

NOTE also, style={{}} is not a special syntax, but a regular {} object inside the style={ } JSX curly braces.

The ternary operator works inside JSX (unlike if)

<div>
  {isLoggedIn ? (
    <AdminPanel />
  ) : (
    <LoginForm />
  )}
</div>

React map() and keys

Inside a component, use map() to transform a JS array into an array of <li> items:

const listItems = products.map(product =>
  <li key={product.id}>
    {product.title}
  </li>
);

return (
  <ul>{listItems}</ul>
);

Notice how <li> has a key attribute. Each item in a list needs a string or number (e.g. a DB id) that is unique w.r.t. its siblings. React relies on those keys to track later inserts, deletes , or reorders of the items.

Event handlers

Respond to events by declaring event handler functions inside components:

function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

Notice how onClick={handleClick} has no parentheses at the end. This is because we do not want to call the event handler function yet, but only pass it down (it's a "callback"). React will call your event handler when the user clicks the button.

How Components share data and update together

To make both MyButton components display the same count and update together, you need to move the state from the individual buttons “upwards” to the closest component containing all of them.

Component Lifecycle

  1. Mount - create component
  • Before: ComponentWillMount()
  • After: ComponentDidMount()
  1. Update - change state
  • TODO from here...
  1. Unmount - destroy component

Lifecycle Methods

From codecademy.com, lifecycle-methods

In React, lifecycle methods are unique event listeners that listen for changes during certain points a component‘s lifecycle. A component’s lifecycle usually runs in this order:

  • Rendering/mounting a component to the DOM for the first time.
  • Updating an existing component.
  • Catching any errors associated with a component render-gone-wrong.
  • Unmounting a component and removing it from the DOM.

Lifecycle methods were originally exclusive to class components. But thanks to React hooks, functional components can also 'hook' into the component lifecycle.

With React, you don’t directly manipulate the DOM. For example, you won’t write commands like “disable the button”, “enable the button”, “show the success message”, etc. Neither will you write myDiv.style.display = "block".

Instead, you will describe the UI you want to see for the different visual states of your component (“initial state”, “typing state”, “success state”), and then trigger the state changes in response to user input. This is similar to how designers think about UI.

React takes responsibility for efficiently updating the DOM based on the state changes.

Setting and Preserving State

From beta.reactjs.org, preserving and resetting state

For each component, React associates its state with that component's location in the UI tree.

React preserves a component’s state for as long as that same component is being rendered at the same position in the UI tree.

Same component at the same position preserves state

When a component gets removed, or a different component gets rendered at the same position, React destroys its state.

...
// The Counter's state is preserved, whether fancy or not.
// In React's UI tree, it sits just under <div> in both cases
return (
  <div>
    {isFancy ? (
      <Counter isFancy={true} />
    ) : (
      <Counter isFancy={false} />
    )}
  </div>
)
...
...
// The Counter's state is NOT preserved!
// 2nd instance is at different place in the UI tree
return (
  <div>
    {isFancy ? (
      <Counter isFancy={true} />
    ) : (
      <div>
        <Counter isFancy={false} />
      </div>
    )}
  </div>
)
...

One can think of them as having the same “address”, e.g., "the first child of the first child of the root". This is how React matches them up between the previous and next renders, regardless of how you structure your logic. React doesn’t know where you place the conditions in your function. All it “sees” is the tree you return.

Also, rendering a component under a different UI element--even if it's in the same relative position under that different UI element--resets the state of the component's entire subtree:

...
// The Counter's state is destroyed if isFancy changes.
// In React's UI tree, sitting just under <div> is a different
// position than sitting just under <section>
return (
    {isFancy ? (
      <div>
        <Counter />
      </div>
    ) : (
      <section>
        <Counter />
      </section>
    )}
)
...

This is why you should not nest component function definitions:

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  // Every time MyComponent renders, a *different*
  // MyTextField() function is being created, which
  // means its state is lost.

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

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}

What if you want state to change on re-render?

Two approaches:

  1. Put component in different position
  2. Give each component an explicit identity with key

Putting same component in different position resets state

This example illustrates a very tricky behavior. React creates positions for TWO <Counter /> components even though one position is empty due to the isFancy condition. Therefore, state is destroyed when isFancy changes and the component is removed from one position and added to the other--but the position is there regardless.

export default function Example() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      { isFancy && <Counter isFancy={isFancy} /> }
      {!isFancy && <Counter isFancy={isFancy} /> }
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => { setIsFancy(e.target.checked) } }
        />
        Use fancy styling
      </label>
    </div>
  );
}

Giving same component different identity with key attribute resets state

// Resets state
...
return (
  <div>
    {isFancy ?
      <Counter isFancy={isFancy} key="A" /> :
      <Counter isFancy={isFancy} key="B" />
    }
  </div>
)
...

(NOTE: A ternary operator can live inside a JSX expression, but an if statement cannot; the if must go outside the return().)

// Does NOT reset state.  (The `id` prop does not differentiate components)
...
return (
  <div>
    {isFancy ?
      <Counter isFancy={isFancy} id="a"/> :
      <Counter isFancy={isFancy} id="b"/>
    }
  </div>
)
...

Specifying a key tells React to use the key itself as part of the position, instead of their order within the parent.

Keys are not globally unique. They only specify the position within the parent.

Also note that the above examples are working with drawing one component versus another; if multiple components are being drawn, then a key field will serve to assign state to the correct component...see next example.

For multiple instances of same component, the key field helps React assign correct state across re-renders

export default function App() {
  const [reverse, setReverse] = useState(false);

  // Cool, assigning a component to a variable!
  let checkbox = (
    <label>
      <input type="checkbox" checked={reverse}
        onChange={e => setReverse(e.target.checked)} />
      Reverse order
    </label>
  );

  if (reverse) {
    return (
      <>
        <Field label="Last name"  key="L" />
        <Field label="First name" key="F" />
        {checkbox}
      </>
    );
  } else {
    return (
      <>
        <Field label="First name" key="F" />
        <Field label="Last name"  key="L" />
        {checkbox}
      </>
    );
  }
}

function Field({ label }) {
  const [text, setText] = useState('');
  return (
    <div>
      {label}:
      <input type="text" value={text} placeholder={label}
        onChange={e => setText(e.target.value)} />
    </div>
  );
}

Summary

  • React keeps state for as long as the same component is rendered at the same position.
  • State is not kept in JSX tags. It’s associated with the position of that JSX in the tree.
  • A key lets you specify a named position instead of relying on order.
  • You can force a subtree to reset its state by giving it a different key.
  • Don’t nest component definitions, or you’ll reset state by accident.

Excerpted from testdriven.io, React Hooks Primer - February 27th, 2019

React hooks help ease the aggravation of using complex higher order components to re-use stateful logic and relying on lifecycle methods that trigger unrelated logic.

Outline:

  • What are hooks?
  • Why were they implemented in React?
  • How are hooks used?
  • What are the rules for using hooks?
  • (Not covering custom hooks here. See above link for that)

What are hooks?

Hooks give functions class-like abilities, including state and side-effects, that let you “hook into” a component’s render cycle.

The useState() hook adds state to a component, letting you share stateful logic between components, thus simplifying component logic and letting you avoid writing classes.

Why were hooks implemented in React?

Hooks simplify component logic.

When the inevitable exponential growth of logic appears in your application, simple components become a dizzying abyss of stateful logic and side effects. Lifecycle methods become cluttered with unrelated methods. A component's responsibilities grow and become inseparable.

Skip [writing/using] classes

Classes are a huge part of React's architecture. Classes have many benefits, but form a barrier to entry for beginners. With classes, you also have to remember to bind "this" to event handlers, and so code can become lengthy and a bit redundant.

How are hooks used?

Add state variables at the top of the component function that "owns" the state, and specify the initial state of your application.

The useState() hook method

useState(<initialState>) gives you a state variable and a function to modify that variable.

// Bring in the useState and useEffect 'hooks' ...

import { useState, useEffect } from 'react';

The way to use the state hook is to destructure it and set the initial value. The first parameter stores the state, the second is a function to update the state.

For example:

const [weight, setWeight] = useState(150);

onClick={() => setWeight(weight + 15)}
  • weight is the state
  • setWeight is a method used to update the state
  • useState(150) is the method used to set the initial value (any primitive type)
  • It's worth noting that you can destructure the state hook many times in a single component:
const [age, setAge] = useState(42);
const [month, setMonth] = useState('February');
const [todos, setTodos] = useState([{ text: 'Eat pie' }]);

So, the component might look something like:

import React, { useState } from 'react';

export default function App() {
  const [weight, setWeight] = useState(150);
  const [age] = useState(42);
  const [month] = useState('February');
  const [todos] = useState([{ text: 'Eat pie' }]);

  return (
    <div className="App">
      <p>Current Weight: {weight}</p>
      <p>Age: {age}</p>
      <p>Month: {month}</p>
      <button onClick={() => setWeight(weight + 15)}>
        {todos[0].text}
      </button>
    </div>
  );
}

The useEffect() hook method

Without useEffect(), state only flows one way: from the top down. But to change the state according to user input, you'll need to support data flowing the other way: the form components deep in the hierarchy need to update the state in the topmost common component.

Use the useEffect() hook as you would common lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount: Bind a function to the state variable that modifies pieces of the display that depend on that state variable.

// check out the variable count in the array at the end...
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [ count ]);

Whenever the component updates, the function bound by useEffect() will be called after render.

Rules to using React hooks

The rules for React hooks may seem arbitrary at first glance, but once you understand the basics of how React hooks are initiated, the rules make more sense.

(Plus, React has a linter to keep you from breaking the rules).

(1) Hooks must be called in the same order, at the top level

Hooks must be called at the top level of a React function i.e. components or custom hooks) and not from a regular JavaScript function, so they render in the same order every time.

Why? Hooks create an array of hook calls to keep order. This order helps React tell the difference, for example, between multiple useState and useEffect method calls in a single component or across an application.

  1. On first render, the values -- 42, February, [{ text: 'Eat pie'}] -- are all pushed into a state array.
  2. When the component re-renders (NOTE: a re-render is NOT a page-refresh!), the useState method arguments are ignored.
  3. The values for age, month, and to-dos are retrieved from the component's state, which is the aforementioned state array.

(2) Hooks cannot be called within conditional statements or loops

Because of the way that hooks (that is, useState() and useEffect()) are initiated, they must not be used within conditional statements, loops, or nested functions. For hooks, if the order of the initializations changes during a re-render, there is a good chance your application will not function properly. You can still use conditional statements and loops in your components, but don't put hooks inside conditionals or loops.

For example:

const [DNAMatch, setDNAMatch] = useState(false)

if (name) {
  setDNAMatch(true)
  const [name, setName] = useState(name)

  // DON'T DO THIS!!  Don't call useEffect() inside a conditional
  useEffect(function persistFamily() {
    localStorage.setItem('dad', name);
  }, []);
}
const [DNAMatch, setDNAMatch] = useState(false)
const [name, setName] = useState(null)

useEffect(() => {
  // DO THIS!!  The conditional is inside useEffect()
  if (name) {
    setDNAMatch(true)
    setName(name)
    localStorage.setItem('dad', name);
  }
}, []);

(3) Hooks cannot be used in class components

Hooks cannot be used in class components (which makes sense, because hooks are for enabling class-like behavior without needing to use classes). Hooks must be initialized in either a functional component or in a custom hook function. Custom hook functions can only be called within a functional component and must follow the same rules as non-custom hooks.

Consolidate State with a Reducer

From beta.reactjs.org, extracting state logic into a reducer.

As a component grows in complexity, it gets harder to see how its state gets updated.

A React reducer is a single function outside your component that consolidates all the state update logic into one place.

The reducer function takes state as an argument; hence, you can declare it outside of your component, and even move it to a different file and import it, which simplifies your component and makes your code easier to read.

Reducers allow the component logic to separate concerns:

  1. The event handlers only specify what happened by dispatching actions, and
  2. the reducer function determines how the state updates in response to those actions.

Why is it called a "reducer"?

The function you pass to reduce() is called a “reducer”, because it takes the result so far and the current item, and then returns the next result. This behavior is reminiscent of the reduce() operation on arrays, which takes an array and "reduces" many values down to one:

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
  //  The reducer accumulates its work in the "result" variable:
  (result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

React reducers embody the same idea: they take the state so far (analogous to the result variable above) and the action (analogous to the current iteration's number), and return the next state.

In this way, reducers transform sequential actions into sequential state changes.

You can migrate from useState to useReducer in three steps:

  1. Move from setting state to dispatching actions.
  2. Write a reducer function.
  3. Wire up the reducer to your component.

Event handers call dispatch(action)

Managing state with reducers differs from directly setting state: Instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers. This is more expressive of the user’s intent.

So instead of “setting state”, event handlers dispatch an action--e.g. “added/changed/deleted"--on some state. The state update logic still happens, but lives elsewhere.

An action object can have any shape. It is conventional to give it a string type that describes what happened, and pass any additional info in other fields. The value of type is specific to a component, so in this example either 'added' or 'added_task' would be fine.

export default function TaskApp() {
  const [state, dispatch] = useReducer(myReducer, initialState);

  function handleSomeEvent(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }
  ...
}

Write a reducer function

A reducer function is where the state logic lives. It accepts current state and action object as args, and returns the next state. For instance...

function myReducer(state, action) {
  if (action.type === 'added') {
    return [
      ...state,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if (
  ...
}

NOTE: myReducer() doesn't actually store the updated state--it merely returns it. Storing state is the job of the dispatch() function returned by useReducer().

By the way: Instead of if/else statements, it’s a convention to use switch statements inside your reducers. The result is the same, but it can be easier to read switch statements at a glance.

Reactjs.org recommends wrapping each case block with curly braces so that variables declared inside of different cases don’t clash with each other. Also, a case should usually end with a return. If you forget to return, the code will “fall through” to the next case, which can lead to mistakes!

Or just use if/else.

3. Wire up the reducer to your component

import {useReducer} from 'react';

Replace useState:

const [tasks, setState] = useState(initialState);

with useReducer like so:

const [tasks, dispatch] = useReducer(myReducer, initialState);

You're basically collecting setState() and other state-handling logic all into the myReducer() reducer you've written. Now you call dispatch() everywhere, which invokes your reducer for all the state-handling. To put it another way: All the state-setting that an event handler requires will now be done by myReducer, called via the dispatch() function returned from useReducer().

useState versus useReducer

  • Code size: Generally, useState requires less code upfront. useReducer means writing both a reducer function and dispatch actions. However, useReducer can DRY out code if many event handlers modify state in a similar way.
  • Readability: useState is easy to read when the state updates are simple. When they get more complex, they can bloat your component’s code and make it difficult to scan. In this case, useReducer lets you cleanly separate the how of update logic from the what happened of event handlers.
  • Debugging: When you have a bug with useState, it can be difficult to tell where the state was set incorrectly, and why. With useReducer, you can add a console log into your reducer to see every state update, and why it happened (due to which action). If each action is correct, you’ll know that the mistake is in the reducer logic itself. However, you have to step through more code than with useState.
  • Testing: A reducer is a pure function that doesn’t depend on your component. This means that you can export and test it separately in isolation. For complex state update logic it can be useful to assert that your reducer returns a particular state for a particular initial state and action.
  • Personal preference: You can always convert between useState and useReducer: they are equivalent.

Writing reducers well

  1. Reducers must be pure. Similar to state updater functions, reducers run during rendering! (Actions are queued until the next render.) This means that reducers must be pure—same inputs always result in the same output. They should not send requests, schedule timeouts, or perform any side effects (operations that impact things outside the component). They should update objects and arrays without mutations.
  2. Each action describes a single user interaction, even if that leads to multiple changes in the data. For example, if a user presses “Reset” on a form with five fields managed by a reducer, it makes more sense to dispatch one reset_form action rather than five separate set_field actions. If you log every action in a reducer, that log should be clear enough for you to reconstruct what interactions or responses happened in what order. This helps with debugging!

Context - Passing State deeply

From https://beta.reactjs.org/learn/passing-data-deeply-with-context.

The nearest common ancestor Component could be far removed from the components that need data, and lifting state up that high can lead to a situation sometimes called “prop drilling”.

Wouldn’t it be great if there were a way to “teleport” data to the components in the tree that need it without passing props all the way down to it? With React’s context feature, there is!

Context lets a parent component provide data to the entire UI tree below it.

Instead of getting the state from its parent Component's props, a Component can call useContext(MyContext) to ask for the state.

Implementing Context

Three steps:

  1. Create a context for storing state. (E.g., LevelContext, if providing a heading level.)
  2. Use that context from the component that needs the data. (Heading will use LevelContext.)
  3. Provide that context from the component that specifies the data. (Section will provide LevelContext.)

Create a context

import { createContext } from 'react';

export const ImageContext = createContext(100);

Provide the context at the level of the component where the state lives

import { ImageContext } from './Context.js'

export default function App() {
  const imageSize = isLarge ? 150 : 100;
  return (
    <>
      <ImageContext.Provider value={imageSize}>
        <List />
      </ImageContext.Provider>
    </>
  )
}

Use the context at the component that needs it

import { useContext } from 'react';

function PlaceImage({ place }) {
  const imageSize = useContext(ImageContext);
  return (
    <img src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}

How context works might remind you of CSS property inheritance. In CSS, you can specify color: blue for a <div>, and any DOM node inside of it, no matter how deep, will inherit that color unless some other DOM node in the middle overrides it with color: green. Similarly, in React, the only way to override some context coming from above is to wrap children into a context provider with a different value.

In CSS, different properties like color and background-color don’t override each other. You can set all <div>’s color to red without impacting background-color. Similarly, different React contexts don’t override each other. Each context that you make with createContext() is completely separate from other ones, and ties together components using and providing that particular context. One component may use or provide many different contexts without a problem.

A few alternatives to consider before using context

  • Start by passing props. If your components are not trivial, it’s not unusual to pass a dozen props down through a dozen components. It may feel like a slog, but it makes it very clear which components use which data! The person maintaining your code will be glad you’ve made the data flow explicit with props.
  • Extract components and pass JSX as children to them. If you pass some data through many layers of intermediate components that don’t use that data (and only pass it further down), this often means that you forgot to extract some components along the way. For example, maybe you pass data props like posts to visual components that don’t use them directly, like <Layout posts={posts} />. Instead, make Layout take children as a prop, and render <Layout><Posts posts={posts} /></Layout>. This reduces the number of layers between the component specifying the data and the one that needs it.

Use cases for context

Theming: If your app lets the user change its appearance (e.g. dark mode), you can put a context provider at the top of your app, and use that context in components that need to adjust their visual look. Current user: Many components might need to know the currently logged in user. Putting it in context makes it convenient to read it anywhere in the tree. Some apps also let you operate multiple accounts at the same time (e.g. to leave a comment as a different user). In those cases, it can be convenient to wrap a part of the UI into a nested provider with a different current account value. Routing: Most routing solutions use context internally to hold the current route. This is how every link “knows” whether it’s active or not. If you build your own router, you might want to do it too. Managing state: As your app grows, you might end up with a lot of state closer to the top of your app. Many distant components below may want to change it. It is common to use a reducer together with context to manage complex state and pass it down to distant components without too much hassle.

Context is not limited to static values. If you pass a different value on the next render, React will update all the components reading it below!

Redux

Redux is a library for managing the state of complex React applications. It is separate from React and developed by a different team.

Redux is the most popular library and a viable alternative to Context API and a more advanced library with rich features. It can be a replacement for Context API. However, Context API does not have all the features of Redux.