Fundamentals of React

I've already stated that I don't like React. It produces hard-to-debug and slow JS-taxed stacks. Moreover, it significantly decreases production time.

However, whether it's for the better or worst, React is a de-facto standard in our industry.

So, I'd been looking for a book to get back on track with 2023's React and found The Road to React to be a good one. It's written by Robin Wieruch and the source code for the book is available on GitHub.

Here are my notes from the first part: "Fundamentals of React".

Setting up a React Project

Vite allows getting started with React while not getting distracted by any tooling around it.

It's a build tool with :

  • a development server
  • a bundler which outputs files for a production-ready deployment

Set up the hacker-stories project:

cd /Users/kemar/code/react
npm create vite@latest hacker-stories -- --template react
cd hacker-stories
npm install

Setup ESLint:

npm install vite-plugin-eslint --save-dev
npm install eslint --save-dev
npm install eslint-config-react-app --save-dev
printf '{\n  "extends": [\n    "react-app"\n  ]\n}' > .eslintrc

Run dev:

npm run dev

React DOM

The entry point is index.html which includes src/main.jsx.

Two libraries are imported in src/main.jsx:

  • react
    • used for the day to day business of a React developer
  • react-dom
    • used to hook React into the native HTML world (usually used once in a SPA)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

In index.html, the HTML element with id="root" is where React inserts itself into the HTML to bootstrap the React application.

createRoot() expects the HTML element that is used to instantiate React.

Once we have the root object, we can call render() on the returned root object with JSX as parameter which usually represents the entry point component (also called root component).

React DOM is everything that's needed to integrate React into any website which uses HTML.

React Component

Every React application is built on the foundation of React components.

A component:

  • has to start with a capital letter (PascalCase), otherwise it isn't treated as component in React
  • is just a JavaScript function (function components are the modern way of using components in React)

The function of a component runs:

  • at initial browser rendering
  • whenever the component updates because it has to display something different due to changes (re-rendering)

A variable that doesn't need to be re-defined every time a function runs can be defined outside of the component.

Declare a component

function Search() {
  return (
    <div>
      <label htmlFor="search">Search:</label>
      <input id="search" type="text" />
    </div>
  );
}

Instantiate a component

Instantiate the Search component in a parent component:

function App() {
  return (
    <div>
      <h1>Hello</h1>
      <Search />
    </div> );
}

A React component has only one component declaration but can have multiple component instances:

function App() {
  return (
    <div>
      <Search />
      <Search />
    </div> );
}

Different ways of declaring a component

Since components are function components, we can leverage the different ways of declaring functions in JavaScript.

// Function declaration.
function App() { … }

// Arrow function expression.
const App = () => { … }

If an arrow function's only purpose is to return a value, we can remove the block body of the function:

// Concise body as multi line.
const addOne = (count) =>
    count + 1;

// Concise body as one line.
const addOne = (count) => count + 1;

In React:

const App = () => (
  <div>…</div>
)

Block bodies are necessary to introduce business logic:

const App = () => {

    // Perform a task in between.

    return (
        <div>…</div>
    );
};

React JSX

Introducing JSX.

Everything returned from a React component will be displayed in the browser.

The output of a component is in JSX.

JSX (JavaScript XML) is a syntax extension that allows to combine HTML and JavaScript without the need of any extra templating syntax (except for the curly braces).

While HTML can be used almost (except for the attributes) in its native way in JSX, everything in curly braces ({}) can be used to interpolate JavaScript in it.

Some attributes differs from native HTML because of internal implementation details of React itself.

React does not require us to use JSX at all, instead it's possible to use methods like createElement():

const title = 'React';

// JSX…
const myElement = <h1>Hello {title}</h1>;

// …gets transpiled to JavaScript
const myElement = React.createElement('h1', null, `Hello ${title}`);

// …gets rendered as HTML by React
<h1>Hello React</h1>

Lists in JSX (key)

Every React element in a list should have a key assigned to it.

The key is an HTML attribute and should be a stable identifier.

Whenever React has to re-render a list, it checks whether an item has changed.

When using keys, React can efficiently exchange the changed items.

const list = [
  {title: 'React', objectID: 0},
  {title: 'Redux', objectID: 1},
];

