Categories
JavaScript React

Create React Error Boundary Components to Handle Errors Gracefully

React is a library for creating front end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.

We can handle the error gracefully with error boundary components.

In this article, we’ll look at how to define and use them.

Error Boundaries

JavaScript inside components used to corrupt React’s internal state and cause cryptic errors to be emitted on the next renders.

These errors are always caused by earlier errors in the application code. React didn’t provide a way to handle them gracefully in components and recover from them.

We can use error boundaries to catch JavaScript errors anywhere in the child component tree, log those errors and display a fallback UI instead of crashing the app.

However, error boundaries don’t catch errors for event handlers, asynchronous code, server-side rendering, or errors thrown in the error boundary itself.

We can create an error boundary component by adding the static getDerivedStateFromError() or componentDidCatch() methods inside a class-based component.

For example, we can use it as follows:

class ErrorBoundary extends React.Component {  
  constructor(props) {  
    super(props);  
    this.state = { hasError: false };  
  } 

  static getDerivedStateFromError(error) {  
    return { hasError: true };  
  } 

  componentDidCatch(error, errorInfo) {  
    console.log(error, errorInfo);  
  } 

  render() {  
    if (this.state.hasError) {  
      return <h1>Error occured.</h1>;  
    } return this.props.children;  
  }  
}

class Button extends React.Component {  
  constructor(props) {  
    super(props);  
    this.state = { count: 0 };  
  }  
  increment() {  
    this.setState({ count: this.state.count + 1 });  
  }  
  render() {  
    if (this.state.count === 5) {  
      throw new Error("error");  
    } 

    return (  
      <>  
        <button onClick={this.increment.bind(this)}>Click Me</button>  
        <br />  
        <p>{this.state.count}</p>  
      </>  
    );  
  }  
}

class App extends React.Component {  
  render() {  
    return (  
      <div>  
        <ErrorBoundary>  
          <Button />  
        </ErrorBoundary>  
      </div>  
    );  
  }  
}

In the code above, we have the ErrorBoundary component, which has the componentDidCatch method to log the errors given by the parameters.

Then we added the static getDerivedStateFromError to set the hasError state to true if an error occurred.

In the render method, we render the error message if hasError state is true . Otherwise, we display the child components as usual.

In the Button component, we have a button element that increases the count state it’s clicked. We have an if statement for this.state.count which throws an error if it reaches 5.

Once it reaches 5, the ‘Error occurred’ message from the ErrorBoundary component will be shown since the error is thrown in the render method, which isn’t an event handler, async code, and other places error boundaries can’t catch.

As we can see, we placed the ErrorBoundary outside any code that catches errors. This way, ErrorBoundary can actually catch them.

During development, the whole stack trace will be displayed, we have to disable that in production. This will be done automatically when we make a production build with Create React App.

Catching Errors Inside Event Handlers

We have to use try/catch block to catch errors in event handlers.

For example, we can write the following code to catch those:

class App extends React.Component {  
  constructor(props) {  
    super(props);  
    this.state = { error: null, count: 0 };  
    this.handleClick = this.handleClick.bind(this);  
  } 

  handleClick() {  
    try {  
      if (this.state.count === 5) {  
        throw new Error("error");  
      }  
      this.setState({ count: this.state.count + 1 });  
    } catch (error) {  
      this.setState({ error });  
    }  
  } 
  
  render() {  
    if (this.state.error) {  
      return <h1>Error occurred.</h1>;  
    }  
    return (  
      <button onClick={this.handleClick}>Count: {this.state.count}</button>  
    );  
  }  
}

In the handleClick method, we have an if statement that throws an error if this.state.count is 5.

If an error is thrown, the catch block will set the error state to the error object.

This will then render the ‘Error occurred’ message instead of the button to increase the count state.

Conclusion

We can use error boundary components to handle errors gracefully and log them.

To create an error boundary component, we add a static getDerivedStateFromError() or componentDidCatch() method to do that.

We use componentDidCatch to log errors and getDerivedStateFromError to render a fallback UI.

We wrap the error component around child components that may throw errors.

