My notes of the Udemy course 'Modern React with Redux'
- React
- Chrome extensions
- Tools
- CodeSandbox
- Codepen.io
- Babel
- Prettier VSCode formatter plugin (*)
- Fomantic UI (a.k.a. Semantic UI)
- Node Media Server
- JavaScript libraries
- Axios
- Faker (
basics
app) - Lodash (
blog
app) - Google API JavaScript client
- JSON Server (github)
- Redux / MobX
- Browser APIs
- Geolocation API (
seasons
app)
- Geolocation API (
- REST APIs
- {JSON}Placeholder (
blog
app) - Unsplash (
image-finder
app) - Google (
movie-player
app) - Google Translate (
widgets
app) - Wikipedia (
widgets
app)
- {JSON}Placeholder (
- Courses
- Modern React with Redux (Udemy)
- Manage Complex State in React Apps with MobX (egghead.io)
- React State Management (LinkedIn Learning)
- Courses (OAuth)
- The Nuts and Bolts of OAuth 2.0 (Udemy)
- OAuth and OpenID Connect (LinkedIn Learning)
(*) Open VSCode settings.json
('Open Settings (JSON)' command) and add the following line: "editor.defaultFormatter": "esbenp.prettier-vscode"
. Optionally, set 'Format on save' = true.
Throughout this course, the following React applications will be built.
Section | Application | Topics | Libs & tools | MobX? |
---|---|---|---|---|
3 | basics | functional components, props | Faker, Semantic UI | |
4-6 | seasons | class components, state, lifecycle | Geolocation API | |
7-10 | image-finder | forms, events, callbacks, refs, promises | Axios, Unsplash, grid | |
11 | movie-player | refs | Youtube API, grid | |
12 | widgets | hooks (useState, useEffect, useRef), API throttling | Wikipedia API, Google translate API | |
13 | navigation (custom) | |||
17 | songs | redux, actions, reducers | ||
18 | blog | redux thunk (asycn actions), middleware | ||
19 | API call caching, memoization | Lodash | ||
20-22 | streams | navigation (react router) | OAuth (Google API), Redux dev tools | |
23 | redux form | |||
24 | REST, path params (react router), browser history | JSON Server, Lodash | ||
25 | portals (modal popups) | |||
27 | context | context |
This course focuses on state management with standard React (state, context) and with Redux. For some app
s I have ceated a app-mobx
copy to illustrate what the app would look like when using MobX.
At the end of this README there's an appendix on MobX.
My system:
- Node v14.15.1
- npm 6.14.8
Each React application created during this course is generated using create-react-app
:
$ npx create-react-app myapp
$ cd myapp
$ npm start
Libraries included in each new React app:
- Babel (tool for converting JavaScript to an older, more widely supported version)
- Webpack
- Dev Server
After generating a new React app (with the default template) I follow these steps:
-
Add to
.gitignore
:.eslintcache
-
Remove everything from
./src
-
Remove everything from
./public
except./public/index.html
-
Fix
index.html
:- Delete references to
manifest.json
andfavicon.ico
- Add fomantic UI (a.k.a. semantic ui):
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.8.7/semantic.min.css" />
- Delete references to
A React component is a function or a class that produces HTML (JSX) and handles feedback (event handlers). Example of a function based component:
const App = () => {
return <div>Hi there!</div>;
};
JSX stands for JavaScript XML and is a React syntax extension to JavaScript.
Each React component is usually put in its own file. This file can have extension .js
or .jsx
. The transpiler will resolve the type of file contents. I prefer using .jsx
since JSX is not pure JavaScript. This makes it easier to distinguish React from JavaScript files.
With babeljs.io you can check how Babel translates JSX into JavaScript. The above component translates into
const App = () => {
return React.createElement("div", null, "Hi there!");
};
const App = () => {
const buttonText = "Click me!";
return <button>{buttonText}</button>;
};
<div style='background-color: red; border: 1px solid red'></div>
becomes
<div style={{ backgroundColor: "red", border: "1px solid red" }}></div>
<label class='label' for='name'>
Name
</label>
becomes
<label className='label' for='name'>
Name
</label>
Props (properties) are used to pass information to a React component in order to customize / configure it.
You can use named properties:
const Paragraph = (props) => {
return <div>{props.content}</div>;
};
const App = () => {
return <Paragraph content='Quisquam sunt vero odio excepturi.' />;
};
Or nested components (children):
const ApprovalPanel = (props) => {
return <div className='content'>{props.children}</div>;
};
const App = () => {
return (
<div className='ui container comments'>
<ApprovalPanel>
<Comment />
</ApprovalPanel>
<ApprovalPanel>
<h3>Please confirm</h3>
<b>Are you sure?</b>
</ApprovalPanel>
</div>
);
};
Application: seasons
In the old days class components had much more capabilities than function components. Therefore, in older React projects, you will see more class components than function components.
With the introduction of Hooks, functional components now have the same capabilities:
Class components
- can produce JSX to show content to the user
- can use lifecycle events to run code at specific points in time
- can use state to update content on the screen
Function components
- can produce JSX to show content to the user
- can use Hooks to run code at specific points in time
- can use Hooks to access state & update content on the screen
In general, function components are good for simple content. Otherwise use class components. Also, Hooks are easier to understand when you have a good understanding of class components.
A class based component extends React.Component
and defines a render()
method that returns JSX:
import React from "react";
class App extends React.Component {
render() {
return <div>Hello!</div>;
}
}
Do not put initialization code inside the render()
method, as the render()
method is called very often!
Anywhere inside a class component you can lookup props using this.props.propName
.
State is a JavaScript object that contains data relevant to a class-based component.
- Updating the State causes the component to rerender.
- State must be initialized when a component is created.
- State can only be updated using the function
setState()
.
Example:
class App extends React.Component {
constructor(props) {
super(props);
// Initialize state
this.state = { latitude: "Unknown" };
window.navigator.geolocation.getCurrentPosition(
// Update state
(position) => this.setState({ latitude: position.coords.latitude }),
(err) => console.error(err.message)
);
}
render() {
// Use state
return <div>Your location: {this.state.latitude}</div>;
}
}
- constructor
- Initialize state
- render
- Return JSX
- Called many times
- No data loading!
- componentDidMount
- Initialization logic (like data loading)
- componentDidUpdate
- Called when state changes
- Example: Data loading based on user input
- componentWillUnmount
- Cleanup when component is removed from screen
- Freuqently used in combination with non-React libraries
We have already seen constructor()
and render()
. We can also add the other methods to our component class, for instance componentDidMount()
to implement initialization logic.
There are a few more (rarely used) lifecycle methods as described in the React.Component API Reference.
Example of state and lifecycle (a clock that updates itself every second):
class Clock extends React.Component {
state = { time: new Date().toLocaleTimeString() };
componentDidMount() {
setInterval(() => {
this.setState({ time: new Date().toLocaleTimeString() });
}, 1000);
}
render() {
return <div className='time'>The time is: {this.state.time}</div>;
}
}
Application: image-finder
For an uncontrolled element only the HTML DOM knows the value of the element:
class SearchBar extends React.Component {
onInputChange(event) {
console.log(event.target.value);
// Do stuff...
}
render() {
return (
<form className='ui form'>
<input type='text' name='search-term' onChange={this.onInputChange} />
</form>
);
}
}
A controlled element is backed by the component's state:
class SearchBar extends React.Component {
state = { term: "" };
render() {
return (
<form className='ui form'>
<input
type='text'
name='search-term'
value={this.state.term}
onChange={(e) => this.setState({ term: e.target.value })}
/>
</form>
);
}
}
The source of thruth for controlled elements is the State and not the HTML DOM. The React application is storing all the data; the purpose of the DOM is presentation only!
This code will result in an error on form submit (TypeError: Cannot read property 'state' of undefined
):
class SearchBar extends React.Component {
state = { term: "" };
onFormSubmit(event) {
event.preventDefault();
console.log(this.state.term);
}
render() {
return <form onSubmit={this.onFormSubmit}>// Input elements using state...</form>;
}
}
class SearchBar extends React.Component {
state = { term: "" };
constructor() {
super();
this.onFormSubmit = this.onFormSubmit.bind(this);
}
onFormSubmit(event) {
event.preventDefault();
console.log(this.state.term);
}
render() {
return <form onSubmit={this.onFormSubmit}>// Input elements using state...</form>;
}
}
class SearchBar extends React.Component {
state = { term: "" };
onFormSubmit = (event) => {
event.preventDefault();
console.log(this.state.term);
};
render() {
return <form onSubmit={this.onFormSubmit}>// Input elements using state...</form>;
}
}
class SearchBar extends React.Component {
state = { term: "" };
onFormSubmit(event) {
event.preventDefault();
console.log(this.state.term);
}
render() {
return <form onSubmit={(e) => this.onFormSubmit(e)}>// Input elements using state...</form>;
}
}
We start with a comparison of the three most popular techniques for making API requests from a browser: XHR, Fetch and Axios.
- Low level built-in browser object.
- Since 1999.
- Low level built-in browser object.
- Modern replacement for XHR.
- More advanced than XHR (supports concurrent requests, promise-based requests).
- Third party library.
- Good for more advanced use cases. For basic use cases, fetch will do.
- Comparison of fetch vs. Axios.
In our projects we will be using Axios, and we use the free Unsplash API for searching photos.
const numbers = [0, 1, 2, 3, 4];
numbers.map((n) => n * 2);
Example of a React component that renders a list of photos from the Unsplash API:
import React from "react";
const ImageList = (props) => {
const images = props.images.map(({ id, description, urls }) => {
return (
<div key={id}>
<img src={urls.thumb} alt={description} />
</div>
);
});
return <div>{images}</div>;
};
export default ImageList;
Refs provide a way to access DOM nodes or React elements created in the render method.
- Managing focus or media playback
- Integrating with 3rd party libraries
We use Refs in our image-finder
app to get hold of <img>
DOM elements to calculate the height of an image.
class ImageCard extends React.Component {
constructor(props) {
super(props);
this.state = { spans: 0 };
// Initialize Ref
this.imageRef = React.createRef();
}
componentDidMount() {
// Add behavior to Ref
this.imageRef.current.addEventListener("load", this.setSpans);
}
setSpans = () => {
// Use Ref
const height = this.imageRef.current.clientHeight;
const spans = Math.ceil(height / 10);
this.setState({ spans: spans });
};
render() {
const { description, urls } = this.props.image;
// Bind Ref to DOM element
return (
<div style={{ gridRowEnd: `span ${this.state.spans}` }}>
<img ref={this.imageRef} alt={description} src={urls.regular}></img>
</div>
);
}
}
Application: movie-player
In the movie-player
application we bring together all the techniques learned so far:
- Functional components vs. Class-based components
- Props
- State
- Lifecycle methods
- HTTP Requests (Axios)
- Styling (Fomantic UI)
Application: widgets
Hooks allow you to add extra capabilities to functional components.
useState
lets you use stateuseEffect
somehow mimic lifecycleuseRef
lets you create a Ref
Hooks help you write reusable code. Other hooks built in to React:
useContext
useReducer
useCallback
useMemo
useImperativeHandle
useLayoutEffect
useDebugValue
The widgets
application is built with functional components and uses hooks to add state, lifecycle methods & other advanced stuff.
Allows you to define a container and a setter a 'piece of state'. In this example we use useState
to store the active element of an accordion:
import React, { useState } from "react";
const Accordion = (props) => {
// Initialize state (holder & setter)
const [activeIndex, setActiveIndex] = useState(0);
const onTitleClick = (index) => {
// Update state
setActiveIndex(index);
};
const renderedItems = props.items.map((item, index) => {
// Use state
const activeStyle = index === activeIndex ? "active" : "";
return (
<React.Fragment key={item.title}>
<div className={`title ${activeStyle}`} onClick={() => onTitleClick(index)}>
<i className='dropdown icon'></i>
{item.title}
</div>
<div className={`content ${activeStyle}`}>
<p>{item.description}</p>
</div>
</React.Fragment>
);
});
return <div className='ui styled accordion'>{renderedItems}</div>;
};
Widget: widgets/Search
The useEffect
hook mimics lifecycle and can be configured to run some code when:
- component renders for the first time
useEffect(() => { ... }, []);
- component renders or re-renders
useEffect(() => { ... });
- component renders or re-renders and some piece of data (state) has changed
useEffect(() => { ... }, [state]);
It basically serves as an onChange event handler for state.
The function provided to useEffect
can return an optional cleanup function. This cleanup function will be executed
- on the next execution of
useEffect
- when the component is removed from the DOM
Example of a Wikipedia search widget that calls the Wikipedia API with a search 'term' 500ms after the last keystroke:
import React, { useState, useEffect } from "react";
import Wikipedia from "../api/Wikipedia";
const Search = () => {
const [term, setTerm] = useState("");
const [items, setItems] = useState([]);
useEffect(() => {
if (term) {
const timeoutId = setTimeout(() => {
Wikipedia.search(term)
.catch((err) => console.log(err))
.then((response) => setItems(response.data.query.search));
}, 500);
return () => {
clearTimeout(timeoutId);
};
}
}, [term]);
const renderedItems = items.map((item) => {
return (
<div key={item.pageid}>
<div>{item.title}</div>
<p>{item.snippet}</p>
</div>
);
});
return (
<div>
<input value={term} onChange={(evt) => setTerm(evt.target.value)} />
<div>{renderedItems}</div>
</div>
);
};
The previous example shows how to throttle API calls: We don't call the API until the user has paused typing for 500ms.
Remaining problem: If the user has searched for cow
then types s
followed by Backspace
the API will be called with the unchanged search term cow
.
To prevent unnecessary API calls we use a technique called debouncing.
- useDebounce on useHooks.com
- How to throttle or debounce (Stackoverflow)
- Sections 169 (Fixing a warning) and 193 (Deboucing translation updates) of the course.
The idea is to use a second piece of state (debouncedTerm) and use two useEffect
hooks: one for term
(which sets a timer to update the debouncedTerm after T millis) and one for debouncedTerm
(which does the API call if debouncedTerm has changed). Like this:
import React, { useState, useEffect } from "react";
import Wikipedia from "../api/Wikipedia";
const Search = () => {
const [term, setTerm] = useState("");
const [debouncedTerm, setDebouncedTerm] = useState(term);
const [items, setItems] = useState([]);
// Copy term to debouncedTerm after 500m
useEffect(() => {
if (term) {
const timeoutId = setTimeout(() => {
setDebouncedTerm(term);
}, 500);
return () => {
clearTimeout(timeoutId);
};
}
}, [term]);
// Call API as soon as debouncedTerm has changed
useEffect(() => {
if (debouncedTerm) {
Wikipedia.search(debouncedTerm)
.catch((err) => console.log(err))
.then((response) => setItems(response.data.query.search));
}
}, [debouncedTerm]);
const renderedItems = items.map((item) => {
return (
<div key={item.pageid}>
<div>{item.title}</div>
<p>{item.snippet}</p>
</div>
);
});
return (
<div>
<input value={term} onChange={(evt) => setTerm(evt.target.value)} />
<div>{renderedItems}</div>
</div>
);
};
If you reference a piece of state inside useEffect
that is not included in the state array passed into useEffect
, React will give this warning: React Hook useEffect has a missing dependency.
Section 12 (Hooks) Video 169 (Optional Video: Fixing a Warning) gives a solution.
Widget: widgets/Dropdown
The useRef
hook gives you a reference to a DOM element, similar to React.createRef()
for class-based components (section 10).
If multiple elements in the DOM hierarchy have an event handler defined for the same event, they will all be invoked, in the following order:
- by default propagated from the innermost to the outermost element
- but event handlers defined with
addEventListener()
are invoked first
This means that, depending on how you've set up your DOM, events are not necessarilly propagated linearly.
Navigation can be implemented
- from scratch
- with the
React Router
library
In the widgets
application we build navigation from scratch. A later application will use React Router (Section 20).
In this section the class-based components of the movie-player
application are refactored to functional components using hooks.
Custom hooks are (besides components) one of the best ways to create reusable code in React. Take a look at the videos (210-213) in this section to find out about custom hooks.
A custom hook always makes use of one or more standard React hook.
Examples:
- Vercel - Deploy to Vercel from the command line using the Vercel CLI
- Netlify - Automatically re-deploys on github commits
- AWS S3:
npm run build
- upload the contents of the
build
folder to S3 - open
BUCKET/index.html
in your browser
Redux is a state container for JavaScript.
Standard React state (this.state
and useState
) is bound to a single component. If you want to share this state across components, you will have to pass it around using things like props
.
If your application grows and you have a lot of state that must be shared across components, you can use a centralized state manager like Redux
.
MobX or Redux: Which is Better For React State Management?
Run the following snippet on codepen.io:
// Action creators
const createPolicy = (name, amount) => {
return {
type: "CREATE_POLICY",
payload: { name: name, amount: amount }
};
};
const deletePolicy = (name) => {
return {
type: "DELETE_POLICY",
payload: { name: name }
};
};
const createClaim = (name, amount) => {
return {
type: "CREATE_CLAIM",
payload: { name: name, amountClaimed: amount }
};
};
// Reducers
const claimsReducer = (claims = [], action) => {
if (action.type === "CREATE_CLAIM") {
return [...claims, action.payload];
}
return claims;
};
const accountingReducer = (account = { balance: 0 }, action) => {
if (action.type === "CREATE_POLICY") {
return { balance: account.balance + action.payload.amount };
} else if (action.type === "CREATE_CLAIM") {
return { balance: account.balance - action.payload.amountClaimed };
}
return account;
};
const policiesReducer = (policyHolders = [], action) => {
if (action.type === "CREATE_POLICY") {
return [...policyHolders, action.payload.name];
} else if (action.type === "DELETE_POLICY") {
return policyHolders.filter((name) => name !== action.payload.name);
}
return policyHolders;
};
// Redux in action
const { createStore, combineReducers } = Redux;
// Following structure reflects Redux' state object
const departments = combineReducers({
account: accountingReducer,
claims: claimsReducer,
policies: policiesReducer
});
const store = createStore(departments);
store.dispatch(createPolicy("Alex", 25));
store.dispatch(createPolicy("Bob", 25));
store.dispatch(createPolicy("Chris", 25));
store.dispatch(createPolicy("Drew", 25));
store.dispatch(createClaim("Chris", 40));
store.dispatch(deletePolicy("Bob"));
console.log(store.getState());
State can only be modified through the dispatcher. You can not modify state directly.
- Action Creator
- Action
- dispatch
- Reducers
- State
Do not modify existing data structures inside reducers. Always return new ones.
For tips & tricks how to modify state in reducers, see the _doc folder, and the JavaScript appendix at the end of this README.
originalItems.push(newItem);
return originalItems; // BAD
return [...originalItems, newItem]; // GOOD
Application: songs
Below is an example of a very simple React application with just one component (SongList) that integrates with Redux using Provider
and connect()
from the react-redux
library:
./redux/reducers/index.js
import { combineReducers } from "redux";
const songs = [
{ title: "Ninjago Overtue", duration: "4:05" },
{ title: "The Weekend Whip", duration: "8:54" },
{ title: "The Croc Swamp", duration: "7:35" },
{ title: "Hills of Chima", duration: "3:33" }
];
// Reducers are responsible for managing state (based on actions).
// In this example we have just one reducer that returns a static list.
const songsReducer = () => {
return songs;
};
export default combineReducers({ songs: songsReducer });
./App.jsx
import React from "react";
import SongList from "./components/SongList";
const App = () => {
return (
<div>
<h2>Songs</h2>
<SongList />
</div>
);
};
export default App;
./components/SongList.jsx
import React from "react";
import { connect } from "react-redux";
class SongList extends React.Component {
renderSongs = (songs) => {
return songs.map((song) => {
return <div key={song.title}>{song.title}</div>;
});
};
render() {
return <div className='ui divided list'>{this.renderSongs(this.props.songs)}</div>;
}
}
// Boiler plate code: Map state to props (named 'mapStateToProps' by convention)
const mapStateToProps = (state) => {
return {
songs: state.songs
};
};
// Boiler plate code: Connect component to Redux
export default connect(mapStateToProps)(SongList);
./index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import reducers from "./redux/reducers";
import App from "./App";
ReactDOM.render(
{/* Provider manages the React/Redux integration */}
<Provider store={createStore(reducers)}>
<App />
</Provider>,
document.querySelector("#root")
);
Application: blog
With a plain basic Redux store, you can only do simple synchronous updates by dispatching an action. The redux-thunk middleware extends the store's abilities, and allows you to write action creators that perform async logic (like API calls).
Action Creators are responsible for making API requests. Do not make API requests from components or reducers. Action Creators are typically called from lifecycle methods like componentDidMount
and componentDidUpdate
and they are typically performed asynchronously.
You would expect an asynchronous API call to be implemented something like this:
import typicode from "../api/Typicode";
export const loadPosts = async () => {
const response = await typicode.get("/posts");
return {
type: "POSTS_UPDATED",
payload: response.data
};
};
But when React executes the above action creator it will result in this error message:
Error: Actions must be plain objects. Use custom middleware for async actions.
.
It may look like the action creator is returning a plain object, but it is not. On babeljs.io you will find that the above simple action creator is transpiled into es2015
that in fact returns the request returned by the axios.get()
, i.e. a promise!
Redux detects that this is a promise and not a plain object, and refuses to store.dispatch()
it, hence the error.
Redux-Thunk is an example of Redux Middleware. Middleware sits between the dispatcher and the reducers.
- It gets called with every action we dispatch
- It can stop or modify an action, or do with it whatever it wants
- logging or crash reporting
- dealing with async actions
In standard Redux an action creator must return an action object.
With Redux-Thunk an action creator may also return a function. This function is called by Redux-Thunk with arguments
function(dispatcher, getState)
.
If Redux-Thunk receives
- an object, then it simply passes it on to the reducers.
- a function, then it calls that function with arguments
function(dispatch, getState)
The function does what it needs to do (e.g. an async API call) and chooses if and when to dispatch the resulting object (or again a function).
Register the Redux-Thunk middleware (irrelevant imports left out):
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const store = createStore(reducers, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
The above action creator can now be rewritten to the following:
import typicode from "../api/Typicode";
export const loadPosts = () => {
return async (dispatch, getState) => {
const response = await typicode.get("/posts");
dispatch({ type: "POSTS_UPDATED", payload: response.data });
};
};
- Never return undefined
- Produce state using only previous state and the action
- Should not mutate the input state (best practice; see video 265 ''A misleading rule')
- Lodash can be helpful in returning new state without mutating the input state.
The signature of a reducer: myReducer(state, action)
.
Redux calls the reducer
- 1st time:
myReducer(undefined, action_1)
- next times:
myReducer(previousState, action_2..)
where previousState is the value returned by the previous call to myReducer
.
const postsReducer = (posts = [], action) => {
if (action.type === "POSTS_UPDATED") {
return action.payload;
}
return posts;
};
Redux' state
is a map from reducers (keys) to their state (value). In other words it is the combination of the outputs of all reducers.
In the source code of combineReducers you will see that Redux will only tell React to re-render if one or more reducers has changed their state.
If a Reducer changes the original state object (originalState.someProp = 'newValue'
) then this change will go undetected (even if the store was actually updated). If all reducers do this, no re-render will take place!
This is why, as a best practice, reducers shouldn't mutate input state.
The blog
application displays a list of 100 blog posts and loads the user details for the author of each of the posts. There are 10 different authors each of which has written 10 blog posts.
You could use an action creator like this:
const loadUser = (id) => {
return async (dispatch, getState) => {
console.log(`loadUser(${id})`);
const response = await typicode.get(`/users/${id}`);
dispatch({ type: "LOAD_USER", payload: response.data });
};
};
but this will result in each user being looked up 10 times. We need to 'memoize' our function calls. Lodash has a _.memoize()
function for this purpose. But simply memoizing loaduser or the inner async function will not work (there will still be 10 requests per user).
The correct way to memoize an asynchronous action creator with Lodash is (see video 278 (Memoization issues)):
import _ from "lodash";
export const loadUser = (id) => {
return (dispatch, getState) => _loadUser(id, dispatch);
};
const _loadUser = _.memoize(async (id, dispatch) => {
console.log(`loadUser(${id})`);
const response = await typicode.get(`/users/${id}`);
dispatch({ type: "LOAD_USER", payload: response.data });
});
NB: There is no way to invalidate these cached function calls. If you want to reload a user (for instance because it was updated) you can't.
An alternative solution to prevent duplicate API calls is to combine the loadPosts
and loadUser
action creators into one (see blog
application):
export const loadPostsAndUsers = () => {
return async (dispatch, getState) => {
await dispatch(loadPosts());
const userIds = _.uniq(_.map(getState().posts, "userId"));
userIds.forEach((id) => dispatch(loadUser(id)));
};
};
Instead of passing in the userId to the UserHeader component, and make each component responsible for loading its own data, the list component now triggers the loading of all the different users and passes the complete user object on to the child components (UserHeader).
Application: streams
Features:
- React Router (section 20)
- Authentication (OAuth) (section 21)
- Forms (section 23)
- CRUD (section 24)
- Error handling
ReactRouter comes with 3 types of routers. Which one to use depends on your deployment situation (what web server you use and how you can/want to configure it).
- BrowserRouter
- path
/page1
=> urlhttps://host:port/page1
- configure your web server to route all unknowns to
index.html
.
- path
- HashRouter
- path
/page1
=> urlhttps://host:port/#/page1
- configure your web server to ignore everything behind
#
.
- path
- MemoryRouter
- path
/page1
=> urlhttps://host:port
- URL is constant. Easy to deploy but no way to bookmark parts of your application.
- path
A React application is typically a single page application that is served by /index.html
. Web servers automatically serve index.html
if you request /
. But what happens if you request a resource /page2
that the server doesn't know of (NB: the React Router path /page2
only has meaning inside the React application, which runs inside the browser)?
- Traditional web server will return HTTP 404 by default.
- React Dev web server is configured to return
/index.html
.
In other words, your bookmarks for specific pages inside your React app won't work, unless you configure your web server so that it routes those paths to /index.html
.
OAuth 2.0 allows us to use Google authentication to let users login to our application.
Scope is a mechanism in OAuth 2.0 to limit an application's access to a user's account. An application can request one or more scopes, this information is then presented to the user in the consent screen, and the access token issued to the application will be limited to the scopes granted.
We are going to use the Google API to authenticate our users.
In our Streams application we are only interested in the user's email address. So we will choose the 'profile' or 'email' scope from the OAuth 2.0 Scopes for Google APIs.
You can use OAuth on the server or in the client. In our case we use OAuth for JS Browser Apps.
Setup an OAuth 2.0 project in the Google API Console.
- Using OAuth 2.0 to Access Google APIs for JavaScript web apps
- API Reference
- Github
- Google Authentication (login status stored in component state)
- Google Authentication with Redux
Redux DevTools Chrome extension
Save data in the Redux store between page refreshes:
https://localhost:3000/?debug_session=SOME_KEY
By changing the session id in the URL you can even maintain multiple sessions (i.e. multiple Redux stores) at the same time.
When using React with Redux you have to write a lot of boiler plate code:
- mapStateToProps to use data from the Redux store in your React components
- Action creators and reducers to store input data from your React components in the Redux store
When it comes to form elements, Redux Form simplifies this by providing standard mappers, action creators and reducers to automatically map data between the Redux store and form components.
See this commit for an example.
With JSON Server (egghead.io) you can run a fake REST API.
npm init
npm install json-server
Create a db.json
file containing your test data. Every object in JSON Server must have an id
property. For instance
"streams": [
{
"id": "1",
"userId": 103021620253644174463,
"name": "Alex",
"email": "alex@company.com"
},
{
"id": "2",
"userId": 103021620253644174463,
"name": "Bob",
"email": "bob@company.co.uk"
}
]
}
And add a start
script to package.json
that starts the server on port 3001 (and watches db.json
for changes):
"scripts": {
"start": "json-server -p 3001 -w db.json"
}
We will connect our React front-end to JSON Server.
An example of intentional navigation with React Router:
import { Link } from "react-router-dom";
renderMyButton() {
return (
<div>
<Link to='/post/new'>
Create new Post
</Link>
</div>
);
}
Sometimes we would like to navigate the user away only after an action has successfully completed. In that case the action creator is responsible for the navigation.
This is harder than intentional navigation because action creators are not part of any Route. The BrowserRoute
maintains a History object that tracks the routes visited by the user. Action creators do not have access to this History object.
We solve this by using a Router
instead of a BrowserRouter
and providing it with our own history object:
history.js:
import { createBrowserHistory } from "history";
export default createBrowserHistory();
App.js:
import React from "react";
import { Router, Route } from "react-router-dom";
import history from "../history";
class App extends React.Component {
render() {
return (
<div>
<Router history={history}>...</Router>
</div>
);
}
}
export default App;
actions/index.js
import history from "../history";
export const myAction = (input) => async (dispatch, getState) => {
const output = doYourThingWith(input);
dispatch({ type: DO_THING, payload: output });
history.push("/");
};
React Router makes all path parameters available on the match.params
property:
<Router history={history}>
<div>
<Route path='/streams/show/:streamId' exact component={StreamShow} />
</div>
</Router>
class Stream extends React.Component {
render() {
const streamId = this.props.match.params.streamId;
return <div>Stream {streamId}</div>;
}
}
A modal window is a screen element users must interact with before they can return to the main application.
Getting a modal to display on top of all the other elements in an HTML page can be a challenge (tweaking positioning and z-index etc.). In particular in a React application where components are deeply nested inside a structure like body > div#root > Provider > App > Components ..
. The more deeply nested the modal, the harder to get it rendered on top of everything else.
In React we can use a Portal
to create a modal element. A portal is a way to render children outside the hierarchy of the parent component (for instance the body).
Below an example of a Modal component styled with Fomantic UI. The onClick
events enable the user to click outside the modal to close it.
index.html:
<body>
<div id="root"></div>
<div id="modal"></div>
</body>
Modal.jsx:
import React from "react";
import ReactDOM from "react-dom";
import history from "../history";
const Modal = ({ header, content }) => {
return ReactDOM.createPortal(
<div onClick={() => history.goBack()} className='ui dimmer modals visible active'>
<div onClick={(e) => e.stopPropagation()} className='ui standard modal visible active'>
<div className='header'>{header}</div>
<div className='content'>
<p>{content}</p>
</div>
<div className='actions'>
<button className='ui button negative'>OK</button>
<button className='ui button'>Cancel</button>
</div>
</div>
</div>,
document.getElementById("modal")
);
};
export default Modal;
By default React Router renders all routes that match the given path. When you request path /streams/123
then component StreamShow
will render. But when you request /streams/new
then both StreamCreate
and StreamShow
will be rendered (StreamShow interprets new
as the stream id):
import { Router, Route } from "react-router-dom";
class App extends React.Component {
render() {
return (
<div>
<Router>
<Route path='/streams/new' exact component={StreamCreate} />
<Route path='/streams/edit/:streamId' exact component={StreamEdit} />
<Route path='/streams/:streamId' exact component={StreamShow} />
</Router>
</div>
);
}
}
By wrapping Route
s in a <Switch>
React Router will only render the first component that matches (so the order matters inside a switch):
import { Router, Route, Switch } from "react-router-dom";
class App extends React.Component {
render() {
return (
<div>
<Router>
<Switch>
<Route path='/streams/new' exact component={StreamCreate} />
<Route path='/streams/edit/:streamId' exact component={StreamEdit} />
<Route path='/streams/:streamId' exact component={StreamShow} />
</Switch>
</Router>
</div>
);
}
}
Application: context
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Creating a context with a default value:
import React from "react";
export default React.createContext("english");
The Context Provider
can update the Context and provides the Context to its children.
import LanguageContext from "../contexts/LanguageContext";
class App extends React.Component {
state = { language: "english" };
render() {
return (
<div>
<i className='us flag' onClick={() => this.setState("english")} />
<i className='nl flag' onClick={() => this.setState("dutch")} />
<LanguageContext.Provider value={this.state.language}>
<UserCreate />
</LanguageContext.Provider>
</div>
);
}
}
The Provider is actually a React component. The value you provide to the context can be anything (from state, props or a static value) of any type (from literals to complex objects).
You can use multiple Providers for the same Context. Each of the Providers stores its own value on the Context. A component using the Context will get its value from its closest Provider.
Any component can use a Context, even if it is not wrapped in (a) Provider(s). In which case it will always get the default value from the Context.
By defining a static contextType = SomeContext
you get access to this.context
.
import LanguageContext from "../contexts/LanguageContext";
class Field extends React.Component {
static contextType = LanguageContext;
render() {
const label = this.context === "dutch" ? "Naam" : "Name";
return (
<div>
<label>{label}</label>
<input />
</div>
);
}
}
Alternatively you can use a Consumer
:
import LanguageContext from "../contexts/LanguageContext";
class Button extends React.Component {
render() {
return (
<button>
<LanguageContext.Consumer>
{(value) => (value === "dutch" ? "Verstuur" : "Submit")}
</LanguageContext.Consumer>
</button>
);
}
}
With this Consumer
approach you could access multiple Contexts in your component (either nested or at the same level). With static contextType
you can always only access one this.context
.
The Problem with React's Context API
Redux | Context |
---|---|
Distributes data to various components | Distributes data to various components |
Centralized data store | |
Provides mechanism for changing data in the store (action creators, reducers) |
Even though the Context systems seems more limited than Redux, section 29 illustrates how you can replace Redux with Context by encapsulating data and business logic in a single custom Context component.
Contains some interesting patterns!
A named export (export const doSomething = () => { ... }
) is imported like this:
import { doSomething } from "actions";
A default export (default export doSomething;
) is imported like this:
import doSomething from "actions";
The spread syntax (...
) can be used to copy an array or object:
> const numbers = [1, 2, 3];
> const copy = [...numbers, 4];
> copy.push(5);
> console.log(numbers);
< [1, 2, 3]
> console.log(copy);
< [1, 2, 3, 4, 5]
With the property accessor you can change the value of a property of an object:
> const person = { name: 'John', age: 20 };
> person['name'] = 'Jane';
> console.log(person);
< {name: "Jane", age: 20}
Recall that state in Redux is (should be) immutable. Inside a reducer, if your state contains a list of persons, you should never update (or replace) single person in the list stored in the Redux store and return the original state object (no components will be re-rendered if Redux can't detect the state has changed). What you should do instead is create a copy of the list of persons, and apply the changes to that copy. Like this:
const newState = { ...state };
newState[action.payload.id] = action.payload;
return newState;
or, shorter:
return { ...state, [action.payload.id]: action.payload };
This last statement creates a copy of an object and replaces one of its values.
Create a copy of an object containing only specific properties:
import _ from "lodash";
_.pick(this.props.stream, "propA", "propB");
Documentation and instruction videos on Egghead.io make use of the ES.next decorators (@observable, @observer, ..).
Because decorators are not an ES standard yet, as of MobX version 6 it is recommended to use observable
/ makeObservable
/ makeAutoObservable
instead of decorators.
The MobX documentation focuses on functional components. It doesn't say much on class components. This works, but only for mobx-react
:
import { observer } from "mobx-react";
const App = observer(
class App extends React.Component {
render() {
return <ChildComponent prop1={this.props.state.value} />;
}
}
);
export default App;
With mobx-react-lite
you will see TypeError: Class constructor App cannot be invoked without 'new'.