function App() {
  return (
    <ul>
      {list.map(function (item) {
        return <li key={item.objectID}>{item.title}</li>;
      })}
    </ul>
}

Why do we need a React List Key.

As last resort, we can use the index of the item in the list (<li key={index}>).

Comments in JSX

{/* Only use an index as last resort. */}

Handler Function in JSX

In native HTML, we can add event handlers/listeners (usually in the form of JavaScript functions) by using the addEventListener() method programmatically on a DOM node.

In React, event handler functions will be passed instances of SyntheticEvent. A synthetic event is essentially a wrapper around the browser's native event.

In React, an (event) handler function is passed to the onChange attribute:

const Search = () => {

  const handleChange = (event) => {
    console.log(event);
    console.log(event.target.value);
  };

  return (
    <div>
      <label htmlFor="search">Search: </label>
      <input id="search" type="text" onChange={handleChange} />
    </div>
  );
};

Always pass functions to these attributes, not the return value of the function, except when the return value is a function again.

To access the native HTML event, use event.nativeEvent.

React Props (properties)

props:

  • is as an immutable data structure
  • can only be used down the component hierarchy (passed from a parent to a child component and not vice versa)
const App = () => {
  const stories = […];
  return (
    <div>
      <List list={stories} />
    </div>
  );
}

const List = (props) => (
  <ul>
    {props.list.map(item => (
      <Item key={item.objectID} item={item} />
    ))}
  </ul>
)

const Item = (props) => (
  <li>
    <a href={props.item.url}>{props.item.title}</a>
  </li>
)

About key={item.objectID}:

  • the key must be set inside the <List /> component when looping over the array rather than on the <li> element in the (extracted) Item component
  • keys only make sense in the context of the surrounding array
  • a good rule of thumb is that elements inside the map() call need keys

React State (the useState hook)

React state introduces a mutable data structure:

  • props are used to pass information down the component hierarchy
  • state is used to change information over time

We must tell React what is a stateful value:

const Search = () => {

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

  const handleChange = (event) => {
    setSearchTerm(event.target.value);
  };

  return (
    <div>
      <label htmlFor="search">Search: </label>
      <input id="search" type="text" onChange={handleChange} />

      <p>Searching for <strong>{searchTerm}</strong>.</p>

    </div>
  );
};

useState tells React that we want to have a stateful value which changes over time.

And whenever this stateful value changes, the affected components will re-render to use it.

The useState method:

  • takes an initial state as an argument (an empty string here)
  • returns an array with two entries:
    1. the first entry (searchTerm) represents the current state (read)
    2. the second entry (setSearchTerm) is a function to update this state (a state updater function) (write)

The useState function is called a React hook. A hook lets us opt into React's component lifecycle.

We can have as many useState hooks as we want in one or multiple components whereas state can be anything from a JavaScript string to a more complex data structure such as an array or object.

Callback Handlers in JSX

props transport information to descendant components.

When using state, we can make information stateful, but this information can also only be passed down by using props.

There is no way to pass information up the component tree.

However, we can introduce a callback handler instead.

To communicate the state up to a parent component:

  • pass a function from a parent component to a child component via props
  • call this function in the child component
  • but have the actual implementation of the called function in the parent component

In other words: when an (event) handler function is passed as props from a parent component to its child component, it becomes a callback handler.

A callback handler:

  • gets introduced as event handler (A)
  • is passed as function in props to another component (B)
  • is executed there as callback handler (C)
  • and calls back to the place it was introduced (D)
const App = () => {

  const handleSearch = (event) => {             // A
    console.log(event.target.value);            // D
  };

  return (
    <div>
      <Search onSearch={handleSearch} />        {/* B */}
    </div>
  );
}

const Search = (props) => {

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

  const handleChange = (event) => {
    setSearchTerm(event.target.value);
    props.onSearch(event);                      // C
  };

  return (
    <div>
      <label htmlFor="search">Search: </label>
      <input id="search" type="text" onChange={handleChange} />
      <p>Searching for <strong>{searchTerm}</strong>.</p>
    </div>
  );
};

The information is passed up implicitly with callback handlers.

Lifting State

The process of moving state from one component to another is called lifting state:

  • if a component below needs to update the state (e.g. Search), pass a callback handler down to it which allows this particular component to update the state above in the parent component
  • if a component below needs to use the state (e.g. displaying it), pass it down as props

The state should always be there where all components which depend on the state can read (via props) and update (via callback handler) it.

Tricks to make passing props more convenient (Props Handling)

Props Destructuring via Object Destructuring

JavaScript object destructuring:

const user = {
    firstName: 'Robin',
    lastName: 'Wieruch',
};

// Without object destructuring.
const firstName = user.firstName;
const lastName = user.lastName;

// With object destructuring.
const { firstName, lastName } = user;

In React:

const Search = (props) => {
  const { search, onSearch } = props;

  return (
    <div>
      <label htmlFor="search">Search: </label>
      <input id="search" type="text" value={search} onChange={onSearch}
      />
    </div>
  );
};

Destructuring props in the function signature:

const Search = ({ search, onSearch }) => (
  <div>
    <label htmlFor="search">Search: </label>
    <input
      id="search"
      type="text"
      value={search}
      onChange={onSearch}
    />
  </div>
);

Nested Destructuring

const user = {
    firstName: 'Robin',
    pet: {
      name: 'Trixi',
    },
};

// Without object destructuring.
const firstName = user.firstName;
const name = user.pet.name;

console.log(firstName + ' has a pet called ' + name);
// "Robin has a pet called Trixi"

// With nested object destructuring.
const {
    firstName,
    pet: {
      name,
    },
} = user;

console.log(firstName + ' has a pet called ' + name);
// "Robin has a pet called Trixi"

It's not the most readable option but can still be useful:

const Item = ({
  item: {
    title,
    url,
    author,
    num_comments,
    points,
},
}) => (
  <li>
    <span>
          <a href={url}>{title}</a>
    </span>
        <span>{author}</span>
        <span>{num_comments}</span>
        <span>{points}</span>
    </li>
);

Spread Operator

JavaScript's spread operator allows us to literally spread all key/value pairs of an object to another object:

const profile = {
    firstName: 'Robin',
    lastName: 'Wieruch',
};

const address = {
    country: 'Germany',
    city: 'Berlin',
};

const user = {
    ...profile,
    gender: 'male',
    ...address,
};

console.log(user);
// {
//   firstName: "Robin",
//   lastName: "Wieruch",
//   gender: "male"
//   country: "Germany,
//   city: "Berlin",
// }

In React:

// Instead of:
// const List = ({ list }) => (
//   <ul>
//     {list.map((item) => (
//       <Item
//         key={item.objectID}
//         title={item.title}
//         url={item.url}
//         author={item.author}
//         num_comments={item.num_comments}
//         points={item.points}
//       />
//     ))}
//   </ul>
// );

const List = ({list}) => (
  <ul>
    {list.map(item => (
      <Item key={item.objectID} {...item} />
    ))}
  </ul>
)

// `item` is now:
// {
//       title: 'Redux',
//       url: 'https://redux.js.org/',
//       author: 'Dan Abramov, Andrew Clark',
//       num_comments: 2,
//       points: 5,
//       objectID: 1,
// }

const Item = ({ title, url, author, num_comments, points }) => (
  <li>
    <span>
      <a href={url}>{title}</a>
    </span>
    <span>{author}</span>
    <span>{num_comments}</span>
    <span>{points}</span>
  </li>
)

Rest Operator

Rest parameters.

Even though spread and rest operators have the same syntax (three dots):

  • the spread operator is on the right side of an assignment
  • the rest operator is on the left side of an assignment

The rest operator is always used to separate an object from some of its properties:

const user = {
    id: '1',
    firstName: 'Robin',
    lastName: 'Wieruch',
    country: 'Germany',
    city: 'Berlin',
};

const { id, country, city, ...userWithoutAddress } = user;

console.log(userWithoutAddress);
// {
//   firstName: "Robin",
//   lastName: "Wieruch"
// }

Now it can be used in the List component to separate the objectID from the item:

const List = ({list}) => (
  <ul>
    {list.map(({ objectID, ...item }) => (
      <Item key={objectID} {...item} />
    ))}
  </ul>
)

const Item = ({ title, url, author, num_comments, points }) => (
  <li>
    <span>
      <a href={url}>{title}</a>
    </span>
    <span>{author}</span>
    <span>{num_comments}</span>
    <span>{points}</span>
  </li>
)

Destructuring assignment.

React Side-Effects (useEffect)

A React component's output is defined by its props and state.

Side-effects can affect this output too by interacting with:

  • third-party APIs (e.g. browser's localStorage API, remote APIs for data fetching)
  • HTML elements for width and height measurements
  • built-in JavaScript functions such as timers or intervals

Example:

const App = () => {
  // …

  const [searchTerm, setSearchTerm] = React.useState(
    localStorage.getItem('search') || 'React'
  );

  const handleSearch = (event) => {
    setSearchTerm(event.target.value);
    localStorage.setItem('search', event.target.value);
  };

  // …
}

There is one flaw:

  • the handler function should mostly be concerned with updating the state, but it has a side-effect now
  • if we use setSearchTerm somewhere else, we break the feature because the local storage doesn't get updated

This can be fixed by handling the side-effect at a centralized place and not in a specific handler.

A useEffect hook can trigger the desired side-effect each time the searchTerm changes:

const App = () => {
  // …

  const [searchTerm, setSearchTerm] = React.useState(
    localStorage.getItem('search') || 'React'
  );

  React.useEffect(() => {
    localStorage.setItem('search', searchTerm);
  }, [searchTerm]);

  const handleSearch = (event) => {
    setSearchTerm(event.target.value);
  };

  // …
}

The useEffect hook takes two arguments:

  • a function that runs the side-effect (stores searchTerm into local storage)
  • a dependency array of variables
    • if one of these variables changes, the function for the side-effect is called
    • it's also called initially when the component renders for the first time
    • leaving it out would make the function run on every render of the component
    • an empty array would make the function run only once when the component renders for the first time

React Custom Hooks

A custom hook can:

  • encapsulate non-trivial implementation details that should be kept away from a component
  • be used in more than one React component
  • be a composition of other hooks
  • be open-sourced as an external library

Two conventions of React's built-in hooks:

  1. the naming convention puts the use prefix in front of every hook name
  2. the returned values are returned as an array (a state and a state updater function)
const useStorageState = (key, initialState) => {
  const [value, setValue] = React.useState(
    localStorage.getItem(key) || initialState
  );

  React.useEffect(() => {
    localStorage.setItem(key, value);
  }, [value, key]);

  return [value, setValue];
};

const App = () => {
  …
  const [searchTerm, setSearchTerm] = useStorageState('search', 'React');
  …
}

React Fragments (<></>)

All React components return JSX with one top-level HTML element (here, a <div>):

const Search = ({ search, onSearch }) => (
  <div>
    …
  </div>
);

Another approach (rarely used) returns all sibling elements as an array. Each list item has a mandatory key attribute:

const Search = ({ search, onSearch }) => [
  <label key="1" htmlFor="search">
    Search:{' '}
  </label>,
  <input
    key="2"
    id="search" type="text" value={search} onChange={onSearch} />,
];

Another solution is to use a React fragment:

const Search = ({ search, onSearch }) => (
  <React.Fragment>
    <label htmlFor="search">Search: </label>
    <input id="search" type="text" value={search} onChange={onSearch} />
  </React.Fragment>
);

Fragments let us group a list of children without adding extra nodes to the DOM.

A more popular alternative is using fragments in their shorthand version:

const Search = ({ search, onSearch }) => (
  <>
    <label htmlFor="search">Search: </label>
    <input id="search" type="text" value={search} onChange={onSearch} />
  </>
);

Reusable React Component

Every implementation detail of the Search component is tied to the search feature.

However, internally the component is only a label and an input, so why should it be tied so strictly to one domain?

Let's turn the specialized Search component into a more reusable InputWithLabel component:

const App = () => {
  …
  return (
    <div>
      …
      <InputWithLabel
        id="search"
        label="Search"
        value={searchTerm}
        onInputChange={handleSearch}
      />
      …
    </div>
  );
}

const InputWithLabel = ({id, label, value, type="text", onInputChange}) => (
  <>
    <label htmlFor={id}>{label}: </label>
    <input id={id} type={type} value={value} onChange={onInputChange} />
  </>
)

Reusable Components in React:

React Component Composition

Component composition is one of React's more powerful features.

We can use a React element in the same fashion as an HTML element by leveraging its opening and closing tag.

const App = () => {
  …
  return (
    <div>
      …
      <InputWithLabel
        id="search"
        value={searchTerm}
        onInputChange={handleSearch}
      >
        Search:
      </InputWithLabel>
      …
    </div>
  );
}

We inserted the text Search: between the component's element's tags.

In the InputWithLabel component, we have access to this information via React's children prop:

const InputWithLabel = ({id, value, type="text", onInputChange, children}) => (
  <>
    <label htmlFor={id}>{children}</label>
    <input id={id} type={type} value={value} onChange={onInputChange} />
  </>
)

Now the React component's elements behave similarly to native HTML.

Everything that's passed between a component's elements can be accessed as children in the component and be rendered somewhere.

We can compose React components into each other by passing React elements via React children.

Access DOM API after rendering (Imperative React)

React is declarative.

However, it's not always possible to use the declarative approach, in cases such as these:

  • read/write access to elements via the DOM API
    • measuring (read) an element's width or height
    • setting (write) an input field's focus state
  • implementation of more complex animations
  • integration of third-party libraries (D3)

The DOM can be manipulated imperatively with React's useRef hook:

Imperative programming in React is verbose and counterintuitive.

Inline Handler in JSX

The application should render a button next to each list item which allows its users to remove the item from the list.

First solution:

  • To gain control over the list, make it stateful:

    const initialStories = [
      {title: 'React', …},
      {title: 'Redux', …},
    ];
    
    const App = () => {
      …
      const [stories, setStories] = React.useState(initialStories);
      …
    }
    
  • Write an event handler which removes an item from the list:

    const App = () => {
      …
      const handleRemoveStory = (item) => {
        const newStories = stories.filter(
          (story) => item.objectID !== story.objectID
        );
        setStories(newStories);
      };
      …
      return (
        <div>
          …
          <List list={searchedStories} onRemoveItem={handleRemoveStory} />
          …
        </div>
      );
    }
    
  • Use this new functionality in List/Item components to modify the state in the App component:

    const List = ({ list, onRemoveItem }) => (
      <ul>
        {list.map(item => (
          <Item
            key={item.objectID}
            item={item}
            onRemoveItem={onRemoveItem}
          />
        ))}
      </ul>
    )
    
    const Item = ({ item, onRemoveItem }) => {
      const handleRemoveItem = () => {
        onRemoveItem(item);
      };
      return (
        <li>
          <span>
            <a href={item.url}>{item.title}</a>
          </span>
          <span>{item.author}</span>
          <span>{item.num_comments}</span>
          <span>{item.points}</span>
          <span>
            <button type="button" onClick={handleRemoveItem}>Dismiss</button>
          </span>
        </li>
      );
    };
    

We had to introduce an additional handleRemoveItem handler in the Item component which is in charge of executing the incoming onRemoveItem callback handler.

To make this more elegant, we can use an inline handler which allows to execute the callback handler function in the Item component right in the JSX.

There are two solutions:

  1. using JavaScript's bind method (setting null as the value of this/context) :

    const Item = ({ item, onRemoveItem }) => (
      <li>
        …
        <span>
          <button type="button" onClick={onRemoveItem.bind(null, item)}>
            Dismiss
          </button>
        …
      </li>
    );
    
  2. using an inline arrow function (arrow functions do not have this, and if this is accessed, it is taken from the outside):

    const Item = ({ item, onRemoveItem }) => (
      <li>
        …
        <span>
          <button type="button" onClick={() => onRemoveItem(item)}>
            Dismiss
          </button>
        …
      </li>
    );
    

While using an inline handler is more concise than using a normal event handler, it can also be more difficult to debug because JavaScript logic may be hidden in JSX.

It's okay to use inline handlers if they do not obscure critical implementation details. If inline handlers need to use a block body, because there are more than one line of code executed, it's about time to extract them as normal event handlers.

Asynchronous Data

Simulate fetching stories asynchronously:

  1. a function (getAsyncStories)
    • returns a promise with data once it resolves
    • delays resolving of the promise for 2 seconds
  2. a useEffect hook (in the App component)
    • calls the function and resolves the returned promise as a side-effect
    • only runs once the component renders for the first time (due to the empty dependency array)
const getAsyncStories = () =>
  new Promise((resolve) =>
    setTimeout(
      () => resolve({ data: { stories: initialStories } }),
      2000
    )
);

const App = () => {
  …

  const [stories, setStories] = React.useState([]);

  React.useEffect(() => {
    getAsyncStories().then(result => {
      setStories(result.data.stories);
    });
  }, []);

  …
}

Conditional Rendering

A conditional rendering happens if we have to render different JSX based on information (e.g. state, props).

Using an if-else statement inlined in JSX is not encouraged though due to JSX's limitations.

We can use a ternary operator instead:

const App = () => {
  …
  const [isLoading, setIsLoading] = React.useState(false);

  React.useEffect(() => {
    setIsLoading(true);                 // True
    getAsyncStories().then(result => {
      setStories(result.data.stories);
      setIsLoading(false);              // False
    });
  }, []);
  …

  return (
    <div>
      …
      {isLoading ? (                    // Ternary operator
        <p>Loading ...</p>
      ) : (
        <List list={searchedStories} onRemoveItem={handleRemoveStory} />
      )}
      …
    </div>
  );
}

Implementation of error handling for the asynchronous data:

const App = () => {
  …
  const [isError, setIsError] = React.useState(false);

  React.useEffect(() => {
    setIsLoading(true);
    getAsyncStories().then(result => {
      setStories(result.data.stories);
      setIsLoading(false);
    })
    .catch(() => setIsError(true));         // Catch error
  }, []);
  …

  return (
    <div>
      …
      {isError && <p>Something went wrong ...</p>}
      …
    </div>
  );
}

We used expression && JSX which is more concise than using expression ? JSX : null.

In React, we can use this JavaScript behaviour to our advantage:

  • true && 'Hello World' always evaluates to 'Hello World'
  • false && 'Hello World' always evaluates to false (React ignores it and skips the expression)

Advanced State ( useReducer)

State management in this application makes heavy use of React's useState hook.

React's useReducer hook enables one to use more sophisticated state management.

To manage stories and its state transitions in a reducer, first, introduce a reducer function outside of components which:

  • receives as arguments:
    • a state
    • an action always associated with:
      • a type
      • a payload (as a best practice)
  • returns a new state
const storiesReducer = (state, action) => {
  switch (action.type) {
    case 'SET_STORIES':
      return action.payload;
    case 'REMOVE_STORY':
      return state.filter(
        (story) => action.payload.objectID !== story.objectID
      );
    default:
      throw new Error();
  }
};

In the App component, exchange useState for useReducer for managing the stories. The new hook:

  • receives as arguments:
    • a reducer function (storiesReducer)
    • an initial state ([])
  • returns an array with two items:
    • the current state (stories)
    • the state updater function or dispatch function (dispatchStories)
const App = () => {
  …
  const [stories, dispatchStories] = React.useReducer(
      storiesReducer,
      []
  );
  …
}

The dispatch function (dispatchStories) sets the state implicitly by dispatching an action for the reducer:

const App = () => {
  …
  React.useEffect(() => {
    setIsLoading(true);
    getAsyncStories().then(result => {
      dispatchStories({
        type: 'SET_STORIES',
        payload: result.data.stories,
      });
      setIsLoading(false);
    })
    .catch(() => setIsError(true));
  }, []);
  …
  const handleRemoveStory = (item) => {
    dispatchStories({
      type: 'REMOVE_STORY',
      payload: item,
    });
  };
  …
}

Guide about reducers in JavaScript.

Impossible States

Multiple useState with their multiple state updater functions and conditional states can lead to impossible states and undesired behavior in the UI.

We can improve our chances of not dealing with such bugs by moving states that belong together from multiple useState (and useReducer) hooks into a single useReducer hook.

useReducer come into play to manage domain related states.

Replace the following hooks…

const App = () => {
  …
  const [stories, dispatchStories] = React.useReducer(
      storiesReducer,
      []
  );
  const [isLoading, setIsLoading] = React.useState(false);
  const [isError, setIsError] = React.useState(false);
  …
}

… with:

const storiesReducer = (state, action) => {
  switch (action.type) {
    case 'STORIES_FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false,
      };
    case 'STORIES_FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'STORIES_FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    case 'REMOVE_STORY':
      return {
        ...state,
        data: state.data.filter(
        (story) => action.payload.objectID !== story.objectID
        ),
      };
    default:
      throw new Error();
  }
};

const App = () => {
  …
  const [stories, dispatchStories] = React.useReducer(
    storiesReducer,
    {data: [], isLoading: false, isError: false}
  );
  …
  React.useEffect(() => {
    dispatchStories({type: 'STORIES_FETCH_INIT'});
    getAsyncStories().then(result => {
      dispatchStories({
        type: 'STORIES_FETCH_SUCCESS',
        payload: result.data.stories,
      });
    })
    .catch(() =>
      dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
    );
  }, []);
  …
  const handleRemoveStory = (story) => {
    dispatchStories({
      type: 'REMOVE_STORY',
      payload: story,
    });
  };
  …
  const searchedStories = stories.data.filter((story) =>
    story.title.toLowerCase().includes(searchTerm.toLowerCase())
  );
  …
  return (
    <div>
      …
      {stories.isError && <p>Something went wrong ...</p>}

      {stories.isLoading ? (
        <p>Loading ...</p>
      ) : (
        <List list={searchedStories} onRemoveItem={handleRemoveStory} />
      )}
    </div>
  );
}

The state object managed by the reducer encapsulates everything related to the fetching of stories including loading and error states, but also implementation details like removing a story from the stories.

We moved one step closer towards more predictable state management.

Data Fetching

Fetch the data directly using the Fetch API instead of using initialStories array and getAsyncStories():

const API_ENDPOINT = 'https://hn.algolia.com/api/v1/search?query=';

const App = () => {
  …
  React.useEffect(() => {

    dispatchStories({type: 'STORIES_FETCH_INIT'});

    fetch(`${API_ENDPOINT}react`)
      .then((response) => response.json())
      .then((result) => {
        dispatchStories({
          type: 'STORIES_FETCH_SUCCESS',
          payload: result.hits,
        });
      })
     .catch(() =>
       dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
     );

  }, []);
  …
}

Data Re-Fetching

The search term react is hardcoded in the previous example but we want to fetch data related to the search term.

Delete searchedStories and replace it with stories.data:

const App = () => {
  …
  return (
    <div>
      …
      {stories.isLoading ? (
        <p>Loading ...</p>
      ) : (
        <List list={stories.data} onRemoveItem={handleRemoveStory} />
      )}
    </div>
  );
}

Then use the actual searchTerm from the component's state:

const App = () => {
  …
  React.useEffect(() => {
    if (!searchTerm) return;

    dispatchStories({type: 'STORIES_FETCH_INIT'});

    fetch(`${API_ENDPOINT}${searchTerm}`)
      .then((response) => response.json())
      .then((result) => {
        dispatchStories({
          type: 'STORIES_FETCH_SUCCESS',
          payload: result.hits,
        });
      })
     .catch(() =>
       dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
     );

  }, [searchTerm]);             // Run the side-effect when `searchTerm` changes.
  …
}

Re-fetching data each time someone types into the input field isn't optimal though.

Memoization (useMemo and useCallback)

React keeps the UI in sync with the application state via re-renders.

In general, re-renders aren't a big deal.

But, in certain situations, re-renders can lead to performance problems.

useMemo and useCallback help optimize re-renders by:

  1. reducing the amount of work that needs to be done in a given render
  2. reducing the number of times that a component needs to re-render

useMemo re-uses an already computed value instead of calculating it from scratch every time. It takes two arguments: a function and a list of dependecies. It's a cache, and the dependencies are the cache invalidation strategy.

useCallback is the same thing, but for functions instead of arrays/objects. It lets us cache a function definition between re-renders.

Example of the creation of a memoized function with useCallback:

  • move all the data fetching logic from the side-effect into a arrow function expression (A)
  • wrap this new function into React's useCallback hook (B)
  • invoke it in the useEffect hook (C)
const App = () => {
  …

  // A
  const handleFetchStories = React.useCallback(() => {  // B
    if (!searchTerm) return;
    dispatchStories({type: 'STORIES_FETCH_INIT'});
    fetch(`${API_ENDPOINT}${searchTerm}`)
      .then((response) => response.json())
      .then((result) => {
        dispatchStories({
          type: 'STORIES_FETCH_SUCCESS',
          payload: result.hits,
        });
      })
     .catch(() =>
       dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
     );
  }, [searchTerm]);          // E

  React.useEffect(() => {
    handleFetchStories();    // C
  }, [handleFetchStories]);  // D
  …
}
  • useCallback creates a memoized function every time its dependency array (E) changes
  • as a result, the useEffect hook runs again (C)
  • because it depends on the new function (D)
  • visualization:

    1. change: searchTerm (cause: user interaction)
    2. change: handleFetchStories
    3. run: side-effect
    

useCallback hook changes the function only when one of its values in the dependency array changes. That's when we want to trigger a re-fetch of the data, because the input field has new input and we want to see the new data displayed in our list.

Understanding useMemo and useCallback.

Explicit Data Fetching

Move from implicit to explicit data fetching to avoid re-fetching all data each time someone types in the input field.

We need:

  • a new <button> element which confirms the search and executes the data request
  • two handlers for
    • the input field to set searchTerm
    • the button to set url (derived from the current searchTerm)
  • a new stateful url used whenever a user clicks the button
const App = () => {

  const [searchTerm, setSearchTerm] = useStorageState('search', 'React');

  const [url, setUrl] = React.useState(
    `${API_ENDPOINT}${searchTerm}`
  );
  …

  const handleFetchStories = React.useCallback(() => {
    dispatchStories({type: 'STORIES_FETCH_INIT'});
    fetch(url)
      .then((response) => response.json())
        …
     );
  }, [url]);
  …

  const handleSearchInput = (event) => {
    setSearchTerm(event.target.value);
  };

  const handleSearchSubmit = () => {
    setUrl(`${API_ENDPOINT}${searchTerm}`);
  };

  return (
    <div>
      …
      <InputWithLabel
        id="search"
        value={searchTerm}
        isFocused
        onInputChange={handleSearchInput}
      >
        <strong>Search: </strong>
      </InputWithLabel>

      <button
        type="button"
        disabled={!searchTerm}
        onClick={handleSearchSubmit}
      >
        Submit
      </button>
      …
    </div>
  );
}