Error boundary can catch errors in places other than async code, event handlers, server-side rendering code, or errors thrown in the error boundary itself.

Categories
JavaScript TypeScript

Merging Declarations in TypeScript

With TypeScript, we can declare many entities that don’t exist in vanilla JavaScript like interfaces, enums, and type alias. Sometimes, these types have to be merged together into one, like when we try to derive a new class from multiple classes or interfaces. This means that there are rules for merging them together. The process of declaration merging is the combining of multiple members from different sources with overlapping member names or types.

Basic Rules of Merging Interfaces

Merging interfaces is the simplest and most common operation type of declaration merging. The most basic case is that 2 interfaces have no overlapping members. In this case, they just get merged together mechanically into one. For example, if we have 2 Person interfaces like we do in the following code:

interface Person {  
  name: string;  
}

interface Person {  
  age: number;  
}

Then they’ll just be merged together into one when we declare a variable with the Person type. For example, we can write something like:

let person: Person = {  
  name: 'Jane',  
  age: 20  
}

Then TypeScript would accept this code and compile and run the code.

If there’re any overlap of non-function members, then they must be unique. Otherwise, they must have the same type. For example, if we have:

interface Person {  
  name: string;  
  age: string;  
}

interface Person {  
  age: number;  
}

Then we get the following errors:

Subsequent property declarations must have the same type.  Property 'age' must be of type 'string', but here has type 'number'.(2717)input.ts(3, 5): 'age' was also declared here.

Also when 2 interfaces are being merged, whatever is in the second interface will overwrite the members of the first one if the first one has overlapping members with the second one. For function members, they’ll just be combined together as overloads of the same function. For example, if we have the following interface declarations:

interface Animal { };  
interface Sheep { };  
interface Dog { };  
interface Cat { };

interface Eater {  
  eat(animal: Animal): Animal;  
}

interface Eater {  
  eat(animal: Sheep): Sheep;  
}

interface Eater {  
  eat(animal: Dog): Dog;  
  eat(animal: Cat): Cat;  
}

Then we get the Eater interface, would be:

interface Eater {    
  eat(animal: Dog): Dog;  
  eat(animal: Cat): Cat;  
  eat(animal: Sheep): Sheep;  
  eat(animal: Animal): Animal;  
}

The elements of each group maintain the same order but the group of functions that are in the later interfaces is ordered first in the merged interface. The only exception is that when the signature of a function member is composed of a single string literal, then that’ll be moved to the top. For example, if we have:

interface Animal { };  
interface Sheep { };  
interface Dog { };  
interface Cat { };
interface Eater {  
  eat(animal: 'Animal'): Animal;  
}

interface Eater {  
  eat(animal: Sheep): Sheep;  
}

interface Eater {  
  eat(animal: Dog): Dog;  
  eat(animal: Cat): Cat;  
}

Then the merged Eater interface would be:

interface Eater {    
  eat(animal: 'Animal'): Animal;  
  eat(animal: Dog): Dog;  
  eat(animal: Cat): Cat;  
  eat(animal: Sheep): Sheep;  
}

Namespaces

Namespaces of the same name will have their members merged together. All the exported interfaces and classes are merged into one namespace. For example, if we have the following namespaces:

namespace Animals {  
  export class Cat { }  
}

namespace Animals {  
  export interface Mammal {  
      name: string;          
  }  
  export class Dog { }  
}

Then the merged namespace will be:

namespace Animals {  
  export interface Mammal {  
      name: string;          
  } 
  export class Cat { }  
  export class Dog { }  
}

Non-exported members, on the other hand, are only visible in their own un-merged namespace. For example, if we have:

namespace Animal {  
  export class Cat { }  
  export const getAnimal = () => {  
    return animal;  
  }  
}

namespace Animal {  
  let animal = {};  
  export interface Mammal {  
    name: string;          
  }  
  export class Dog { }  
}

Then we get the following error messages:

Cannot find name 'animal'. Did you mean 'Animal'?(2552)

since the animal variable isn’t exported.

We can’t have 2 namespace members with the same name since we would have duplicate declarations that aren’t allowed. For example, if we have:

