Table of Contents
Pure Functions
- Rules:
- No Side Effects
- Consistent Outputs
- Benefits:
- composable and reusable
- can be cached
- testable
- readable
- side effects: If a function alters a variable outside of its scope or relies on state thats not the input value it receives, it's said to have a side effect.
- We want to avoid these because they depend on context outside of their local environments which can lead to unexpected behavior.
- If a function doesn't return anything, it's unpredictable.
- It can also falsely lead you to believe that function isn't doing anything.
- An example of what inconsistent output looks like is shown below:
const friends = ["Ben", "Lynn", "Alex"]
friends.splice(0, 1) // ["Ben"]
friends.splice(0, 1) // ["Lynn"]
friends.splice(0, 1) // ["Alex"]
Components
- A react component is just a function that returns a description of a UI.
- It's important to follow naming conventions... CapitalCamelCase.
- This is essential so JSX knows its a React component.
- Components should follow the Single Responsibility Principle.
- A good rule of thumb in terms of having components in their own files is to only put a component in its own file if it is being reused.
Here is a basic example of an
Author
component and then seeing how we can use that component.
export default function Authors () {
return <aside>
<h2>Authors</h2>
<ul>
<li>Tyler McGinnis</li>
<li>Ben Adam</li>
<li>Lynn Fisher</li>
</ul>
</aside>
}
import Authors from './Authors'
export default function About () {
return <main>
<h1>About Us</h1>
<p>We write JavaScript and words about JavaScript.</p>
<Authors />
</main>
}
JSX
- Allows us to write HTMl with JavaScript.
- When returning HTML is important to wrap it in parentheses and ensure we are returning one element (it can have plenty of children elements)
- If we have multiple elements and don't want to create a div just for the sake of wrapping, we can use React fragments which achieves the goal of returning one thing but it wont add this wrapper div to the markup.
<React.Fragment>
or - We must explicitly close self closing tags.
<img src="" />
- Can't have variables that much keywords or that have dashes in them, we need to use camelCase.
- To use JS expressions or variables we wrap it in
{}
Props
-
Props are to components what arguments are to functions.
-
How to Pass Data into Components:
- Anytime you pass a value that isn't a string, you have to wrap it in
{}
{{}}
: Don't be scared, this just means you're passing an object in. One to be able to write JS and the second because that's object syntax.- Passing a prop without a value will automatically be set to true.
<DatePicker settings={{ format: "yy-mm-dd", animate: false, days: "abbreviate" }} /> <Header name="Dempsey" />
- Anytime you pass a value that isn't a string, you have to wrap it in
-
How to Access the Data:
- React puts all the props into a props object and passes that object as the first argument to the function when it renders.
- We can pass the entire props object and access what we want via keys:
function Layout(props) { return ( <div className='layout'> <SideBar /> {props.name} <Footer /> </div> ) }
- Since props is just an object, we can also destructure it as we like.
function Hello ({ first, last }) { return ( <h1> Hello, {first} {last} </h1> ) }
- If we have data been opening and closing tags of a React Component, we can access this data via
props.children
:
function Header (props) { return <h1 className="header">{props.children}</h1> } function Layout (props) { return ( <div className="layout"> <Sidebar /> {props.children} <Footer /> </div> ) }
Events
- We can create functions and pass them in through props to be invoked on certain events.
- Notice the difference between passing in a function reference to passing in a function invocation.
onClick={handleClick}
: referenceonClick={handleClick()}
: invocation
- Always pass functions as references and NOT invocations!
- Whenever an event handler is called by React, React will pass an object as the first argument to the event handler that contains all the information about the event that was triggered.
- To create an event handler we need to create a function that takes the event and does something with it:
export default function App() {
const handleChange = (event) => {
if (event.target.value.length > 10) {
alert("Character limit exceeded");
}
}
return (
<section>
<h1>Character Limit</h1>
<input onChange={handleChange} placeholder="Enter some text" />
</section>
);
}
Every Event you can Listen for inside React
Clipboard Events:
- onCopy
- onCut
- onPaste
Composition Events:
- onCompositionEnd
- onCompositionStart
- onCompositionUpdate
Keyboard Events:
- onKeyDown
- onKeyPress
- onKeyUp
Focus Events:
- onFocus
- onBlur
Form Events:
- onChange
- onInput
- onInvalid
- onReset
- onSubmit
Generic Events:
- onError
- onLoad
Mouse Events:
- onClick
- onContextMenu
- onDoubleClick
- onDrag
- onDragEnd
- onDragEnter
- onDragExit
- onDragLeave
- onDragOver
- onDragStart
- onDrop
- onMouseDown
- onMouseEnter
- onMouseLeave
- onMouseMove
- onMouseOut
- onMouseOver
- onMouseUp
Pointer Events:
- onPointerDown
- onPointerMove
- onPointerUp
- onPointerCancel
- onGotPointerCapture
- onLostPointerCapture
- onPointerEnter
- onPointerLeave
- onPointerOver
- onPointerOut
- onPointerHover
Selection Events:
- onSelect
Touch Events:
- onTouchCancel
- onTouchEnd
- onTouchMove
- onTouchStart
UI Events:
- onScroll
Wheel Events:
- onWheel
Media Events:
- onAbort
- onCanPlay
- onCanPlayThrough
- onDurationChange
- onEmptied
- onEncrypted
- onEnded
- onLoadedData
- onLoadedMetadata
- onLoadStart
- onPause
- onPlay
- onPlaying
- onProgress
- onRateChange
- onSeeked
- onSeeking
- onStalled
- onSuspend
- onTimeUpdate
- onVolumeChange
- onWaiting
Image Events:
- onLoad
- onError
Animation Events:
- onAnimationStart
- onAnimationEnd
- onAnimationIteration
Transition Events:
- onTransitionEnd
Other Events:
- onToggle
States
-
useState
: is how we can preserve a variable across component renders and it is a hook.- It takes a single argument, the initial value for that piece of state.
- It returns an array with the first item being the state value and the second item being a way to update that state.
// Array destructuring to get these values out of the default array const [state, setState] = React.useState( "initial state value" )
-
Below is a simple example that counts how many times a button is clicked:
import * as React from "react" export default function Counter() { const [count, setCount] = React.useState(0) const handleClick = () => setCount(count + 1) return ( <button onClick={handleClick}> {count} </button> ) }
Lifting State Up
- One of the most crucial parts to State Management in React.
- Whenever you have state that multiple components depend on, you want to lift that state up to the nearest parent component and pass it down via props.
- To solve the issue of decoupling where the state lives from the event handlers we create an updater function in the parent component and then invoke the functions in the child components via props.
- Important Concept to Remember:
- Whenever the state you're updating lives in a different location from the event handlers that update that state, you'll create an updater function in the component where the state lives and you'll invoke that function from the component where the event handlers live.
// Example of what passing a function to handle event change in the child element looks like
import * as React from "react";
import Todo from "./Todo"
export default function TodoList() {
const [todo, setTodo] = React.useState({
id: 1,
label: "Learn React",
completed: false,
})
const handleUpdateTodo = (updatedTodo) => {
setTodo(updatedTodo)
}
return (
<ul>
<Todo
todo={todo}
handleUpdateTodo={handleUpdateTodo}
/>
</ul>
)
}
-
When we update state, whatever we pass in to the update function will overwrite the current state.
- If we are dealing with an object or array and want to change a single value we can use the spread operator to get the current values and then we update what we want to.
const handleUpdateEmail = (email) => { const updatedUser = { ...user, email: "tyler@ui.dev" } setUser(updatedUser) // 🫡 }
Dealing with Arrays
- Add an element: Spread Operator
const handleNewHighScore = (session) => {
const newHighScores = [...highScores, session]
setHighScores(newHighScores)
}
- Remove an element: Filter
const handleRemoveCheater = (id) => {
const newHighScores = highScores.filter((session) =>
session.id !== id
)
setHighScores(newHighScores)
}
- Update an Element: Map
const handleUpdateHighScore = (updatedSession) => {
const newHighScores = highScores.map((session) => {
return session.id === updatedSession.id
? updatedSession
: session
})
setHighScores(newHighScores)
}
Hooks
-
Special functions that give us help from React.
-
Hooks are functions, but it's helpful to think of them as unconditional declarations about your component's needs.
-
You use React features at the top of your component similar to how you import modules at the top of your file.
-
useEffect
allows you to run a side effect that synchronizes your component with some outside systems- it takes in a function that does what you want after rendering and then it takes a list of arguments that it needs for it to complete its task.\
- If we don't want our app to re-render every time we are setting state, we can call our set state function with a function which means we don't need to pass our state variable in to the useEffect's dependency array.
- This is due to the set state function's API, you can either pass it the state variable which relies on closure, or a function that just gets the current value of the state.
- Runs Asynchronously after render.
React.useEffect(() => { document.title = `Welcome, ${name}` }, [name])
-
useEffectEvent
: allows us to abstract reactive but non-synchronizing values out of useEffect into a function that we can call inside useEffect.- Effectively removes values from the dependency array that don't need to be there.
-
useRef
: Creates a value that is preserved across renders but won't trigger a re-render when it changes.- When we want a variable or state to be preserved across renders but don't want it changing to trigger a re-render, we use this hook.
- DOM node reference: Whenever you pass a ref as a prop to a React element, React will put a reference to the DOM node it creates into that ref's current property.
-
useContext
: allows us to reference props from anywhere in our application. We can create a context withcreateContext()
. See more -
useReducer
: allows us to manage state with the reducer pattern.const initialState = 0; function reducer(state, value) { return state + value; } const [state, dispatch] = React.useReducer( reducer, initialState )
dispatch
is a function that when called will invoke the reducer function with whatever value is passed into it.- reducers hold their power in being able to decouple different states into it via action types:
function reducer(state, action) { if (action.type === "next_step") { return { ...state, currentStep: state.currentStep + 1 } } else if (action.type === "prev_step") { return { ...state, currentStep: state.currentStep - 1 } } else if (action.type === "change") { return { ...state, formData: { ...state.formData, [action.name]: action.value } } } else if (action.type === "reset") { return initialState } else { return state } } // What the handlers can look like const [state, dispatch] = React.useReducer(reducer, initialState) const handleNextStep = () => { dispatch({ type: "next_step"}); }; const handlePrevStep = () => { dispatch({ type: "prev_step"}); }; const handleChange = (e) => { dispatch({ type: "change", name: e.target.name, value: e.target.value }); }; const handleSubmit = (e) => { e.preventDefault(); alert("Thank you for your submission"); dispatch({ type: "reset" }) };
-
NOTE: Purpose of
React.memo
is to opt out of React's default rendering behavior whereasReact.useMemo
is to cache the result of a calculation. -
useMemo
: lets you cache the result of a calculation between renders- The first argument is a function that returns the value you want to cache.
- The second argument is an array of dependencies the function depends on.
-
useCallback
: works the same as useMemo but caches the function itself rather than the result.- it is an abstraction of useMemo, can be thought of like this:
function useCallback(fn, dependencies) { return useMemo(() => fn, dependencies); }
-
useLayoutEffect
: similar to useEffect but ensures that the code within useLayoutEffect and any state updates scheduled inside will be processed before the browser repaints the screen.- Used the same way useEffect is used, just behaves slightly differently.
- Runs synchronously after render, but before React shows anything to the user.
-
useSyncExternalStore
: allows us to subscribe a component to an extern store allowing us to not have to have a duplicate piece of state that seems redundant.- takes a subscribe function as the first parameter and a snapshot as the second argument.
Custom Hooks
- Ideal for reusing non-visual logic.
- Needs to start with
use
- just export the hook at the bottom of the file.
- If your hook doesn't use other hooks, it's not a hook it's a function.
Managing Side Effects
-
Rule #0: When a component renders, it should do so without running into any side effects.
-
Rule #1: If a side effect is triggered by an event, put that side effect in an event handler.
-
Rule #2: If a side effect is synchronizing your component with some external system, put that side effect inside useEffect.
- it works by removing the side effect from React's rendering flow and delaying its execution until after the component has rendered.
-
Rule #3: if a side effect is synchronizing your component with some outside system and that side effect needs to run before the browser paints the screen, put that side effect inside
useLayoutEffect
. -
Rule #4: If a side effect is subscribing to an external store, use the
useSyncExternalStore
hook. -
Clean up function: If we have some async function that we may be waiting for a response and it gets called multiple times. This can make for an unpleasant experience as we won't know which request will resolve first.
- The clean up function solves this issue, simply have a return at the end of the your useEffect block and have an ignore variable that is set to true within it.
- We know this return (clean up function) will only run when the effect is stale.
- Then we only need to check this ignore variable before we set our variable to the result of the async function.
import * as React from "react" import { fetchPokemon } from "./api" import Carousel from "./Carousel" import PokemonCard from "./PokemonCard" export default function App () { const [id, setId] = React.useState(1) const [pokemon, setPokemon] = React.useState(null) const [loading, setLoading] = React.useState(true) const [error, setError] = React.useState(null) const handlePrevious = () => { if (id > 1) { setId(id - 1) } } const handleNext = () => setId(id + 1) React.useEffect(() => { const handleFetchPokemon = async () => { setLoading(true) setError(null) const { error, response } = await fetchPokemon(id) // *********** Where we check the ignore variable ********* if (ignore) { return } else if (error) { setError(error.message) } else { setPokemon(response) } setLoading(false) } handleFetchPokemon() // **************** CLEAN UP FUNCTION ********************** return () => { console.log(`The cleanup function for ${id} was called`) } }, [id]) return ( <Carousel onPrevious={handlePrevious} onNext={handleNext}> <PokemonCard loading={loading} error={error} data={pokemon} /> </Carousel> ) }
-
It's important to clean up any timeouts or event listeners you set in
useEffect
.- You clean them up when the component is removed from the DOM, if you don't it will cause a memory leak.
React.useEffect(() => { const handleResize = () => { setWidth(window.innerWidth) setHeight(window.innerHeight) }; window.addEventListener("resize", handleResize) return () => { window.removeEventListener("resize", handleResize) } }, []);
-
Managing animation times can be difficult. It can be useful to use the getAnimations API in
useEffect
to figure out specific animation times:const [active, setActive] = React.useState(false) const node = React.useRef(null) const animating = React.useRef(false) React.useEffect(() => { const checkAnimations = async () => { animating.current = true; // NOTE: node references a DOM node because in the JSX it is being used as a prop // <div ref={ref}> const animations = node.current.getAnimations({ subtree: true }) const promises = animations.map((animation) => animation.finished) await Promise.all(promises) animating.current = false; } checkAnimations() })
Teleporting Props with Context
-
3 Things we need in order to do this:
- Create the context
- Decide what we want to teleport and to where
- Getting what we teleported from inside a component
-
We can create a context with
React.createContext()
-
Now we need to know what and where in regards to the teleportation.
- When you create a context, React gives you an object with the provider property.
- Provider accepts a value prop which is the data that you want to teleport to any component in the Provider's subtree.
import delorean from "./delorean" return ( <delorean.Provider value={marty}> <TwoThousandFifteen /> <NineteenEightyFive /> <NineteenFiftyFive /> <EighteenEightyFive /> </delorean.Provider> )
-
To get access to this value we can use
React.useContext()
and pass it in the context.export default function Cafe80s () { const marty = React.useContext(delorean) return ( <div className="location"> <h3>Cafe80s</h3> <p>Step back in time and join us at Cafe 80's for a blast from the past! Enjoy a delicious meal while surrounded by iconic 80's memorabilia, relive the era of big hair, bright colors, and all your favorite 80's hits. Don't miss out on the ultimate retro dining experience!</p> <h4>Current Guests</h4> <ul> <li>{marty.name}: Age {marty.age}. {marty.occupation}.</li> </ul> </div> ) }
Referential Equality
- A value in JavaScript can be one of two types: primitive value or a reference value
- When we create a new variable and assign it to a reference value, what we're really doing is copying the reference to the spot in memory where the value is stored
let leo = {
type: "Dog",
age: 0,
goodBoy: true,
name: "Leo",
}
let snoop = leo
snoop.name = "Snoop"
console.log("leo's name:", leo.name) // leo's name: Snoop
console.log("snoop's name:", snoop.name) // snoop's name: Snoop
-
Comparing reference values can cause some confusion:
- Primitive values are compared via value
- Reference values are compared by their location in memory
const me = "Tyler" const friend = "Tyler" console.log(me === friend) // true const leo = { type: "dog" } const leito = { type: "dog" } console.log(leo === leito) // false