During my foundational time, I write notes on what I studied and learned.
These notes were taken while watching the Front End Masters course "Intermediate React, v4" by Brian Holt.
Most common hooks
useState
The useState hook allows us to manage state with functions rather than class components. When state is stored in a hook, the component is loaded on every render and the component always has the latest state.
Example
Used when switching a component between green and not green
const[isGreen, setIsGreen] = useState(true);
onClick={()=> setIsGreen(!isGreen)}
Note: when putting something in the default state, it's preferable that it's something already computed or cheap (true, false, 0, [] etc) rather than a div element, for example. This is a performance consideration since it's not a cheap procedure to create and destroy things like DOM nodes.
useEffect
useEffect recreates the componentDidMount, componentDidUpdate, and componentDidUnmount functionality from React. This hook is useful for updates like AJAX requests or third-party library integrations occurring outside the render method.
If you add an empty array as a dependency, useEffect only runs once.
Example
In this example, the browser displays a timer that updates every second. The page re-renders every second when setTime is changed.
If you put time in the dependency array, useEffect will run every time the time variable is updated. It's better to be explicit with your dependencies, which is why we're adding time here.
useEffect(() => {
const timer = setTimeout(() => setTime(new Date()), 1000);
return () => clearTimeout(timer);
}, [time]);
On clearTimeout
clearTimeout takes in whatever the result of setTimeout is it'll prevent the timer you scheduled from running. You do this to clear out the component so you're not calling setTimeout when the component is out of scope (if the component gets unmounted).
useContext
Allows data from one component to be available in a subcomponent. This avoids the need to drop drill or pass the data from parent to child. Typically the data used with useContext is application-level state or data that's shared through the entire application.
Users are an example since you need to read and write user accounts from every part of the application.
Example
Passing in userState at the top level and reading it down to a child five levels down. At level five, you use useContext to pull out the original userState hook as user.
<UserContext.Provider value={userState}>
<h1>first level</h1>
<LevelTwo userState={userState} />
</UserContext.Provider>;
const LevelFive = () => {
const [user, setUser] = useContext(userContext);
};
Note: use circumventing the normal data flow of React sparingly because it's not as explicit as prop-drilling. It's difficult to debug.
useRef
The useRef hook will always return the current value of the object because it is not subject to the closure's scope like useState.
Example
import { useState, useRef } from "react";
const RefComponent = () => {
const [stateNumber, setStateNumber] = useState(0);
const numRef = useRef(0);
function incrementAndDelayLogging() {
setStateNumber(stateNumber + 1);
numRef.current++;
setTimeout(
() => alert(`state: ${stateNumber} | ref: ${numRef.current}`),
1000
);
}
return (
<div>
<h1>useRef Example</h1>
<button onClick={incrementAndDelayLogging}>delay logging</button>
<h4>state: {stateNumber}</h4>
<h4>ref: {numRef.current}</h4>
</div>
);
};
export default RefComponent;
Why is this useful? It can be useful for things like holding on to setInterval and setTimeout IDs so they can be cleared later. Or any bit of statefulness that could change but you don't want it to cause a re-render when it does.
It's also useful for referencing DOM nodes directly and we'll see that a bit later in this section.
useReducer
A reducer is passed the state and an action. Based on the action's type, a new state is returned. The useReducer hook uses a dispatcher to call the reducer.
This is a preferable approach if you have complex state updates or if you have a situation like this: all of the state updates are very similar so it makes sense to contain all of them in one function.
Example
import { useReducer } from "react";
// fancy logic to make sure the number is between 0 and 255
const limitRGB = (num) => (num < 0 ? 0 : num > 255 ? 255 : num);
const step = 50;
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT_R":
return Object.assign({}, state, { r: limitRGB(state.r + step) });
case "DECREMENT_R":
return Object.assign({}, state, { r: limitRGB(state.r - step) });
case "INCREMENT_G":
return Object.assign({}, state, { g: limitRGB(state.g + step) });
case "DECREMENT_G":
return Object.assign({}, state, { g: limitRGB(state.g - step) });
case "INCREMENT_B":
return Object.assign({}, state, { b: limitRGB(state.b + step) });
case "DECREMENT_B":
return Object.assign({}, state, { b: limitRGB(state.b - step) });
default:
return state;
}
};
const ReducerComponent = () => {
const [{ r, g, b }, dispatch] = useReducer(reducer, { r: 0, g: 0, b: 0 });
return (
<div>
<h1 style={{ color: `rgb(${r}, ${g}, ${b})` }}>useReducer Example</h1>
<div>
<span>r</span>
<button onClick={() => dispatch({ type: "INCREMENT_R" })}>➕</button>
<button onClick={() => dispatch({ type: "DECREMENT_R" })}>➖</button>
</div>
<div>
<span>g</span>
<button onClick={() => dispatch({ type: "INCREMENT_G" })}>➕</button>
<button onClick={() => dispatch({ type: "DECREMENT_G" })}>➖</button>
</div>
<div>
<span>b</span>
<button onClick={() => dispatch({ type: "INCREMENT_B" })}>➕</button>
<button onClick={() => dispatch({ type: "DECREMENT_B" })}>➖</button>
</div>
</div>
);
};
export default ReducerComponent;
Hooks you're unlikely to use
useMemo
Memoizes expensive function calls so they are only re-evaluated when needed.
useMemo and useCallback are performance optimizations. Use them only when you already have a performance problem instead of preemptively.
useMemo memoizes expensive function calls so they only are re-evaluated when needed. Brian uses the [fibonacci sequence][fibonacci] in its recursive style to simulate this. All you need to know is that once you're calling fibonacci with 30+ it gets quite computationally expensive and not something you want to do unnecessarily as it will cause pauses. It will now only call fibonacci if count changes and will just the previous, memoized answer if it hasn't changed, instead of calling it again on every re-render.
If we didn't have the useMemo call, every time I clicked on the title to cause the color to change from red to green or vice versa it'd unnecessarily recalculate the answer of fibonacci but because we did use useMemo it will only calculate it when num has changed.
Example
import { useState, useMemo } from "react";
const fibonacci = (n) => {
if (n <= 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
};
const MemoComponent = () => {
const [num, setNum] = useState(1);
const [isGreen, setIsGreen] = useState(true);
const fib = useMemo(() => fibonacci(num), [num]);
return (
<div>
<h1
onClick={() => setIsGreen(!isGreen)}
style={{ color: isGreen ? "limegreen" : "crimson" }}
>
useMemo Example
</h1>
<h2>
Fibonacci of {num} is {fib}
</h2>
<button onClick={() => setNum(num + 1)}>➕</button>
</div>
);
};
export default MemoComponent;
useCallback
useCallback is similar to useMemo because it will limit calls to expensive functions as long as the props have not changed.
With useMemo, an expensive function could be called when a component is re-rendered because the function is redeclared. The useCallback hook solves this issue by ensuring the same function is always reference and only called when a re-render is necessary.
Typically whenever React detects a change higher-up in an app, it re-renders everything underneath it. This normally isn't a big deal because React is quite fast at normal things. However you can run into performance issues sometimes where some components are bad to re-render without reason.
In this case, Brian is using a new feature of React called React.memo. This is similar to PureComponent where a component will do a simple check on its props to see if they've changed and if not it will not re-render this component (or its children, which can bite you.) React.memo provides this functionality for function components.
Given that, we need to make sure that the function itself given to ExpensiveComputationComponent is the same function every time. We can use useCallback to make sure that React is handing the same fibonacci to ExpensiveComputationComponent every time so it passes its React.memo check every single time. Now it's only if count changes will it actually re-render (as evidenced by the time.)
Example
import { useState, useEffect, useCallback, memo } from "react";
const ExpensiveComputationComponent = memo(({ compute, count }) => {
return (
<div>
<h1>computed: {compute(count)}</h1>
<h4>last re-render {new Date().toLocaleTimeString()}</h4>
</div>
);
});
const CallbackComponent = () => {
const [time, setTime] = useState(new Date());
const [count, setCount] = useState(1);
useEffect(() => {
const timer = setTimeout(() => setTime(new Date()), 1000);
return () => clearTimeout(timer);
});
const fibonacci = (n) => {
if (n <= 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
};
return (
<div>
<h1>useCallback Example {time.toLocaleTimeString()}</h1>
<button onClick={() => setCount(count + 1)}>
current count: {count}
</button>
<ExpensiveComputationComponent
compute={useCallback(fibonacci, [])}
count={count}
/>
</div>
);
};
export default CallbackComponent;
useLayoutEffect
useLayoutEffect is almost the same as useEffect except that it's synchronous to render as opposed to scheduled like useEffect is. If you're migrating from a class component to a hooks-using function component, this can be helpful too because useLayout runs at the same time as componentDidMount and componentDidUpdate whereas useEffect is scheduled after. This should be a temporary fix.
The only time you should be using useLayoutEffect is to measure DOM nodes for things like animations. In the example, I measure the textarea after every time you click on it (the onClick is to force a re-render.) This means you're running render twice but it's also necessary to be able to capture the correct measurements.
Example
import { useState, useLayoutEffect, useRef } from "react";
const LayoutEffectComponent = () => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const el = useRef();
useLayoutEffect(() => {
setWidth(el.current.clientWidth);
setHeight(el.current.clientHeight);
});
return (
<div>
<h1>useLayoutEffect Example</h1>
<h2>textarea width: {width}px</h2>
<h2>textarea height: {height}px</h2>
<textarea
onClick={() => {
setWidth(0);
}}
ref={el}
/>
</div>
);
};
export default LayoutEffectComponent;
useImperativeHandle
Here's one you will likely never directly use but you may use libraries that use it for you. Brian uses it in conjunction with another feature called forwardRef that again, you probably won't use but libraries will use on your behalf.
In the example above, whenever you have an invalid form, it will immediately focus the the first field that's invalid. If you look at the code, ElaborateInput is a child element so the parent component shouldn't have any access to the input contained inside the component. Those components are black boxes to their parents. All they can do is pass in props. So how do we accomplish it then?
The first thing we use is useImperativeHandle. This allows us to customize methods on an object that is made available to the parents via the useRef API.
Inside ElaborateInput we have two refs: one thate is the one that will be provided by the parent, forwarded through by wrapping the ElaborateInput component in a forwardRef call which will ten provide that second ref parameter in the function call, and then the inputRef which is being used to directly access the DOM so we can call focus on the DOM node directly.
From the parent, we assign via useRef a ref to each of the ElaborateInputs which is then forwarded on each on via the forwardRef. Now, on these refs inside the parent component we have those methods that we made inside the child so we can call them when we need to. In this case, we'll calling the focus when the parent knows that the child has an error.
Again, you probably use this directly but it's good to know it exists. Normally it's better to not use this hook and try to accomplish the same thing via props but sometimes it may be useful.
Example
import { useState, useRef, useImperativeHandle, forwardRef } from "react";
const ElaborateInput = forwardRef(
({ hasError, placeholder, value, update }, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
}
};
});
return (
<input
ref={inputRef}
value={value}
onChange={(e) => update(e.target.value)}
placeholder={placeholder}
style={{
padding: "5px 15px",
borderWidth: "3px",
borderStyle: "solid",
borderColor: hasError ? "crimson" : "#999",
borderRadius: "5px",
margin: "0 10px",
textAlign: "center"
}}
/>
);
}
);
const ImperativeHandleComponent = () => {
const [city, setCity] = useState("Seattle");
const [state, setState] = useState("WA");
const [error, setError] = useState("");
const cityEl = useRef();
const stateEl = useRef();
function validate() {
// lol I found it on StackOverflow : https://stackoverflow.com/a/25677072
if (
!/^([a-zA-Z\u0080-\u024F]+(?:. |-| |'))*[a-zA-Z\u0080-\u024F]+$/.test(
city
)
) {
setError("city");
cityEl.current.focus();
return;
}
if (!/^[A-Z]{2}$/.test(state)) {
setError("state");
stateEl.current.focus();
return;
}
setError("");
alert("valid form!");
}
return (
<div>
<h1>useImperativeHandle Example</h1>
<ElaborateInput
hasError={error === "city"}
placeholder={"City"}
value={city}
update={setCity}
ref={cityEl}
/>
<ElaborateInput
hasError={error === "state"}
placeholder={"State"}
value={state}
update={setState}
ref={stateEl}
/>
<button onClick={validate}>Validate Form</button>
</div>
);
};
export default ImperativeHandleComponent;
useDebugValue & useId
These hooks, like useImperativeHandle, are more built for library authors.
useDebugValue allows you to surface information from your custom hook into the dev tools. This allows the developer who is consuming your hook (possibly you, possibly your coworker) to have whatever debugging information you choose to surfaced to them. If you're doing a little custom hook for your app (like the breed one we did in the Intro course) this probably isn't necessary. However if you're consuming a library that has hooks (like how react-router-dom has hooks) these can be useful hints to developers.
Normally you'd just use the developer tools built into the browser but CodeSandbox has the dev tools built directly into it. Just know that normally you'd use the browser extension.
Example
import { useState, useEffect, useDebugValue } from "react";
const useIsRaining = () => {
const [isRaining, setIsRaining] = useState(false);
useEffect(() => {
// pretend here you'd make an API request to a weather API
// instead we're just going to fake it
setIsRaining(Math.random() > 0.5);
}, []);
useDebugValue(isRaining ? "Is Raining" : "Is Not Raining");
return isRaining;
};
const DebugValueComponent = () => {
const isRaining = useIsRaining();
return (
<div>
<h1>useDebugValue Example</h1>
<h2>Do you need a coat today? {isRaining ? "yes" : "maybe"}</h2>
</div>
);
};
export default DebugValueComponent;
Top comments (0)