namespace Animal {  
    export class Cat { }  
    export let animal = {};  
    export const getAnimal = () => {  
        return animal;  
    }  
}

namespace Animal {  
    export let animal = {};  
    export interface Mammal {  
      name: string;          
    }  
    export class Dog { }  
}

Then we get the error:

Cannot redeclare block-scoped variable 'animal'.(2451)

Merging Namespaces with Classes

The rules for merging namespaces with classes is the same as merging any other namespace members. They have to be exported for us to merge them. We can write something like the following to define inner classes by referencing a class inside a namespace from an outer class:

class Person {  
    label: Person.PersonName;      
}

namespace Person {  
    export class PersonName { }  
}

We can also reference members from a namespace from a function that has the same name as the namespace. For example, we can write the following code:

function buildName(middleName: string): string {  
    return `${buildName.firstName} ${middleName} ${buildName.lastName}`;  
}

namespace buildName {  
    export let firstName = "Jane";  
    export let lastName = "Smith";  
}
buildName('Mary');

We can access the members of the buildName namespace inside the buildName function as long as we export the members. Likewise we can reference enum members from inside a namespace with the same name as the enum. For example, we can write something like:

enum Fruit {  
  Orange = 1,  
  Apple = 2,  
  Banana = 4  
}

namespace Fruit {  
  export const mixFruit = (fruit: string) => {  
    if (fruit == "Orange and Apple") {  
      return Fruit.Orange + Fruit.Apple;  
    }  
    else if (fruit == "Apple and Banana") {  
      return Fruit.Apple + Fruit.Banana  
    }  
    return 0;  
  }  
}

As we can see, we can access the members of the Fruit enum in the Fruit namespace inside the mixFruit function.

Disallowed Merges

Currently, we can merge classes with other classes or variables.

Module Augmentation

We can use the declare keyword to declare that a module has members with properties that the TypeScript compiler doesn’t know about. For example, we can write something like:

// fruit.ts  
export class Fruit<T> {  
  // ...  
}

// setColor.ts

import { Fruit } from "./fruit";  
declare module "./fruit" {  
  interface Fruit<T> {  
    setColor(f: (x: string) => string): Fruit;  
  }  
}  
Fruit.prototype.setColor = function (f) {  
  // ...  
}

In the code above, we used the declare module keywords to declare the items in the Fruit class that the TypeScript compiler can’t see. It lets us manipulate things that are defined but TypeScript compiler can’t spot.

We can use the method above to patch existing declarations. It doesn’t work with new top-level declarations. Also, default exports can’t be augmented since it has no name.

Global Augmentation

We can also add declarations in the global scope from inside a module. For example, we can write something like the following code:

// fruit.ts  
export class Fruit {  
   
}

declare global {  
  interface Array<T> {  
    toFruitArray(): Fruit[];  
  }  
}

Array.prototype.toFruitArray = function () {  
  return this.map(a => <Fruit>{ ... });  
}

In the code above, we made the TypeScript compiler aware of the global Array object and then added a toFruitArray to its prototype. Without the declare clause, we would get an error since the TypeScript compiler doesn’t know that the toFruitArray method exists in the Array global object.

Many things can be merged together in TypeScript. Merging interfaces is the simplest and most common operations types of declaration merging. The most basic case is that 2 interfaces have no overlapping members. In this case, they just get merged together mechanically into one. Also when 2 interfaces are being merged, whatever is in the second interface will overwrite the members of the first one if the first one has overlapping members with the second one. For function members, they’ll just be combined together as overloads of the same function. Namespaces of the same name will have their members merged together. All the exported interfaces and classes are merged into one namespace.

We can use the declare keyword to declare that a module has members with properties that the TypeScript compiler doesn’t know about. We can also add declarations in the global scope from inside a module with global module augmentation. Classes and variables currently can’t be merged together.

Categories
JavaScript TypeScript

Manipulate JavaScript Arrays the Functional Way

Functional programming is a programming paradigm which states that we create computations as the evaluation of functions and avoid changing state and mutable data.

