- Basic building block of React app are components
- Component is a piece of UI with its own appearance and logic
- Components are functions that return markup
- Can nest components within other components
- Main component in file uses
export default function
- Markup syntax used is JSX
- JSX is stricter than HTML as it must close tags, must start with a capital letter
- Component must only return one JSX tag, so must nest or wrap with empty
<>...</>
wrapper - CSS
class
isclassName
attribute in JSX - Use curly braces to use JS inside JSX
- Can also use JS for attributes but instead of double quotes use curly braces
- Conditional rendering
let content;
if (isLoggedIn) {
content = <AdminPanel />;
} else {
content = <LoginForm />;
}
return (<div>content}</div>);
<div>
{isLoggedIn ? (<AdminPanel />) : (<LoginForm /> )}
</div>
<div>
{isLoggedIn && <AdminPanel />}
</div>
- Render lists using
for
loop andmap
function- Each item needs
key
attribute so React knows when add, remove, reorder item
- Each item needs
const listItems = products.map(product =>
<li key={product.id}>
{product.title}
</li>
);
return (
<ul>{listItems}</ul>
);
- Responding to events
onClick={fnName}
- Do not call function inside
- State inside component
import { useState } from 'react';
- Declare state variable (
const [count, setCount] = useState(0);
) useState
returns current state and a function for you to update the state of that variable- Each component gets its own state
- Hooks
- Functions that start with
use
- Built-in hook is
useState
- Can build your own by combining existing ones
- Functions that start with
- Sharing state
- State should go in top most component that it wants to share state with
- Parent component passes it down to child components via props (state and event handler)
- Child reads prop
function MyButton({ count, onClick }) {
return (
<button onClick={onClick}>
Clicked {count} times
</button>
);
}
- https://react.dev/reference/react-dom/components/common
- JSX turned into JS objects so majority of attributes need to be in camel case and not use reserved keywords
- Inline style properties written in camel case
- Can set default value for prop
children
prop is what is passed to parent component when children are between its opening and closing tags- Components are pure
- It shouldn’t change any objects or variables that existed before rendering
- Same input, same output
- Components shouldn’t depend on each others rendering sequence since rendering can happen at any time
- Do not mutate any inputs (props, state, context)
- Use
setState
instead
- Use
- Use event handlers. As a last resort use
useEffect
- Render tree only contains React components to remain platform agnostic
- Module dependency tree shows imports of modules
- May include modules of components, functions, constants
Interactivity
- state: data that changes over time
- Event handlers
- Functions that will be triggered in response to user events / interactions
- Usually defined inside your components
- Have name that start with
handle
, followed by the name of the event
onClick={handleClick}
pass function as a prophandleClick
is an event handler
onClick={() => { alert('You clicked me'); }}
- Inline event handlers convenient for short functions
- Do not call functions passed to event handlers
- It will fire the function immediately during rendering, without any interactions
- This happens because JS inside curly braces executes right away
- By convention, event handler props should start with
on
followed by a capital letter - Events propagate in React except
onScroll
- Event handlers receive an event object as their only argument
- To stop propagation,
e.stopPropagation()
onClickCapture={() => { runs first }}
- Catches all events on child elements, even if they stopped propagation
- Each event propagates in 3 phases:
- Travels down, calling all
onClickCapture
handlers - Runs clicked element’s
onClick
handler - Travels upwards, calling all
onClick
handlers
- Travels down, calling all
- Useful for code like routers or analytics
- Can pass handlers as an alternative to propagation
- Add more code to be called before calling handler passed down from parent
- Benefit is that you can clearly follow whole chain of code that executes as a result of some event
e.preventDefault()
- Some browser events have default behaviour associated with them (e.g. form submit event)
- Event handlers don’t need to be pure so can change something
State
-
Local variables don’t persist between renders
-
Changes to local variables won’t trigger renders
-
useState
provides a state variable to retain data between renders and a state setter function to update variable and trigger React re-render -
import { useState } from 'react';
-
const [index, setIndex] = useState(0);
-
Functions that start with
use
is a Hook- Special functions that are only available while React is rendering
- Can only be called at top level of components or your custom Hooks
- Cannot be called inside conditions, loops, or other nested functions
-
Isolated and private for each component
-
Render triggered
- Initial render
createRoot
with target DOM node, then callrender
method with componentconst root = createRoot(document.getElementById('root));
root.render(<Image/>)
- State updated so re-render
- Render gets queued
- Initial render
-
React renders components
- React calls components to figure out what to display on screen
- Calls root component on initial render
- DOM nodes created
- Calls component on re-renders
- Calculate properties changes since previous render
-
React commits changes to DOM
appendChild()
DOM API to place all rendered nodes on screen on initial render- React will execute minimum necessary operations to update screen with new values
- Browser painting
-
State value never changes during render
-
Updating same state multiple times before next render
- Pass in updater function to
setState
… parameter is the current value of that state variable setNumber(n => n + 1)
- Pass in updater function to
Managing State
- Imperative UI
- Giving step by step instructions on how the UI should be updated
- Manually update DOM based on user interactions or data changes
- Vanilla JS, jQuery
- Declarative UI
- Describes what the UI should show
- State management handled by framework
- Step 1: Identify component’s different visual states
- For a form, different states might include empty, typing, submitting, success, and error
- Create mocks for different states before adding logic
- Step 2: Determine what triggers those state changes
- Computer inputs, user inputs
- Step 3: Represent state in memory using
useState
- Step 4: Remove any non-essential state variables
- Step 5: Connect event handlers to set state
- Principles for structuring state
- Group related state
- If certain values are usually updated at the same time, think about merging them into a single state variable
- Avoid contradictions in state
- Causes confusion and leaves room for mistakes
- Example is a form with
isSending
andisSent
state - Leaves room for “impossible” states like having both set to true at the same time
- Better to have a single
status
state variable
- Avoid redundant state
- If can calculate new state from existing props or state variables, don’t need a new state variable for this
- Example is state with
firstName
,lastName
, andfullName
… can calculatefullName
fromfirstName
andlastName
during render so it is redundant
- Avoid duplication in state
- Difficult to keep them in sync
- Example is having an array of products in state and also a
selectedItem
in state that holds the selected product (same as in the products array). If item is editable, updates may only go in the object inside the array and we forget to updateselectedItem
- Better to change to use
selectedId
- Better to change to use
- Avoid deeply nested state
- Not convenient to update
- Group related state
- “controlled” (driven by props) or “uncontrolled” (driven by state)
- React preserves a component’s state for as long as it’s being rendered at its position in the UI tree
- if you want to preserve the state between re-renders, the structure of your tree needs to “match up” from one render to another. If the structure is different, the state gets destroyed because React destroys state when it removes a component from the tree.
- You can use keys to make React distinguish between any components.
Reducers
- Can place all state update logic outside of component into a reducer function
- Good if components have many state updates spread across many event handlers
- Migrate from
useState
touseReducer
- Step 1: Move from setting state to dispatching actions
- Specifies what the user just did by dispatching actions from event handlers in component
- State update logic lives in reducer
dispatch({ type: 'added', id: nextId++, text })
- Object passed to
dispatch
is an “action”
- Step 2: Write reducer function
- Function where state logic goes
- Takes two arguments: current state and action object
- Returns next state
- Step 3: Use reducer from component
- Import
useReducer
Hook from'react'
- Replace
useState
withuseReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
- Import
- Step 1: Move from setting state to dispatching actions
Context
- allows parent component to make information available to components below it (children and grandchildren) without passing via props
- “prop drilling”
- lifting state high through many layers
- Step 1: Create a context
import { createContext } from 'react';
export const LevelContext = createContext(1); // argument is the default value
- Step 2: Use that context from the component that needs the data
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
const level = useContext(LevelContext);
useContext
is a Hook- tells React that this component wants to read the
LevelContext
- Step 3: Provide that context from the component that specifies the data
- Wrap children with context provider to provide context to them
import { LevelContext } from './LevelContext.js';
<LevelContext.Provider value={level}>{children}</LevelContext.Provider>
- Context lets you write components that adapt to their surroundings and display themselves differently depending on where they are being rendered
- It’s like CSS property inheritance… the only way to override some context coming from above is to wrap children into a context provider with a different value
- Different React contexts don’t override each other
- Before using context
- Start by passing props
- Extract components and pass JSX as
children
to them<Layout posts={posts}/>
to<Layout><Posts posts={posts}/></Layout>
- Use cases
- Theming
- Current account
- Routing
- Managing state
Combining reducers and context to manage state
- State and
dispatch
function / event handlers may only be available in the top-level component- If child components need these, would need to prop them down as props
- Bad if many children below to get to child component that needs these values
- Instead, place the state and
dispatch
function into context so any component below top-level component can read the state and dispatch actions without prop drilling
- Step 1: Create the context
- Create separate context for state variable and dispatch function
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
- Step 2: Put state and dispatch into context
- Import both contexts into parent component, take the state and dispatch function returned by
useReducer()
and provide them to entire tree below const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
<TasksContext.Provider value={tasks}><TasksDispatchContext.Provider value={dispatch}>...</TasksDispatchContext.Provider></TasksContext.Provider>
- Import both contexts into parent component, take the state and dispatch function returned by
- Step 3: Use context anywhere in the tree
- Any component that needs task list can read from
TasksContext
const tasks = useContext(TasksContext);
- To update task list, any component can read
dispatch
function from context and call itconst dispatch = useContext(TasksDispatchContext);
dispatch({ type: 'added', id: nextId++, text });
- Any component that needs task list can read from
- Can also move reducer code and context code into a single file
- Create a new
TasksProvider
component that will manage state with reducer, provide both contexts to components below, and takechildren
as a prop so you can pass JSX to it
- Create a new
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
// Custom Hooks
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
Escape Hatches
Referencing Values with Refs
- Used when you want a component to remember some info but don’t want it to trigger new renders
import { useRef } from 'react';
const ref = useRef(0);
- Pass initial value you want to reference
- Returns an object with current value (
{ current: 0 }
)- Mutable (can read and write to it), React doesn’t track this
- When piece of info only needed by event handlers and changing it doesn’t require re-render, use ref instead of state
- Examples of when to use refs:
- Storing timeout IDs
- Storing and manipulating DOM elements
- Storing other objects that aren’t necessary to calculate the JSX
Manipulating DOM with refs
- Sometimes might need to access DOM elements managed by React (focus node, scroll to node, measure size and position)
- Pass ref as
ref
attribute to JSX tag for which you want to get the DOM node - Can access DOM node from event handlers and use built-in browser APIs defined on it
- Managing a list of refs
- Could get a single ref to parent element then use DOM manipulation to “find” the child nodes of interest but easily breakable if DOM changes
- Callback passed to
ref
attribute- Function is called with DOM node when it’s time to set the ref, and with
null
when it’s time to clear it - Maintain an array or a Map and access any ref by its index or an ID
- Function is called with DOM node when it’s time to set the ref, and with
- Can access another component’s DOM nodes by passing refs from parent to child components but can make code fragile
useImperativeHandle
- Restrict exposed functionality
- Instructs React to provide your own special object as the value of a ref to the parent component
function MyInput({ ref }) {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose focus and nothing else
focus() {
realInputRef.current.focus();
},
}));
return <input ref={realInputRef} />;
};
- React sets
ref.current
during the commit- Before updating DOM, React sets affected
ref.current
tonull
- After updating DOM, React sets them to the corresponding DOM nodes
- Before updating DOM, React sets affected
- Force React to update/flush the DOM synchronously
import { flushSync } from 'react-dom';
- Wrap state in call
flushSync(() => { setTodos([...todos, newTodo]);
- Then call
listRef.current.lastChild.scrollIntoView();
- Avoid changing DOM nodes managed by React because it can lead to inconsistent visual results or crashes
Effects
-
Run code after rendering so you can sync component with some system outside React
-
Examples include connecting to a server or use of a third-party library
-
Run at the end of a commit after screen changes
-
Writing an effect
- Step 1: Declare an effect
import { useEffect } from 'react';
useEffect(() => { ..code here will come after every render.. });
- Step 2: Specify Effect dependencies
- Array of dependencies as second argument to
useEffect
- Skip running if anything not in array is the same as it was during previous render
- Empty array as dependencies will only run
useEffect
on mount / when component appears ref
andset
functions returned byuseState
have stable identity so they’re usually omitted from dependencies
- Array of dependencies as second argument to
- Step 3: Add cleanup if needed
- Clean up function returned from function inside
useEffect
return () => { connection.disconnect(); };
- Clean up function is called each time before the Effect runs again and one final time when the component unmounts / gets removed
- Clean up function returned from function inside
- Step 1: Declare an effect
-
Controlling non-React widgets
-
Example: add map component to page with
setZoomLevel
method. Want to keep zoom level in sync withzoomLevel
state variable in React code- Effect is called twice but not a problem because setting same value twice doesn’t do anything though it may be slightly slower
useEffect(() => { const map = mapRef.current; map.setZoomLevel(zoomLevel); }, [zoomLevel])
-
Example:
showModal
method of built-in<dialog>
element throws an error if call it twice. Need cleanup function to close dialoguseEffect(() => { const dialog = dialogRef.current; dialog.showModal(); return () => dialog.close(); }, []);
-
-
Subscribing to events
useEffect(() => { function handleScroll(e) { console.log(window.scrollX, window.scrollY); } window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []);
-
Triggering animations
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
}, []);
-
Fetching data
useEffect(() => { let ignore = false; async function startFetching() { const json = await fetchTodos(userId); if (!ignore) { setTodos(json); } } startFetching(); return () => { ignore = true; }; }, [userId]);
- Fetching data inside effects is popular way but its very manual and has many downsides
- Effects don’t run on the server. Initial server-rendered HTML will only include a loading state with no data. Client computer will have to download all JS and render app only to find out that it needs to load the data
- Fetching directly in effects makes it easy to create “network waterfalls”. Fetch data only when parent component is done rendering, then when child components done rendering. Significantly slower than fetching data in parallel
- Fetching directly in effects usually means you don’t preload or cache data
- Not ergonomic. A lot of boilerplate code
- Frameworks may have built-in data fetching mechanism that are efficient and don’t suffer from above pitfalls
- Or build a client-side cache
- Can use
AbortController
to cancel requests that are no longer needed
- Fetching data inside effects is popular way but its very manual and has many downsides
-
Sending analytics
useEffect(() => { logVisit(url); // Sends a POST request }, [url]);
- Can also send analytics from route change event handlers instead
- Intersection observers can help track which components are in the viewport and how long they remain visible
-
Some logic that only runs once when application start can be placed outside components
-
In Strict Mode, React mounts components twice (in development only) to stress-test your Effects
-
If Effect breaks because of remounting, you need to implement a cleanup function
Might not need effect
- No external system involved
- Removing unnecessary Effects will make code easier to follow, faster to run, less error-prone
- Do not need to transform data for rendering
- When state is updated, React will call component functions to calculate what should be on the screen. React will commit these changes to the DOM to update the screen. React then runs Effects. Effects also immediately updates state, restarting the whole process again
- Unnecessary render passes
- Instead transform all data at top level of component
- Do not need to handle user events
- By the time an Effect runs, you don’t know what the user did
- Handle them in event handlers instead
- Need to sync with external systems
- Keep jQuery widget in sync with React state
- Fetch data, sync search results with current search query
- Modern frameworks provide more efficient built-in data fetching mechanisms than writing Effects directly in components
- Can cache expensive calculation by wrapping it in
useMemo
Hookconst visibleTodos = useMemo(() => { return getFilteredTodos(todos, filter); }, [todos, filter]);
- Tells React you don’t want the inner function to re-run unless either
todos
orfilter
have changed - React remembers return value during initial render. If value of
todos
orfilter
are same as last time,useMemo
returns last result it has stored - Runs during rendering… only pure calculations
- Test performance with artificial slowdown (Chrome CPU throttling option)
- Tells React you don’t want the inner function to re-run unless either
useSyncExternalStore
Hook- Subscribe to an external store
- Better to use this instead of subscribing inside
useEffect
function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; } function useOnlineStatus() { // ✅ Good: Subscribing to an external store with a built-in Hook return useSyncExternalStore( subscribe, // React won't resubscribe for as long as you pass the same function () => navigator.onLine, // How to get the value on the client () => true // How to get the value on the server ); } function ChatIndicator() { const isOnline = useOnlineStatus(); // ... }
Lifecycle of Effects
- Can only do 2 things: start syncing something, stop syncing it
- Can happen multiple times if they depend on props and state that change over time
- React components lifecycle
- Mounts, updates, unmounts
- Effect is independent from component’s lifecycle
- How to sync an external system to current props and state
- Sometimes it may be necessary to start and stop syncing multiple times while the component remains mounted
- Example: on first load, user connects to a “general” chatroom. when user selects a different chatroom, it should close connection with “general” chatroom and connect to the new one
- How re-sync works
- React calls cleanup function that your Effect returned after connecting to “general” chatroom.
- React will run Effect that you’ve provided during this render (the one with the new chatroom id)
- Stop syncing when user goes to a different screen and the component unmounts
- Always focus on a single start/stop cycle at a time. Doesns’t matter whether a component is mounting, updating, or unmounting
- How React verifies that your Effect can resync
- In development, React always remounts each component once
- React forces mount, unmount, remount immediately in develop to verify Effect can resync
- How React knows it needs to resync the Effect
- Dependency list
- Every time after component re-renders, React looks at array of dependencies. If any values is different from value in the same spot you passed during previous render, React will resync Effect
- Each Effect represents a separate sync process
- Want to send analytics event when user visits the room. Currently the Effect depends on
roomId
so you might want to add it to the same effect. But what if we add another dependency to this Effect that needs to re-establish connection. If resyncs, it will also call to log the analytic event. - Write as two separate Effects instead
- Want to send analytics event when user visits the room. Currently the Effect depends on
- Effect with empty dependencies
- Specified what your Effect does to start and stop syncing
- No reactive dependencies but if change so it relies on reacting to state or prop changes, just add them to dependency list
- All variables declared in component body are reactive
- Mutable values (including global variables) are not reactive
- Can change at any time completely outside of React rendering data flow
- Changing it won’t trigger re-render of component
- Reading mutable data during rendering (when you calculate dependencies) breaks purity of rendering
- Instead, should read and subscribe to an external mutable value with
useSyncExternalStore
- Instead, should read and subscribe to an external mutable value with
location.pathname
,ref.current
- ref object returned by
useRef
can be a dependency butcurrent
property is intentionally mutable
- React linter will check that every reactive value used by Effect’s code is declared as its dependency
Separating Events from Effects
-
Sometimes want an Effect that re-runs in response to some values but not others
-
Event handlers
- Run in response to specific interactions
- Example: Sending a message
-
Logic inside event handlers is not reactive
- Will not run again unless user performs the same interaction again
- Can read reactive values without “reacting” to their changes
-
Logic inside Effects is reactive
- If reads a reactive value, must specify it as a dependency
- If re-render causes value to change, Effect logic will re-run with the new value
-
Extracting non-reactive logic out of Effects
- Example: When user connects to a chatroom, display a notification (also by getting the colour theme prop and show notification in this colour)
- Setting theme and showing notification should not be reactive… but the changing of chatroomId and connecting to it is reactive (belongs in Effect)
- Declare Effect Event (Experimental API as of Feb 7 when this note was taken)
useEffectEvent
Hook to extract non-reactive logic out of Effect (not released in stable version yet)- Behaves like an event handler
- Logic inside is not reactive and it always “sees” latest values of props and state
function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); // ✅ All dependencies declared // ...
-
Reading latest props and state with Effect Events (Experimental API as of Feb 7 when this note was taken)
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
- Better to pass
url
to Effect Event explicitly- Pass it as an argument to Effect Event… says that visiting a page with different
url
constitutes a separate event from the user’s perspective
const onVisit = useEffectEvent(visitedUrl => { logVisit(visitedUrl, numberOfItems); }); useEffect(() => { onVisit(url); }, [url]);
- Especially important if there is some async logic inside Effect
const onVisit = useEffectEvent(visitedUrl => { logVisit(visitedUrl, numberOfItems); }); useEffect(() => { setTimeout(() => { onVisit(url); }, 5000); // Delay logging visits }, [url]);
url
insideonVisit
corresponds to the latesturl
(which could have already changed), butvisitedUrl
corresponds tourl
that originally caused this Effect (and thisonVisit
call) to run
- Pass it as an argument to Effect Event… says that visiting a page with different
- Limitations of Effect Events
- Only call them from inside Effects
- Never pass them to other components or Hooks
- Always declare Effect Events directly next to the Effects that use them