This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the react category.
Last Updated: 2024-11-21
Disclaimer Most of this code comes from the official docs or other, better sources online. I have made simplifications of my own as well as clarifying comments etc.
Useful esp for technical interviews
npx create-react-app myappname
yarn test
What is wrong with this code in React?
const modifyCarsList = (element, id) => {
carsList[id].checked = element.target.checked;
setCarsList(carsList);
};
The carList
is still the same item (an array) in RAM , even though a value WITHIN the array changed. This update cannot be detected by React. Instead such as change must be done by passing a new array object into the setCarList
function.
const modifyCarsList = (element, id) => {
const { checked } = element.target;
setcarsList((cars) => {
return cars.map((car, index) => {
if (id === index) {
car = { ...car, checked };
}
return car;
});
});
};
setState(prevState => [...prevState, newItem])
// Notice the prev, the circles around the object, the single curly braces (only at start and fin)
setState(prev => ({...prev, "foo": "bar"}))
useEffect
- short for "side effects"- cannot be done during rendering useEffect(() => {
// Runs after EVERY rendering
});
2.Once, after initial render only
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
=> ideal for fetching data from server initially
3.Every time prop OR state changes
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state])
}
{}
as a dependency to useEffect
because it will differ on every render. The reason this is problematic is because useEffect is going to do a referential equality check on options between every render, and thanks to the way JavaScript works, options will be new every time so when React tests whether options changed between renders it'll always evaluate to true, meaning the useEffect callback will be called after every render rather than only when bar and baz changfunction Blub() {
return <Foo bar="bar value" baz={3} />
}
function Foo({bar, baz}) {
const options = {bar, baz}
React.useEffect(() => {
buzz(options)
}, [options]) // we want this to re-run if bar or baz change
return <div>foobar</div>
}
// Instead do this (extract the simple values from the objects and use as dependencies)
function Foo({bar, baz}) {
const options = {bar, baz}
React.useEffect(() => {
buzz(options)
}, [bar, baz]) // take the individual values out of the object instead, which won't change
return <div>foobar</div>
}
OK - but what if bar
is a function and baz
is an complex object (e.g. an array) - these will always be different on
each render, causing performance death
function Blub() {
const bar = () => {}
const baz = [1, 2, 3]
return <Foo bar={bar} baz={baz} />
}
// This is precisely the reason why useCallback and useMemo exist. So here's how you'd fix that (all together now):
// Then do this -- wrap any FUNCTIONS in useCallback and any objects/arrays in useMemo)
function Blub() {
const bar = React.useCallback(() => {}, [])
const baz = React.useMemo(() => [1, 2, 3], [])
return <Foo bar={bar} baz={baz} />
}
Be careful -- if you define a function in your component body then use it in useEffect, then you HAVE to declare it in your dependences to useEffect
const Example = () => {
const [count, setCount] = useState(0)
const [showMessage, setShowMessage] = useState(true)
// inner helper function that will be called w/in `useEffect()`
const hideMessage = () => {
if (count < 10) {
setShowMessage(false)
}
}
useEffect(() => {
window
.fetch('https://api.benmvp.com/')
.then((res) => res.json())
.then((data) => {
if (data.success) {
// ?? not including `hideMessage()` in the dependencies of
// `useEffect()` is a lurking bug and will trigger the
// `react-hooks/exhaustive-deps` ESLint rule
hideMessage()
}
})
}, [])
render (
<p>Bla</p>
)
}
But wait there is an EVEN BIGGER problem here even if you changed the dependencies to
useEffect(() => {
...
}, [hideMessage])
The problem is that hideMessage
will be a new object every time!!! Therefore triggering re-renders. We need to wrap it
in useCallback
to fix this issue!
const Example = () => {
const [count, setCount] = useState(0)
const [showMessage, setShowMessage] = useState(true)
// ???? The reference of `hideMessage()` will only change if/when
// the value of `count` changes
const hideMessage = useCallback(() => {
if (count < 10) {
setShowMessage(false)
}
}, [count])
useEffect(() => {
window
.fetch('https://api.benmvp.com/')
.then((res) => res.json())
.then((data) => {
if (data.success) {
hideMessage()
}
})
}, [hideMessage])
render (
<p>Bla</p>
)
}
You can clean up with an effect by returning a function
useEffect(() => {
Subscribe.toMessages()
return () => Unsubscribe.fromMessages()
}, [])
Remember useEffect itself can't be async -- you need to define a subfunction that is async and call that
useEffect(() => { // <--- CANNOT be an async function
async function fetchEmployees() {
// ...
}
fetchEmployees(); // <--- But CAN invoke async functions
}, []);
const value = useContext(MyContext);
// make some data
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
// pass data in via createContext and name a variable XContent with the result
const ThemeContext = React.createContext(themes.light);
function App() {
return (
// Wrap some components with XContent.Provider, giving it a `value` por with some data
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
// This is just here to show we don't need to drill props down to ThemedButton,
// thanks to useContext
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
// Here we actually use the data
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
Here is how to pass multiple values in
const CounterContext = React.createContext("counter")
export default function MultiValueContextDemo() {
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
return(
<CounterContext.Provider value={{ counter1, setCounter1, counter2, setCounter2 }}>
<CounterComponent />
</CounterContext.Provider>
)
}
Here's a fuller example
// File 1: the context
export type GeoContext = {
isLoading: boolean;
status: Status;
setStatus: React.Dispatch<React.SetStateAction<Status>>;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
export const GeolocationContext = createContext<GeoContext>({
isLoading: false,
status: 'prompt',
setStatus: () => {},
setLoading: () => {},
});
export const GeolocationProvider: React.FC = ({ children }) => {
const [status, setStatus] = useState<Status>('prompt');
const [isLoading, setLoading] = useState(false);
return <GeolocationContext.Provider value={{ isLoading, setLoading, status, setStatus }}>{children}</GeolocationContext.Provider>;
}
// File 2: Usage
const App = () => {
<GeolocationProvider>
<ChildThatUsesThisContext/>
<GeolocatoinProvider/>
}
function MyComponent() {
// handleClick is re-created on each render and
// will be a DIFFERENT function object....
const handleClick = () => {
console.log('Clicked!');
};
// ...
}
Problem? Usually not.Because inline functions are cheap, the re-creation of functions on each rendering is not a problem. A few inline functions per component are acceptable.
However there are special cases where need to mantain that same original photo
useEffect(..., [handleclick])
That's when useCallback(callbackFun, deps) is helpful: given the same dependency values deps, the hook returns the same function instance between renderings (aka memoization):
import { useCallback } from 'react';
function MyComponent() {
// handleClick is the same function object
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []);
// ...
}
What's a a good use-case?
Say you have a component MyBigList
that you to memoize
import useSearch from './fetch-items';
function MyBigList({ term, onItemClick }) {
const items = useSearch(term);
const map = item => <div onClick={onItemClick}>{item}</div>;
return <div>{items.map(map)}</div>;
}
export default React.memo(MyBigList);
import { useCallback } from 'react';
export function MyParent({ term }) {
const onItemClick = useCallback(event => {
console.log('You clicked ', event.currentTarget);
}, [term]);
return (
<MyBigList
term={term}
onItemClick={onItemClick}
/>
);
}
Memo will re-render if your props change. And that matters here because theuseSearch
function could be really slow... therefore optimization that won'd justify itself in "normal areas" would be important here. So you are worried about the onItemClick
bit. You don't want it to be re-rendered everytime onTimeClick functin is generated, as what happens normally
useCallback
not only makes code harder to read but also slower (as sen in a profile)useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
Here it is used in a filter
const filteredUsers = React.useMemo(
() =>
users.filter((user) => {
console.log('Filter function is running ...');
return user.name.toLowerCase().includes(search.toLowerCase());
}),
[search]
);
The following code is gonna have weird bugs. If id
is empty, the component renders 'Please select a game to fetch' and
exits. No hooks are invoked. But if id is not empty (e.g. equals '1'), then the useState() and useEffect() hooks are
invoked.
function FetchGame({ id }) {
if (!id) {
return 'Please select a game to fetch';
}
const [game, setGame] = useState({
name: '',
description: ''
});
useEffect(() => {
const fetchGame = async () => {
const response = await fetch(`/api/game/${id}`);
const fetchedGame = await response.json();
setGame(fetchedGame);
};
fetchGame();
}, [id]);
return (
<div>
<div>Name: {game.name}</div>
<div>Description: {game.description}</div>
</div>
);
}
Purpose: reusing code across components
// Custom hooks make use of useState, and useEffect often
// In general they can use other hooks within them
import { useState, useEffect } from 'react';
// Begin your custom hook with the word "use"
// It takes as many args as you need
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Don't forget to return a function to unsubscribe from whatever is going on
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
// Returns the key result to the caller of the hook
return isOnline;
}
We can pass dynamic state to our custom hooks
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
// This will show count numbers that may be way out of date!
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
Fix by recreating the interval
function WatchCount() {
const [count, setCount] = useState(0);
useEffect(function() {
const id = setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
return function() {
clearInterval(id);
}
}, [count]);
return (
<div>
{count}
<button onClick={() => setCount(count + 1) }>
Increase
</button>
</div>
);
}
// We import Suspense, which must wrap lazy component
import React, { Suspense } from 'react';
// React.lazy(() => import("x")) won't load until necessary
// fallback is a loading indicator
const OtherComponent = React.lazy(() => import('./OtherComponent'));
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
Glimmer problem
<Tabs onTabSelect={handleTabSelect} />
<Suspense fallback={<Glimmer />}>
{tab === 'photos' ? <Photos /> : <Comments />}
</Suspense>
When switching tab to comments the glimmer will show. You want the OLD UI (photos) to show until comments load, so do this
const handleTabSelect = () {
React.startTransition(() => {
setTab(tab);
});
}
import React, { Suspense, lazy } from 'react';
// Notice we grab Router, Routes (wraps multiple routes), and Route (takes path and page/screen component)
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Notice how it is wrapped in lazy
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
)
When a component is wrapped in React.memo(), React renders the component and memoizes the result. Before the next render, if the new props are the same, React reuses the memoized result skipping the next rendering. This is instead of rendering both versions and comparing
export function Movie({ title, releaseDate }) {
return (
<div>
<div>Movie title: {title}</div>
<div>Release date: {releaseDate}</div>
</div>
);
}
export const MemoizedMovie = React.memo(Movie);
{/* Don't forget to use error */}
{isError && <div>Something went wrong ...</div>}
function App() {
const [search, setSearch] = useState("");
const [text, setText] = useState("");
const [selectedValue setSelectedValue] = useState("lime");
return (
<form onSubmit={(e) => e.preventDefault() }>
<input
className="input"
type="text"
// We control the value for this input via React,
// not html
value={search}
placeholder="Search Here"
onChange={(e) => setSearch(e.target.value)}
/>
<textarea value={text} onChange={(e) => setText(e.target.value)}></textarea>
<select value={selectedValue} onChange={(e) => setSelectedValue(e.target.value)}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</form>
);
}
import * as React from 'react';
const useLocalStorage = (storageKey, fallbackState) => {
const [value, setValue] = React.useState(
JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState
);
React.useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(value));
}, [value, storageKey]);
return [value, setValue];
};
const App = () => {
const [isOpen, setOpen] = useLocalStorage('is-open', false);
const handleToggle = () => {
setOpen(!isOpen);
};
}
// Sadly must use classes still
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
// The main bit of state is {hasError}, defaulting to fase
this.state = { hasError: false };
}
// This is also necessary in order that the next render
// will show the fallback UI
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
// NB: Use this to log to sentry
componentDidCatch(error, errorInfo) {
// Something like this
Sentry.reportException(error)
}
render() {
if (this.state.hasError) {
return <h4>Something went wrong</h4>
}
// Happy case: show the regular children of the ErrorBoundary
return this.props.children;
}
}
The error boundary in use:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
Limitations - does not handle errors that are thrown outside of the boundary (obviously) - does not handle errors from async code (e.g. setTeimout, requestAnimationFrame)
css
you must use className
due to claass being a serferee word in JS.e qkli
) a key that is not th array index.import React, { useState } from "react";
export const TestMemo = () => {
const [userName, setUserName] = useState("faisal");
const [count, setCount] = useState(0);
const increment = () => setCount((count) => count + 1);
return (
<>
// This will get rendered every time count changes... what a waste
<ChildrenComponent userName={userName} />
<button onClick={increment}> Increment </button>
</>
);
};
// The fix: const ChildrenComponent = React.useMemo(({userName}) => ... )
const ChildrenComponent =({ userName }) => {
console.log("rendered", userName);
return <div> {userName} </div>;
};
One use case is to avoid infinite rendinering
// This will render infinitely... the renderCount state constantly changes
const App() {
const [renderCount, setRenderCount] = useState(0)
useEffect(() => {
// WARNING: I don't think access to old state is possible anymore...
setRenderCount(prevRenderCount => prevRenderCount + 1)
})
// Instead this would work
betterRenderCount = useRef(0)
useEffect(() => {
// WARNING: I don't think access to old state is possible anymore...
betterRenderCount.current += 1
})
return (
<>
<p>{renderCount}</p>
</>
)
}
{current: 0}
OK so you can also use useRef
to access a DOM element rendered as a child in your component. Here is how
const App() {
const [name, setName] = useState("someName")
const inputRef = useRef
const focusInput = () {
inputRef.current.focus()
}
return (
<>
<input value={name} ref={inputRef} ..../>
<button onClick={focusInput}/>
<p>my name is {name}</p>
</>
)
}
Now if you console.log(inputRef.current) you will see the <input....>
Watch out though:
if you go and change data in in the refed input e.g. inputRef.current.value
to be something like "FOOO", then you'll
see that value for the input.... but you won't see that value propogated throughout the other bits of your application
i.e. the p tag with name
is not updated. So it would be better to use the proper useState for this stuff and let things progpogate
Another usecase of refs is to store the previous value of your state
const prevName = useRef()
useEffect(() => {
prevName.current = name
}, [name])
function Counter() {
const [count, setCount] = useState(0);
// We create a custom hook and pass in a useState holder
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
function usePrevious(value) {
// We create in ref in our hook
const ref = useRef();
useEffect(() => {
// in a useEffect call, we set the current
// to the arg passed into usePrevious
ref.current = value;
}, [value]);
// MAGIC BIG: This bit returns BEFORE the useEffect code which
// updates the value async. Thus the return value of usePrevious
// will be out of date due to useEffect taking so long. But
// by the time we call the hook again, the ref will have the now
// current value.
return ref.current;
}
function TextInputWithFocusButton() {
// create a ref with useRef(null)
const inputEl = useRef(null);
// Have some functionality you want to do with this referred item
const onButtonClick = () => {
// Now got off and do
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
// Don't forget to pass this ref into target
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Or more elegantly
const useFocus = () => {
const htmlElRef = useRef(null)
const setFocus = () => {htmlElRef.current && htmlElRef.current.focus()}
return [ htmlElRef, setFocus ]
}
function TextInputWithFocusButton() {
const [inputEl, onButtonClick] = useFocus()
};
return (
<>
// Don't forget to pass this ref into target
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Ref forwarding is a feature that lets some components take a ref they receive, and pass it further down to a child.
Basically here we create a ref at the top-level, pass it to our custom ButtonElement, which in turn
passes it down to a <button>
bit of HTML that it renders
// Notice how the component is built with React.forwardRef(props, ref) => ()
// instead of (props) => {}
const ButtonElement = React.forwardRef((props, ref) => (
<button ref={ref} className="CustomButton">
{props.children}
</button>
));
// Create ref to the DOM button:
const ref = React.createRef();
<ButtonElement ref={ref}>{'Forward Ref'}</ButtonElement>
Concretely, a higher-order component is a function that takes a component and returns a new component.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
Examples:
- Redux - const ConnectedMyThing = connect({age: state.age}, {setAge: () => dispatch({ type: "INCREMENT"})})(MyThing)
Here is a use-case of where you might use one. Say you have two components that both need to subscribe to something
const UnstyleComponent = () => {
return (
<>
<p>My name is jack</p>
</>
)
}
// Notice how it accepts a component as its first argument, followed by other
// arbitrary arguments
const withStyle = (WrappedComponent, className) => (
// Notice how it returns a component... and that it needs a props argument
(props) => (
<div className={className}>
<WrappedComponent {...props} />
</div>
)
)
const StyledComponent = withStyle(UnstyleComponent, "blue-background")
Alternatively you might pass in more advanced functionality like functions
// THIS CODE MAY NOT ACTUALLY WORK -- just a demo
const withSubscription = (WrappedComponent, dataGrabber) => {
(props) => {
const [data, setData] = useState([])
useEffect({
setData(dataGrabber)
}, [])
return <WrappedComponent data={data}/>
}
}
const BlogPostWithSubscription = withSubscription(
// Different uses of this HOC might take different
// component here, and different functions to grab the data
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
[]
dependency, EVERY time). Then add query to it, so it gets call from new queriesconst [isLoading, setIsLoading] = useState(false);
. in useEffect, before
and after the freaking searchevent.preventDefault();
(otherwise the link will open)function SomeComponent() {
// Define some error catching stuff
const [isError, setIsError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// define an async function here
const fetchData = async () => {
// Reset error at start of fetch
setIsError(false);
// Reset loading at start of fetch
setIsLoading(true);
const fetchParams = {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Auth-Token": token,
},
};
// use a try block, since this shit can fail
try {
const result = await fetch(url, fetchParams).then(response => {
// Maybe do something with http status
const { status } = response;
response.json()
}).then(response=>
setData(response);
)
} catch (error) {
// set is error if catch
setIsError(true);
}
// don't forget to reset loading
setIsLoading(false);
};
// call that function
fetchData();
// Make dependent on user input as it comes from setState
}, [query]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button"
onClick={() => setQuery(`somesite:?query${query}`)}
>
Search
</button>
{/* Don't forget to use error */}
{isError && <div>Something went wrong ...</div>}
<Fragment/>
)
}
Return objects from custom hooks instead of lists
I returned
return [ setCanGoBack, handleBackButtonPress ]
But then the caller mis-labelled the functions
const [handleBackButtonPress, setCanGoBack] = useMyHook();
This mixup would not be possible had I returned an object.