To write programs in a functional way, we use pure functions and immutable data structures.

In JavaScript, we can easily apply functional principles to arrays by using its built-in array methods.

In this article, we’ll look at the array methods that let us manipulate them in a functional way.

Filtering Items

We can use the filter method to return an array with entries from the original array that has the condition that the callback of the filter method returns.

For example, if we have the following array:

const store = [{  
    name: "Apple",  
    price: 1  
  },  
  {  
    name: "Banana",  
    price: 2  
  },  
  {  
    name: "Grape",  
    price: 1.2  
  }  
];

We can get the items that have price less than 2 as follows:

const cheapItems = store.filter(s => s.price < 2);

Then we get the following for cheapItems:

[  
  {  
    "name": "Apple",  
    "price": 1  
  },  
  {  
    "name": "Grape",  
    "price": 1.2  
  }  
]

Using the filter method fits with the functional paradigm since the function is pure, given the same array and same condition, we always get the same results returned.

It also doesn’t change the existing elements of the array that it’s called on, which means we can’t accidentally change anything from the original array.

Mapping Array Objects

map is used to map entries of an array into something else. It’s frequently used to convert and transform data. Using map fits with the functional programming paradigm because again it’s a pure function that gives the same outputs for the given inputs, and it doesn’t change the original array.

As long as the function we pass into the map function to combine the values is pure, the map method should also be pure since it just calls this callback function and returns the updated values into a new array.

For example, given the following array:

const volumesInLiters = [{  
    name: "Milk",  
    volumeInLiters: 1  
  },  
  {  
    name: "Apple Juice",  
    volumeInLiters: 2  
  },  
  {  
    name: "Orange Joice",  
    volumeInLiters: 1.2  
  }  
];

We can add the volumeInQuarts field to each entry of the object and set the volume in quarts to as the value of each by writing:

const volumesInQuarts = volumesInLiters.map(v => {  
  v = {  
    ...v,  
    volumeInQuarts: v.volumeInLiters * 1.057  
  };  
  return v;  
})

We convert v.volumeInLiters * 1.057 and set it to volumeInQuarts for each entry.

Then we get:

[  
  {  
    "name": "Milk",  
    "volumeInLiters": 1,  
    "volumeInQuarts": 1.057  
  },  
  {  
    "name": "Apple Juice",  
    "volumeInLiters": 2,  
    "volumeInQuarts": 2.114  
  },  
  {  
    "name": "Orange Joice",  
    "volumeInLiters": 1.2,  
    "volumeInQuarts": 1.2684  
  }  
]

Using Reduce to Combine Values of Array Entries

Like filter and map, reduce also fits with the functional paradigm since we use it to gather entries of an array and return one value by the way that we specify.

As long as the function we pass into the reduce function to combine the values is pure, the reduce method should be pure. The function just calls the callback function we pass into reduce to compute the combined value.

For example, given the following array:

const store = [{  
    name: "Apple",  
    price: 1  
  },  
  {  
    name: "Banana",  
    price: 2  
  },  
  {  
    name: "Grape",  
    price: 1.2  
  }  
];

We can write the following to get the total price of all items:

const totalPrice = store.map(s => s.price).reduce((subtotal, b) => subtotal + b, 0);

We have to use map to map all the entries in the store array to price numbers. Then we can use the reduce method to add the new value to subtotal.

Then we should get 4.2 for totalPrice, which is the total of all the prices of the 3 items in store.

The second argument of reduce is the starting value of the reduction, or combining the values into one.

Conclusion

The filter, map, and reduce array methods are the main methods for manipulating arrays in a functional manner. They all return values and don’t change the array that it’s called on. This makes it difficult to unintentionally change the values.

filter returns an array that meets the condition that we return in the callback that we pass in.

map returns a new array that has the values of the original array mapped to new values in a way that we specify.

reduce combines the values of array entries into one by taking a callback function that we pass in to do the combining and returns the combined value.

The functions are pure if we pass in pure functions to them as callbacks since they just call the functions that we pass in to do the manipulation.

Categories
JavaScript

JavaScript Events Handlers- onfocus , oncancel , and oncanplay