Third-Party Libraries

Install axios to replace the fetch API:

npm install axios

Import axios in the component's file:

import * as React from 'react';
import axios from 'axios';

Use axios:

const App = () => {
  …
  const handleFetchStories = React.useCallback(() => {
    dispatchStories({type: 'STORIES_FETCH_INIT'});
    axios
      .get(url)
      .then((result) => {
        dispatchStories({
          type: 'STORIES_FETCH_SUCCESS',
          payload: result.data.hits,
        });
      })
     .catch(() =>
       dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
     );
  }, [url]);
  …
}

Async/Await

The function in useCallback requires the async keyword.

Then everything reads like synchronous code.

Actions after the await keyword are not executed until the promise resolves.

const App = () => {
  …
  const handleFetchStories = React.useCallback(async () => {
    dispatchStories({type: 'STORIES_FETCH_INIT'});
    try {
      const result = await axios.get(url);
      dispatchStories({
        type: 'STORIES_FETCH_SUCCESS',
        payload: result.data.hits,
      });
    } catch {
      dispatchStories({ type: 'STORIES_FETCH_FAILURE' });
    }
  }, [url]);
  …
}

How to fetch data with React Hooks.

Forms

A form is the proper vehicle to submit data via a button from various input controls:

  • the handleSearchSubmit() is used in the new form element's onSubmit attribute.
  • the button receives a new type=submit attribute
    • it indicates that the form element's onSubmit handles the click and not the button
  • handleSearchSubmit() executes preventDefault()
    • it prevents HTML form's native behavior which would lead to a browser reload
const App = () => {
  …
  const handleSearchSubmit = (event) => {
    setUrl(`${API_ENDPOINT}${searchTerm}`);
    event.preventDefault();
  };
  …
  return (
    <div>
      …
      <form onSubmit={handleSearchSubmit}>

        <InputWithLabel
          id="search"
          value={searchTerm}
          isFocused
          onInputChange={handleSearchInput}
        >
          <strong>Search: </strong>
        </InputWithLabel>

        <button type="submit" disabled={!searchTerm}>
          Submit
        </button>

      </form>
      …
    </div>
  );
}

It could be extracted to a new SearchForm component.

Avant Quotes from Weaving the Web Après Performance in React

Tag Kemar Joint