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 (providing a development environment)
- 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
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 key
s, 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 by using the addEventListener()
method programmatically on a DOM node.
React's synthetic event is essentially a wrapper around the browser's native event.
In React, this function is called an (event) handler.
The 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 handlers, 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 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 hierarchystate
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:
- the first entry (
searchTerm
) represents the current state (read) - the second entry (
setSearchTerm
) is a function to update this state (a state updater function) (write)
- the first entry (
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
as container.
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 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.
These are all descendant components of the component which instantiates the state.
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
Even though spread and rest operators have the same syntax (three dots):
- the spread operator happens on the right side
- the rest operator happens 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>
)
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:
- the naming convention puts the
use
prefix in front of every hook name - the returned values are returned as an array (a
state
and astate 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} />
</>
)
- How to create a React Button
- How to create a React Radio Button
- How to create a React Checkbox
- How to create a React Dropdown
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
orheight
- setting (write) an input field's
focus
state
- measuring (read) an element's
- 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 theApp
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:
using JavaScript's
bind
method (settingnull
as the value ofthis
/context) :const Item = ({ item, onRemoveItem }) => ( <li> … <span> <button type="button" onClick={onRemoveItem.bind(null, item)}> Dismiss </button> … </li> );
using an inline arrow function (arrow functions do not have
this
, and ifthis
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:
- a function (
getAsyncStories
)- returns a promise with data once it resolves
- delays resolving of the promise for 2 seconds
- a
useEffect
hook (in theApp
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 tofalse
(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)
- a
- a
- 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 (
[]
)
- a reducer function (
- returns an array with two items:
- the current state (
stories
) - the state updater function or dispatch function (
dispatchStories
)
- the current state (
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:
- reducing the amount of work that needs to be done in a given render
- 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 currentsearchTerm
)
- the input field to set
- 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'sonSubmit
attribute. - the button receives a new
type=submit
attribute- it indicates that the form element's
onSubmit
handles the click and not the button
- it indicates that the form element's
handleSearchSubmit()
executespreventDefault()
- 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.