In JavaScript, events are actions that happen in an app. They’re triggered by various things like inputs being entered, forms being submitted, and changes in an element like resizing, or errors that happen when an app is running, etc. We can assign event handler to handle these events. Events that happen to DOM elements can be handled by assigning an event handler to properties of the DOM object for the corresponding events. In this article, we will look at the onfocus , oncancel , and oncanplay event handlers.

onfocus

The onfocus property of the document object lets us set an event handler for the focus event, which is the opposite of the blur event. The focus event is trigger when a user sets focus on an HTML element. If we want to the focus event to fire for non-input elements, we have to put tabindex attribute to it. What that attribute added we can focus on it with our computer’s Tab key. For example, we can attach the onfocus event handler to the document object by writing:

document.onfocus = () => {  
  console.log('focus');  
}

We can also get the Event object that triggered the focus event by adding the event parameter to the event handler function like in the following code:

document.onfocus = (event) => {  
  console.log(event);  
}

Then when we click in and out of the page, we something like the following output from the code above:

bubbles: false  
​cancelBubble: false  
​cancelable: false  
​composed: true  
​currentTarget: null  
​defaultPrevented: false  
​detail: 0  
​eventPhase: 0  
​explicitOriginalTarget: <html>  
​isTrusted: true  
​layerX: 0  
​layerY: 0  
​originalTarget: HTMLDocument https://fiddle.jshell.net/_display/
​rangeOffset: 0  
​rangeParent: null  
​relatedTarget: null  
​returnValue: true  
​srcElement: HTMLDocument https://fiddle.jshell.net/_display/
​target: HTMLDocument https://fiddle.jshell.net/_display/ 
​timeStamp: 1463  
​type: "focus"  
​view: Window https://fiddle.jshell.net/_display
​which: 0

The output above is the properties and the corresponding values of the Event object. To see more details about the Event object, we can look at the previous articles.

oncancel

To get handle the situation when the dialog element has closed, we can use the oncancel event handler, since the cancel event is triggered when it’s closed. Handling the event with the oncancel event handler prevents it from bubbling, so parent handlers aren’t notified of the event. Only one oncancel handler can be assigned to an object at once. However, if we use the addEventListener to attach the event handler function to our element, then we can get around this limitation. For example, we can use it like in the following code:

const dialog = document.getElementById('dialog');  
const openButton = document.getElementById('open-button');  
openButton.onclick = () => {  
  dialog.showModal();  
};

dialog.oncancel = (event) => {  
  console.log('cancel');  
  console.log(event);  
}

Then we have to add the dialog element to our HTML code:

<button id='open-button'>  
  Open Dialog  
</button>  
<dialog id="dialog">  
  <form method="dialog">  
    <p>  
      Dialog  
    </p>  
    <menu>  
      <button id="cancel-button" value="cancel">Cancel</button>  
      <button id="confirmBtn" value="default">Confirm</button>  
    </menu>  
  </form>  
</dialog>

In the code above we added a dialog element to the HTML and get the DOM element for the HTML dialog element with the getElementById method, which gets us has the following methods:

  • close() — a dialog instance method to close the dialog element. An optional string can be passed in as an argument, which updates the returnValue of the dialog , which is useful for indicating which button the user used to closed it.
  • show() — a dialog instance method to display the dialog modelessly, which means we still allow interaction from the outside. It takes no arguments.
  • showModal() — a dialog instance method to display the dialog as a modal over the top of anything else. It displays on the top layer along with a ::backdrop pseudo-element. Interaction with elements outside the dialog is blocked and the content outside can’t be interacted with.

dialog DOM elements also have the following value properties:

  • open — a boolean property that reflects the open HTML attribute, which indicates whether a dialog is open for interaction.
  • returnValue — a string property that sets and returns the return value for the dialog. We can assign it a value directly or we can pass in an argument to the close method to set this property.

We didn’t need to call the close() method to close the dialog box. Having a button is enough. Also, we don’t have to click a button to close the dialog, we can also press the Esc key on our keyboard to do so.

