React: Difference between revisions
No edit summary |
|||
Line 19: | Line 19: | ||
This is because you have not bound the function in the class to this e.g. | This is because you have not bound the function in the class to this e.g. | ||
<syntaxhighlight lang="javascript" line="line"> | <syntaxhighlight lang="javascript" line="line"> | ||
this.onTitleChange = this.onTitleChange.bind(this); | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Revision as of 23:31, 2 June 2020
Quick Reference
Development Setup
A good resource for this can be found https://jscomplete.com/learn/1rd-reactful' by Samer Buna
A template for a project can be found https://github.com/jscomplete/rgs-template
Variables
const myVar = (
<h1>Love it</h1>
);
Common Errors
When using classes and you have forgotten to bind you get
Cannot read property 'setState' of undefined
This is because you have not bound the function in the class to this e.g.
this.onTitleChange = this.onTitleChange.bind(this);
Hooks
Hooks can only be used at the top level. You can set rules in your editor to ensure this.
UseState
Example of setting state
const InputElement = () => {
const [inputText, setInputText] = useState("")
return (
<input placeholder="some text" onChange={(event) => { setInputText(event.target.value) } } />
)
}
UseRef
You can get a reference from the DOM using UseRef
const imageRef = useRef(null)
console.info("Got here")
return (
<img
onMouseOver={ ()=>{ imageRef.current.src = secondaryImg} }
onMouseOut={ ()=>{ imageRef.current.src = primaryImg} }
src={primaryImg}
ref={imageRef}
/>
)
UseEffect
The first argument to useEffect is a function which executes when the component first mounts. The first argument of the return should be a function which remove the resources, if applicable, used as the first argument. e.g.
Add and remove scroll handler
useEffect( () = > {
window.addEventListener("scroll", scrollHandler)
return ( ()=> {
window.removeEventListener("scroll", scrollHandler)
})
})
The second argument is a array of values which, if changed, will cause the effect to be called again.
e.g. Maybe a checkbox value
const [checkboxValue, setCheckboxValue] = useState(false)
useEffect( () = > {
window.addEventListener("scroll", scrollHandler)
return ( ()=> {
window.removeEventListener("scroll", scrollHandler)
}, [checkboxValue])
})
The effect will be called each time the checkbox is clicked.
UseConfig
This allows passing of config to different pages.
Top Level, wraps a context around pages
import React from "react";
import Home from "./Home";
import Speakers from "./Speakers";
export const AppContext = React.createContext();
const pageToShow = pageName => {
if (pageName === "Home") return <Home />;
if (pageName === "Speakers") return <Speakers />;
return <div>Not Found</div>;
};
const configValue = {
showSpeakerSpeakingDays : true
}
const App = ({ pageName }) => {
return <AppContext.Provider value={configValue}>
<div>{pageToShow(pageName)}</div>
</AppContext.Provider>
};
export default App;
A the usage on the page.
import React, { useState, useEffect, useContext} from "react";
import { AppContext } from "./App"
const PageFunction = ({}) => {
const context = useContext(AppContext)
}
UseReducer
A reducer is defined as something which takes a previous state and function and returns a new state
(previousState, action) => newState
UseState is built on Reducer i.e.
const [speakerList, setSpeakerList] = useState([])
Is the same as
const [speakerList, setSpeakerList] = useReducer( (state,action) => action, [])
Is the same as
function speakerReducer(state, action) {
return action
}
const [speakerList, setSpeakerList] = useReducer( speakerReducer, [])
We can now make this work the same a the useState
function speakerReducer(state, action) {
switch(action.type) {
case "setSpeakerList" : {
return action.data;
}
default:
return state;
}
}
// So replaced setSpeakList, which is not returned with a dispatch name, we called it dispatch but we could have called derek
// const [speakerList, setSpeakerList] = useReducer( speakerReducer, [])
const [speakerList, dispatch] = useReducer( speakerReducer, [])
So the old usage was
setSpeakerList(mySpeakerList)
Using the reducer gives
dispatch( {
type: "setSpeakerList",
data: mySpeakerList
})
UseCallback
This caches a function. Given the function below,
const heartFavoriteHandler = (e, favoriteValue) => {
e.preventDefault();
const sessionId = parseInt(e.target.attributes["data-sessionid"].value);
dispatch({
type : favoriteValue === true ? "setFavourite" : "setUnfavourite",
sessionId
});
We can cache this using UseCallback
const heartFavoriteHandler = useCallback( (e, favoriteValue) => {
e.preventDefault();
const sessionId = parseInt(e.target.attributes["data-sessionid"].value);
dispatch({
type : favoriteValue === true ? "setFavourite" : "setUnfavourite",
sessionId
}, [] );
However for this to work you will need to wrap the content it is reloading in React.memo. In this case it was SpeakerDetail
<div className="card-deck">
{speakerListFiltered.map(
({ id, firstName, lastName, bio, favorite }) => {
return (
<SpeakerDetail
key={id}
id={id}
favorite={favorite}
onHeartFavoriteHandler={heartFavoriteHandler}
firstName={firstName}
lastName={lastName}
bio={bio}
/>
);
}
)}
</div>
so this gave
const SpeakerDetail = React.memo( ({
id,
firstName,
lastName,
favorite,
bio,
onHeartFavoriteHandler
}) => {
console.log(`SpeakerDetail:${id} ${firstName} ${lastName} ${favorite}`);
return (
<div className="card col-4 cardmin">
<ImageToggleOnScroll
className="card-img-top"
primaryImg={`/static/speakers/bw/Speaker-${id}.jpg`}
secondaryImg={`/static/speakers/Speaker-${id}.jpg`}
alt="{firstName} {lastName}"
/>
<div className="card-body">
<h4 className="card-title">
<button
data-sessionid={id}
const mySortedSpeakList = useMemo( ()=> speakerList
.filter(
({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
)
.sort(function(a, b) {
if (a.firstName < b.firstName) {
return -1;
}
if (a.firstName > b.firstName) {
return 1;
}
return 0;
}), [speakingSaturday,speakingSunday,speakerList]); className={favorite ? "heartredbutton" : "heartdarkbutton"}
onClick={e => {
onHeartFavoriteHandler(e, !favorite);
}}
/>
<span>
{firstName} {lastName}
</span>
</h4>
<span>{bio}</span>
</div>
</div>
);
});
UseMemo
Similarly you can cache a value using UseMemo. e.g.
const mySortedSpeakerList = useMemo( ()=> speakerList
.filter(
({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
)
.sort(function(a, b) {
if (a.firstName < b.firstName) {
return -1;
}
if (a.firstName > b.firstName) {
return 1;
}
return 0;
}), [speakingSaturday,speakingSunday,speakerList]);
The value will only change if one of the speakingSaturday,speakingSunday,speakerList changes
Custom Hook
An example of a reducer with a custom hook using axios
const dataFetchReducer = (state, action) => {
switch (action.type) {
case "FETCH_INIT":
return { ...state, isLoading: true, isError: false };
case "FETCH_SUCCESS":
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: action.payload
};
case "FETCH_FAILURE":
return {
...state,
isLoading: false,
hasErrored: true,
errorMessage: "Data Retrieve Failure"
};
case "REPLACE_DATA":
// The record passed (state.data) must have the attribute "id"
const newData = state.data.map(rec => {
return rec.id === action.replacerecord.id ? action.replacerecord : rec;
});
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: newData
};
default:
throw new Error();
}
};
const useAxiosFetch = (initialUrl, initialData) => {
const [url] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
hasErrored: false,
errorMessage: "",
data: initialData
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: "FETCH_INIT" });
try {
let result = await axios.get(url);
if (!didCancel) {
dispatch({ type: "FETCH_SUCCESS", payload: result.data });
}
} catch (err) {
if (!didCancel) {
dispatch({ type: "FETCH_FAILURE" });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const updateDataRecord = record => {
dispatch({
type: "REPLACE_DATA",
replacerecord: record
});
};
return { ...state, updateDataRecord };
};
Maps
// Only do this if items have no stable IDs
const todo = ["fix", "syntax", "highlighting"];
const todoItems = todos.map((todo, index) =>
<li key={index}>
{todo.text}
</li>
);
Component Example
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {name: "Frarthur"};
}
render() {
return <div></div>;
}
};
Passing Props to child
Parent.json
import React from 'react';
import ReactDOM from 'react-dom';
import { Child } from './Child';
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { name: 'Frarthur' };
}
render() {
return <Child name={this.state.name} />;
}
}
ReactDOM.render(<Parent />, document.getElementById('app'));
Child.json
import React from 'react';
export class Child extends React.Component {
render() {
return <h1>Hey, my name is {this.props.name}!</h1>;
}
}
Forms with Ref
You can capture import using the React.createRef() e.g.
class Form extends React.Component
{
userNameInput = React.createRef();
handleSubmit = (event) => {
event.preventDefault()
console.log(userNameInput.current.value)
}
render()
{
return (
<form onSubmit={this.handleSubmit} >
<input
type="text"
placeHolder="background text"
ref={this.userNameInput}
required
/>
/>
)
}
}
Forms without Ref
A Better way may be to use a state to hold the name.
class Form extends React.Component
{
state = { userName : ''}
render()
{
return (
<form onSubmit={this.handleSubmit} >
<input
type="text"
value={this.state.userName}
OnChange = {
event => {
this.setState( {userName: event.target.value} )
}
}
required
/>
/>
)
}
}
Binding a function
import React from 'react';
import ReactDOM from 'react-dom';
import { Video } from './Video';
import { Menu } from './Menu';
const VIDEOS = {
fast: 'https://s3.amazonaws.com/codecademy-content/courses/React/react_video-fast.mp4',
slow: 'https://s3.amazonaws.com/codecademy-content/courses/React/react_video-slow.mp4',
cute: 'https://s3.amazonaws.com/codecademy-content/courses/React/react_video-cute.mp4',
eek: 'https://s3.amazonaws.com/codecademy-content/courses/React/react_video-eek.mp4'
};
class App extends React.Component {
constructor(props) {
this.chooseVideo = this.chooseVideo.bind(this);
super(props);
this.state = {
src: VIDEOS.fast
};
}
chooseView(newView) {
this.setState({
src: VIDEOS[newVideo]
});
}
render() {
return (
<div>
<h1>Video Player</h1>
<Menu chooseVideo={this.chooseView()} />
<Video src={this.state.src} />
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
import React from 'react';
export class Menu extends React.Component {
constructor() {
super(props)
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
var text = e.target.value;
this.props.chooseView(text);
}
render() {
return (
<form OnClick={this.handleClick}>
<input type="radio" name="src" value="fast" /> fast
<input type="radio" name="src" value="slow" /> slow
<input type="radio" name="src" value="cute" /> cute
<input type="radio" name="src" value="eek" /> eek
</form>
);
}
}
import React from 'react';
export class Video extends React.Component {
render() {
return (
<div>
<video controls autostart autoPlay muted src={this.props.src} />
</div>
);
}
}
Inline Styling
React needs double quotes to inject style in JSX e.g.
import React from 'react';
import ReactDOM from 'react-dom';
const styleMe = <h1 style={{ background: 'lightblue', color: 'darkred' }}>Please style me! I am so bland!</h1>;
ReactDOM.render(
styleMe,
document.getElementById('app')
);
Another example
mport React from 'react';
import ReactDOM from 'react-dom';
const styles = {
background: 'lightblue',
color: 'darkred',
marginTop: '100px',
fontSize: '50px'
}
const styleMe = <h1 style={styles}>Please style me! I am so bland!</h1>;
ReactDOM.render(
styleMe,
document.getElementById('app')
);
You can write px styles without quotes e.g.
const styles = {
background: 'lightblue',
color: 'darkred',
marginTop: 100,
fontSize: 50
}
Stateless Classes
A component class written in the usual way:
export class MyComponentClass extends React.Component {
render() {
return <h1>Hello world</h1>;
}
}
// The same component class, written as a stateless functional component:
export const MyComponentClass = () => {
return <h1>Hello world</h1>;
}
You can pass the properties from the container class like this
import React from 'react';
export const GuineaPigs = (props) => {
let src = props.src;
return (
<div>
<h1>Cute Guinea Pigs</h1>
<img src={src} />
</div>
);
}
Prototypes
Add them at the bottom so people can see how to use the code
import React from 'react';
export class BestSeller extends React.Component {
render() {
return (
<li>
Title: <span>
{this.props.title}
</span><br />
Author: <span>
{this.props.author}
</span><br />
Weeks: <span>
{this.props.weeksOnList}
</span>
</li>
);
}
}
// This propTypes object should have
// one property for each expected prop:
BestSeller.propTypes = {
title: React.PropTypes.string.isRequired,
author: React.PropTypes.string.isRequired,
weeksOnList: React.PropTypes.number.isRequired,
};
And a render example
import React from 'react';
import ReactDOM from 'react-dom';
import { BestSeller } from './BestSeller';
export class BookList extends React.Component {
render() {
return (
<div>
<h1>Best Sellers</h1>
<div>
<ol>
<BestSeller
title="Glory and War Stuff for Dads"
author="Sir Eldrich Van Hoorsgaard"
weeksOnList={10} />
<BestSeller
title="The Crime Criminals!"
author="Brenda Sqrentun"
weeksOnList={2} />
<BestSeller
title="Subprime Lending For Punk Rockers"
author="Malcolm McLaren"
weeksOnList={600} />
</ol>
</div>
</div>
);
}
}
ReactDOM.render(<BookList />, document.getElementById('app'));
And for stateless functions
import React from 'react';
export class GuineaPigs extends React.Component {
render() {
let src = this.props.src;
return (
<div>
<h1>Cute Guinea Pigs</h1>
<img src={src} />
</div>
);
}
}
GuineaPigs.propTypes = {
src: React.PropTypes.string.isRequired
};
Form Example where we track the input value
import React from 'react';
import ReactDOM from 'react-dom';
export class Input extends React.Component {
constructor(props) {
super(props);
this.state = { userInput: '' };
this.handleUserInput = this.handleUserInput.bind(this);
}
handleUserInput(e) {
this.setState({userInput: e.target.value});
}
render() {
return (
<div>
<input type="text" onChange={this.handleUserInput} value={this.state.userInput} />
<h1>{this.state.userInput}</h1>
</div>
);
}
}
ReactDOM.render(
<Input />,
document.getElementById('app')
);
The New Way Example using custom hooks
This is the example from the react course where they broke the game into a custom hook
Display Elements
const StarsDisplay = props => (
<>
{utils.range(1, props.count).map(starId => (
<div key={starId} className="star" />
))}
</>
);
const PlayNumber = props => (
<button
className="number"
style={{backgroundColor: colors[props.status]}}
onClick={() => props.onClick(props.number, props.status)}
>
{props.number}
</button>
);
const PlayAgain = props => (
<div className="game-done">
<div
className="message"
style={{ color: props.gameStatus === 'lost' ? 'red' : 'green'}}
>
{props.gameStatus === 'lost' ? 'Game Over' : 'Nice'}
</div>
<button onClick={props.onClick}>Play Again</button>
</div>
);
Custom Hook
Here is the custom hook which abstracts the state and returns what is necessary for the display element of the game
const useGameState = timeLimit => {
const [stars, setStars] = useState(utils.random(1, 9));
const [availableNums, setAvailableNums] = useState(utils.range(1, 9));
const [candidateNums, setCandidateNums] = useState([]);
const [secondsLeft, setSecondsLeft] = useState(10);
useEffect(() => {
if (secondsLeft > 0 && availableNums.length > 0) {
const timerId = setTimeout(() => setSecondsLeft(secondsLeft - 1), 1000);
return () => clearTimeout(timerId);
}
});
const setGameState = (newCandidateNums) => {
if (utils.sum(newCandidateNums) !== stars) {
setCandidateNums(newCandidateNums);
} else {
const newAvailableNums = availableNums.filter(
n => !newCandidateNums.includes(n)
);
setStars(utils.randomSumIn(newAvailableNums, 9));
setAvailableNums(newAvailableNums);
setCandidateNums([]);
}
};
return { stars, availableNums, candidateNums, secondsLeft, setGameState };
};
The Game
const Game = props => {
const {
stars,
availableNums,
candidateNums,
secondsLeft,
setGameState,
} = useGameState();
const candidatesAreWrong = utils.sum(candidateNums) > stars;
const gameStatus = availableNums.length === 0
? 'won'
: secondsLeft === 0 ? 'lost' : 'active'
const numberStatus = number => {
if (!availableNums.includes(number)) {
return 'used';
}
if (candidateNums.includes(number)) {
return candidatesAreWrong ? 'wrong' : 'candidate';
}
return 'available';
};
const onNumberClick = (number, currentStatus) => {
if (currentStatus === 'used' || secondsLeft === 0) {
return;
}
const newCandidateNums =
currentStatus === 'available'
? candidateNums.concat(number)
: candidateNums.filter(cn => cn !== number);
setGameState(newCandidateNums);
};
return (
<div className="game">
<div className="help">
Pick 1 or more numbers that sum to the number of stars
</div>
<div className="body">
<div className="left">
{gameStatus !== 'active' ? (
<PlayAgain onClick={props.startNewGame} gameStatus={gameStatus} />
) : (
<StarsDisplay count={stars} />
)}
</div>
<div className="right">
{utils.range(1, 9).map(number => (
<PlayNumber
key={number}
status={numberStatus(number)}
number={number}
onClick={onNumberClick}
/>
))}
</div>
</div>
<div className="timer">Time Remaining: {secondsLeft}</div>
</div>
);
};
const StarMatch = () => {
const [gameId, setGameId] = useState(1);
return <Game key={gameId} startNewGame={() => setGameId(gameId + 1)}/>;
}
== Utilities ==
// Color Theme
const colors = {
available: 'lightgray',
used: 'lightgreen',
wrong: 'lightcoral',
candidate: 'deepskyblue',
};
// Math science
const utils = {
// Sum an array
sum: arr => arr.reduce((acc, curr) => acc + curr, 0),
// create an array of numbers between min and max (edges included)
range: (min, max) => Array.from({length: max - min + 1}, (_, i) => min + i),
// pick a random number between min and max (edges included)
random: (min, max) => min + Math.floor(Math.random() * (max - min + 1)),
// Given an array of numbers and a max...
// Pick a random sum (< max) from the set of all available sums in arr
randomSumIn: (arr, max) => {
const sets = [[]];
const sums = [];
for (let i = 0; i < arr.length; i++) {
for (let j = 0, len = sets.length; j < len; j++) {
const candidateSet = sets[j].concat(arr[i]);
const candidateSum = utils.sum(candidateSet);
if (candidateSum <= max) {
sets.push(candidateSet);
sums.push(candidateSum);
}
}
}
return sums[utils.random(0, sums.length - 1)];
},
};
ReactDOM.render(<StarMatch />, mountNode);
Type Script
This is another way to valid props
import * as React from "react";
interface Props {
name: string
}
function Greeting(props : Props) {
return (
<h1>Hello {props.name}</h1>
)
}
Lifecycle
A component “mounts” when it renders for the first time. This is when mounting lifecycle methods get called.
There are three mounting lifecycle methods:
- componentWillMount
- render
- componentDidMount
There are five updating lifecycle methods:
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
Some words of wisdom
A React component should use props to store information that can be changed, but can only be changed by a different component.
A React component should use state to store information that the component itself can change.