All notes below are taken from the official docs, the best resource to learn React!
Components
React lets us declare components and reuse them across our frontend application. These components have to return JSX. Which is just fancy HTML that can contains Javascript code inside. Hereâs an example of a React component that returns an image of a dog if the user likes dogs, or an image of a generic animal otherwise.
const user = {
preferences: {
favouritePet: 'dog'
}
}
const DisplayPet = () => {
return <>
{/* this comment, alongside the tertiary operator down below, are JS code! */}
<img
src={`/assets/${user.preferences.favouritePet == 'dog' ? 'dog' : 'generic'}.png`}
/>
</>
}
export default DisplayPet;Hooks
Components use hooks to add fancy logic to their code. Anybody can create hooks, theyâre regular JS functions with two special rules:
- they have to be called at the top-level of a component (not in JSX, not in any branches, loops or other functions - they basically have to be called in the same order on every re-render)
- they cannot be called from non-React or non-functional components (yes class components are a thing)
The useState Hook
This lets your component retain a piece of data across renders.
React re-renders components often throughout the lifetime of your application. Every time a component is re-rendered, the whole function is executed again, therefore variables are reset. If we want to preserve a variable, we have to use a state. Think of a state as a snapshot of data that is only changed on re-render.
The useState hook returns two values, a variable (which is persisted across renders) and a function (which lets us set the value, and any time itâs set, render the component again, so that the value may be seen on the UI).
One important caveat with useState is that when you set your state, the component will re-render with the new value, but the current rendering wonât change! Think of a state as a snapshot of data that is only changed on re-render. Hereâs an example:
import { useState, type ChangeEvent } from 'react';
const DisplayPet = () => {
const [favouritePet, setFavouritePet] = useState('dog'); // dog is the default value!
const handleAnimalChange = (e: ChangeEvent<HTMLInputElement>) => {
setFavouritePet(e.target.value);
// on first run, this will always print 'dog'!
// even after we changed the value to something else
console.log(favouritePet);
}
return <>
<img
src={`/assets/${favouritePet == 'dog' ? 'dog' : 'generic'}.png`}
alt={`Closest thing to a ${favouritePet}`}
/>
<br/>
<label>Change your favourite animal: </label>
<input type="text" defaultValue={favouritePet} onChange={handleAnimalChange}/>
</>
}
export default DisplayPet;In the code snippet, above, after the user clears the textbox, and enters âcatâ, the console will look like this:
dog
c
caThink about why âcatâ is not printed!
React Rendering Steps
- Trigger (on initial render, or on state update)
- Render:
- On initial render: call the root component (which will call every child component recursively) & create DOM nodes for each of them
- On re-render: call component whose state has changed & calculate which of their properties has changed since previous render.
- Committing:
- On initial render: put all the DOM nodes on screen
- On re-render: apply minimal necessary operations to make the DOM match the latest rendering output
Batching
All state updates that happen in a continuous piece of code (event handlers, async function continuations, promises, etc) are actually batched together! That means the UI wonât be updated until after the related code completes.
This could cause some issues if you need the newly updated state right away, before the re-render. We can actually get this value by passing a function to our set function that takes in the previous set value as an argument.
These two code snippets may look like they do the same thing, but they actually do completely different things.
// assume number is 0
setNumber(number + 1); // setNumber(0+1)
setNumber(number + 1); // setNumber(0+1)
setNumber(number + 1); // setNumber(0+1)
// assume number is 0
setNumber(n => n + 1); // setNumber(0+1)
setNumber(n => n + 1); // setNumber(1+1)
setNumber(n => n + 1); // setNumber(2+1)
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42); // setNumber(n => 42), so n is unused here
// at the end, number will be 42!When React executes these set function calls, theyâre added to a queue, thatâs then consumed on the next renderâs useState call.
States are immutable
You should treat states as immutable, even if your state value is actually a mutable object like a string. As such, states should be treated as read-only.
const [position, setPosition] = useState({ x: 0, y: 0 });
position.x += 1; // this is illegal!
setPosition(p => ( {...p, p.x + 1} )) // correct way of updatingMutating states like that doesnât actually trigger a re-render and may cause nasty bugs. In a nutshell, not mutating things solves a bunch of headaches. When using arrays, youâre going to want to use methods that donât cause mutations.
Declarative vs Imperative
One thing to realize after all this is that React uses a declarative paradigm, not an imperative paradigm. You donât tell React what to do, but you lay out some states and the triggers for each state change, then the framework takes care of the rest. In vanilla JS instead, you do actually command every action (show this button on input change, hide that specific element, etcâŠ)
SSSSS rule (State Should Stay Simple Stupid)
Make sure your component only has states that it needs. Any of the following states will make your code error-prone:, that donât contradict each other, that are not duplicated, or that can be calculated during render. Any of these will make your code error-prone.
- contradicting state: having two states
isSendingandisSent. If at any point, both are true, itâll cause a paradox. Instead, use astatusstate that can take one of three valid values:typing,sendingandstate. - duplicated state: having
itemsandselectedItemwhereselectedItemsactually stores one of the objects initems. Instead, useselectedItemIdto point to one of the objects initems. - can be calculated during render: having
firstName,lastName, andfullNameas states.fullNamedoes not need to be a state, it can be calculated during render. - deeply nested states: these are hard to reason about and require to recursively use the spread operators when updating the state.
Itâs also a good idea to group up states that you update together into one single state, so that itâs easier to keep them in sync. Whenever your states break any of the rules above, normalize them to make âem 5S!
One Single Truth for the State
For a specific state, there should be one single source of truth.
Therefore, states can and should be passed around as props if needed. To keep using the Inversion of Control philosophy, the set function should only be used in the component that owns the state though, as child components that use the state shouldnât be concerned with state update logic.
Thatâs why in the component below, SearchBar takes in handleChange, and not setQuery.
export default function FilterableList() {
const [query, setQuery] = useState('');
const results = filterItems(foods, query);
const handleChange = e => setQuery(e.target.value);
return (
<>
<SearchBar query={query} onChange={handleChange} />
<List items={results} />
</>
);
}How does React keep track of states?
React keeps track of which state belongs to which component based on their place in the UI tree (not in the JSX, but in the UI tree!). For example, this app has two counters with two different states! Even if itâs rendering the same component object.
const counter = <Counter />;
return (
<div>
{counter}
{counter} {/* different state than the counter above! */}
</div>
);This comes with a few implication.
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
...
</div>
)In the app above, the counter will always be the same even if you change person! This is because the component is rendered in the same place on the UI tree.
One way to fix this is by using the key prop:
...
<Counter key="Sarah" person="Sarah" />
...key is useful whenever you want to tell React to re-create the DOM instead of reusing it.
When displaying lists, itâs important to never use their index for the key, because if the elements change order, the state wonât actually change since itâll be index based, not element-based.
The useReducer hook
If you have lots of different and complicated state updates in your code, it might be worth it to look into using useReducer instead of useState.
In your component, instead of focusing on how to update the state, you just have to worry about what actually happens. The state updating logic can live elsewhere.
Reducers must be pure, and each action should describe a single user interaction.
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function handleAddTask(text) {
dispatch({ type: 'added', id: nextId++, text: text });
}
function handleChangeTask(task) {
dispatch({ type: 'changed', task: task });
}
function handleDeleteTask(taskId) {
dispatch( type: 'deleted', id: taskId });
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
// tasksReducer
export default function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}The useContext hook
Prop-drilling is the a phenomenon that occurs when you have to pass props deep down your UI tree. Letâs just this is is not that convenient. An alternative to this is using context.
import {useContext} from 'react';
export default function App() {
const [isLarge, setIsLarge] = useState(false);
return (
<ImageSizeContext value={isLarge ? 150 : 100}>
<List/>
</ImageSizeContext>
)
}
// ...a bunch of components where the imageSize would otherwise be drilled down...
function MyImage() {
const imageSize = useContext(ImageSizeContext);
return (
<img
src={...}
width={imageSize}
height={imageSize}
/>
);
}
// our ImageSizeContext.js
import {createContext} from 'react';
export const ImageSizeContext = createContext(100);Using context may be tempting but it hides the data flow and it makes your code harder to maintain. Itâs usually used for global state like themes, current user and routing.
Using context, reducer and custom hooks together
Hereâs an example of a to-do app, defining a reducer (for the tasks) and a context (for the reducer) so that components down the UI tree can read the tasks and dispatch events on them.
Letâs define our context and reducer in the same component.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}
/*
We can even define custom hooks so that children don't even need to write `useContext(bla bla)`
*/
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}Then, in our app:
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';
export default function TaskApp() {
return (
<TasksProvider>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksProvider>
);
}Our task list can read tasks from the context using our custom hook, and our task component can do the same to dispatch events!
export default function TaskList() {
const tasks = useTasks();
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task task={task} />
</li>
))}
</ul>
);
}
function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useTasksDispatch();
...
}The useRef hook
If you need a state but you donât want to trigger new renders when changing its value, use useRef. It could be very useful when you need to build a stopwatch for example, and you need to clear a setInterval or setTimeout.
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
// we just need to keep track of the intervalId without rerendering
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
// as you can see, refs are MUTABLE!
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
const handleStop = () => clearInterval(intervalRef.current);
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}Refs are also used to reference to specific DOM nodes, for example to focus a node, scroll to it, or measure its size/position.
export default function Form() {
const inputRef = useRef(null);
function handleClick() { inputRef.current.focus(); }
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}> Focus the input </button>
</>
);
}The useEffect hook
Effects let us specify side effects that are caused by rendering itself, rather than by a particular event.
useEffect takes in one mandatory value (a function to run) and an optional dependency list. By default, an effect runs after every render. The list of dependencies changes this by telling React to run our effect only after one of those dependencies changes.
The effect function can also return a cleanup function, used in cases where we connect to an external system and then the cleanup function disconnects.
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) { ref.current.play(); }
else { ref.current.pause(); }
});
return <video ref={ref} src={src} loop playsInline />;
}Effects should be used sparingly.
- You donât need Effects to transform data for rendering
- You donât need Effects to handle user events Generally, if thereâs no external system involved, donât use effects.
The useEffectEvent hook
Effects are reactive. They run whenever one of their dependencies changes. Whenever we mix reactive logic with non-reactive logic (the user making an action for example).
If we want to read some non-reactive data from our effect, but we donât want to add it to our dependency list, then we can use the useEffectEvent() hook.
function ChatRoom({ roomId, theme }) {
// our effect can read theme without re-running on theme change
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // â
No need for theme!
// ...Other times, it might be useful to move in some data inside of our Effect, so that it doesnât depend on it anymore.
pro tip: as your apps grows, write your own custom hooks to write less and less Effects from scratch :)