Note that the dialog element isn’t enabled by default on Firefox. To use it in Firefox, we have to set dom.dialog_element.enabled to true in the about:config page. Chrome has this feature enabled by default. Then if we click the ‘Open Dialog’ button that we just made, then we will see a native browser dialog box. Then if we press the Esc key to close the dialog, then the cancel event will be triggered and the event handler function that we assigned to the dialog.oncancel will run. The event parameter of the event handler function will get an Event object, which has the information about the source of the cancel event which is our dialog element. So we would get something like the following in our console.log output:

bubbles: false  
cancelBubble: false  
cancelable: true  
composed: false  
currentTarget: null  
defaultPrevented: false  
eventPhase: 0  
isTrusted: true  
path: (5) [dialog#dialog, body, html, document, Window]  
returnValue: true  
srcElement: dialog#dialog  
target: dialog#dialog  
timeStamp: 1102.240000385791  
type: "cancel"

To see more details about the properties of the Event object, we can look at the previous parts of this series.

oncanplay

We can assign an event handler function to the oncanplay property when we want to handle the canplay event. The canplay event is fired when the user agent can play the media, but estimates that not enough data has been loaded to play the media up to its end without having to stop for further buffering of content. For example, we can use it for videos to see if enough parts of the video are downloaded to play until the end, we can first add a video element to the HTML code:

<video width="320" height="240" controls id='video'>  
  <source src="https://sample-videos.com/video123/mp4/240/big_buck_bunny_240p_30mb.mp4" type="video/mp4">  
</video>

Then in our JavaScript code, we can add the following code to check if the video has downloaded enough to play:

const video = document.getElementById('video');  
video.oncanplay = (event) => {  
  console.log(event);  
}

We can also attach the canplay event listener by using the addEventListener method on the video DOM node instead like in the following code:

const video = document.getElementById('video');  
video.addEventListener('canplay', (event) => {  
  console.log(event);  
});

Either way, we can check if enough parts of the media has been downloaded in order for it to finish by using the readyState property of the media element, which include video and audio elements. The readyState can have one of the following possible values:

  • The constant HAVE_NOTHING or number 0 — there’s no information about the media resource
  • The constant HAVE_METADATA or number 1 — enough parts of the media resource has been downloaded that the metadata attributes are initialized. Seeking won’t raise exceptions in this state or beyond
  • The constant HAVE_CURRENT_DATA or number 2 — there’s enough data available for current playback position, but not enough to actually play more than one frame
  • The constant HAVE_FUTURE_DATA or number 3 — there’s enough data for the current playback position is downloaded, as well as enough data for playing at least a little bit into the future, which means at least 2 frames of video.
  • The constant HAVE_ENOUGH_DATA or number 4 — there’s enough data available, and that the download rate is high enough, that the media can be played through to the end without interruption.

We can get the readyState property of our video element by writing the following code:

const video = document.getElementById('video');  
video.oncanplay = (event) => {  
  console.log(event.target.readyState);  
}

If our video can be played all the way through, we should have 4 logged in the console.log output.

The onfocus property of the document object lets us set an event handler for the focus event. We can use it to check if our element is focus or not. To focus non-input elements, we can add a tabindex attribute to it. To get handle the situation when the dialog element has closed, we can use the oncancel event handler. The corresponding event which is the cancel event is triggered when the Esc key on the keyboard is pressed. We can assign an event handler function to the oncanplay property when we want to handle the canplay event, which is the event that’s triggered when the user agent can play the media, but estimates that not enough data has been loaded to play the media up to its end without having to stop for more buffering of content. We can check if the media has downloaded enough data to be played all the way through by checking the readyState from the media element.

Categories
JavaScript React

Basic React Hooks – useContext and useReducer

React is a library for creating front end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.

In this article, we’ll look at the useContext and useReducer hooks.

useContext

We can use the useContext hook to read shared data shared from a React context. It accepts the context object returned from React.createContext as an argument and returns the current context value.

The current context value is determined by the value prop of the nearest context provider.

We can use it as follows:

const ColorContext = React.createContext("green");function Button() {  
  const color = React.useContext(ColorContext);  
  return <button style={{ color }}>button</button>;  
}function App() {  
  return (  
    <>  
      <ColorContext.Provider value="blue">  
        <Button />  
      </ColorContext.Provider>  
    </>  
  );  
}

In the code above, we created a new React context with:

const ColorContext = React.createContext("green");

Then in App , we wrapped out Button with the ColorContext.Provider with the value prop set to blue .

Then in Button , we have:

const color = React.useContext(ColorContext);

to get the value passed in from the ColorContext.Provider and set it to color .

Finally, we set the color style of the button with the color ‘s value.

A component calling useContext will always re-render when the context value changes. If re-rendering is expensive, then we can optimize it with memoization.

useContext is the React hooks version of Context.Consumer .

useReducer

This hook is an alternative to useState . It accepts a reducer function of type (state, action) => newState .

useReducer is preferable to useState when we have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

It also lets us optimize performance for components that trigger deep updates because we can pass dispatch down instead of callbacks.

For example, we can write:

const INCREMENT = "INCREMENT";  
const DECREMENT = "DECREMENT";function reducer(state, action) {  
  switch (action.type) {  
    case INCREMENT:  
      return { count: state.count + 1 };  
    case DECREMENT:  
      return { count: state.count - 1 };  
    default:  
      throw new Error();  
  }  
}

function App() {  
  const [state, dispatch] = React.useReducer(reducer, { count: 0 });  
  return (  
    <>  
      Count: {state.count}  
      <button onClick={() => dispatch({ type: DECREMENT })}>Decrement</button>  
      <button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>  
    </>  
  );  
}

In the code above, we have our reducer which returns the new state depends on the action.type ‘s value. In this case, it’s either 'INCREMENT' or 'DECREMENT' .

If it’s ‘INCREMENT’ , we return { count: state.count + 1 } .

If it’s ‘DECREMENT’ , we return { count: state.count — 1 } .

Otherwise, we throw an error.

Then in App , we call useReducer by passing in a reducer as the first argument and the initial state as the second argument.

Then we get the state object, which has the current state object and a dispatch function, which we can call with an action object, which has the type property with the value being one of ‘INCREMENT’ or ‘DECREMENT' .

We used the dispatch function in the buttons to update the state.

Finally, we display the latest state in state.count .

Lazy initialization

We can pass in a function to the 3rd argument of useReducer to initialize the state lazily.

The initial state will be set to init(initialArg) .

For instance, we can rewrite the previous example as follows:

const init = initialCount => {  
  return { count: initialCount };  
};

const INCREMENT = "INCREMENT";  
const DECREMENT = "DECREMENT";

function reducer(state, action) {  
  switch (action.type) {  
    case INCREMENT:  
      return { count: state.count + 1 };  
    case DECREMENT:  
      return { count: state.count - 1 };  
    default:  
      throw new Error();  
  }  
}
function App() {  
  const [state, dispatch] = React.useReducer(reducer, 0, init);  
  return (  
    <>  
      Count: {state.count}  
      <button onClick={() => dispatch({ type: DECREMENT })}>Decrement</button>  
      <button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>  
    </>  
  );  
}

First, we have:

const init = initialCount => {  
  return { count: initialCount };  
};

to return the initial state.

And instead of writing:

React.useReducer(reducer, { count: 0 });

We have:

React.useReducer(reducer, 0, init);

0 is passed in as the initialCount of init .

Then the rest of the code is the same as before.

Bailing out of a dispatch

If the same value is returned from a Reducer hook is the same as the current state, React will bail out without rendering the children or firing effects.

The comparison is done using the Object.is() algorithm.

If we’re doing expensive operations while rendering, we can optimize it with useMemo .

Conclusion

The useContext hook is the React hook equivalent of the Context.Consumer of the Context API.

It takes a React context object as the argument and returns the current value from the context.

useReducer is an alternative version of useState for more complex state changes.

It takes in a reducer as the first argument and the initial state object as the second argument.

It can also take the same first argument, and take the initial state value as the second argument, and a function to return the initial state as the 3rd argument. This combination lets React set the initial state lazily.