99

Concurrent visitors over the last 30 minutes. (~)

Peak concurrent visitors -
Yesterday: 0 / Today: 0

React Core Patterns

React Core Patterns

This post is based on Starting a React Project with Vite.

# JSX component structure

In a React project, a single .jsx file is called a component.
It has the following basic structure, and component names are usually written in PascalCase.

/src/App.jsx
JSX
1
2
3
4
5
export default function ComponentName() { return ( // Template ) }

Since React 17, you no longer need to write import React from 'react' at the top of a component to use JSX.
However, depending on the development tools or environment of some projects, you may still need to write it.

# JSX fragment

A React component cannot have multiple top-level elements.
You must specify only one top-level element (<div>), like this.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
export default function App() { return ( <div> <header>HEADER</header> <main>MAIN</main> <footer>FOOTER</footer> </div> ) }

However, this can add an unnecessary element that wraps the elements twice, like this.

HTML
1
2
3
4
5
6
7
8
9
<body> <div id="root"> <div> <!-- Unnecessary div element --> <header>HEADER</header> <main>MAIN</main> <footer>FOOTER</footer> </div> </div> </body>
Rendered result.

If you use a JSX fragment (<></>) as the top-level element of a component, you can group multiple elements and render them like a single child element without creating an extra element.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
export default function App() { return ( <> <header>HEADER</header> <main>MAIN</main> <footer>FOOTER</footer> </> ) }

Now it renders like this.

HTML
1
2
3
4
5
6
7
8
<body> <div id="root"> <!-- No div element in between --> <header>HEADER</header> <main>MAIN</main> <footer>FOOTER</footer> </div> </body>
Rendered result.

If you need to set attributes on a JSX fragment, you must use the <Fragment> component explicitly.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
import { Fragment } from 'react' export default function App() { return ( <Fragment key={'123'}> <header>HEADER</header> <main>MAIN</main> <footer>FOOTER</footer> </Fragment> ) }

# Data interpolation

In a component template, you can easily output data with the {} symbols.
This is called interpolation.

/src/App.jsx
JSX
1
2
3
4
5
6
7
export default function App() { const name = 'ParkYoungWoong' return ( <h1>Hello, {name}!</h1> ) }

You can use simple expressions between the {} symbols in a template.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
export default function App() { const count = 1 const getFullName = (firstName, lastName) => `${firstName} ${lastName}` return ( <> <h2>{count + 7}</h2> <h2>{getFullName('YoungWoong', 'Park')}</h2> </> ) }

# Reactive data

In React, when data changes and the view connected to it changes too, this is called the reactivity of data.
That data is called reactive data (reactive state), or simply 'reactive' for short.
You can create reactive data with the useState function, like this.

JSX
1
2
3
4
5
6
7
import { useState } from 'react' // Declare reactive data const [dataName, setDataFunction] = useState(defaultValue) // Change reactive data setDataFunction(newValue)
/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState } from 'react' export default function App() { const [count, setCount] = useState(0) const increment = () => { setCount(count + 1) } return ( <> <p>Current count: {count}</p> <button onClick={increment}>Increase count</button> </> ) }

# Event handling

In a template, you combine the on keyword with an event name, write it as an attribute of the element, and connect the function to run.
Names combined with the on keyword must be written in camelCase, such as onClick and onKeyDown.

JSX
1
2
3
4
5
6
<div onEventName={functionToRun}></div> // Examples <div onClick={handler}></div> <div onChange={handler}></div> <div onKeyDown={handler}></div>
/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState } from 'react' export default function App() { const [count, setCount] = useState(0) const increment = () => { setCount(count + 1) } return ( <> <p>Current count: {count}</p> <button onClick={increment}>Increase count</button> </> ) }

# HTML class binding

class is a reserved word in JavaScript, so in JSX templates you must use className instead of class as the HTML attribute.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
import { useState } from 'react' import './App.css' export default function App() { const [active, setActive] = useState(false) return ( <> <h1 className={`btn ${active ? 'active' : ''}`}>Hello world!</h1> <button onClick={() => setActive(!active)}>Toggle class name!</button> </> ) }
/src/App.css
CSS
1
2
3
.active { color: red; }

When you output multiple class names depending on data, you can also write code that applies class names based on boolean data, like this.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { useState } from 'react' import './App.css' export default function App() { const [btn, setBtn] = useState(false) const [active, setActive] = useState(false) const [size, setSize] = useState(false) const getClassNames = names => Object.keys(names) .filter(name => names[name]) .join(' ') const classNames = getClassNames({ btn, active, size }) return ( <> <h1 className={classNames}>Hello world!</h1> <button onClick={() => setBtn(!btn)}> Toggle 'btn' </button> <button onClick={() => setActive(!active)}> Toggle 'active' </button> <button onClick={() => setSize(!size)}> Toggle 'size' </button> </> ) }
/src/App.css
CSS
1
2
3
4
5
6
7
8
9
.btn { border: 4px solid; } .active { color: red; } .size { font-size: 16px; }

# HTML style binding

You can apply inline styles with the style attribute.
You write them as object data, and you must write CSS properties in camelCase to follow JavaScript naming conventions.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState } from 'react' export default function App() { const [size, setSize] = useState(16) const styles = { fontSize: `${size}px`, // Manage dynamic values with reactive data! fontWeight: '700' // It is better to manage static values in a separate CSS file. } return ( <> <button onClick={() => setSize(size + 3)}> Size up! </button> <button onClick={() => setSize(size - 3)}> Size down! </button> <p style={styles}> Hello world! </p> </> ) }

# Conditional rendering

# Logical operators

You render the template (HTML) or not, depending on a condition.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState } from 'react' export default function App() { const [message, setMessage] = useState('') return ( <> <input value={message} onChange={e => setMessage(e.target.value)} /> {message.trim() && <div>You've typed something!</div>} </> ) }

# The ternary operator

You render a different template depending on a condition.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState } from 'react' export default function App() { const [isActive, setIsActive] = useState(false) const toggle = () => { setIsActive(!isActive) } return ( <> {isActive ? <h1>Active - {String(isActive)}</h1> : <h1>Inactive - {String(isActive)}</h1>} <button onClick={toggle}>Toggle</button> </> ) }

# More complex conditions

When there are two or more templates to render depending on conditions, you cannot use statements (rather than expressions) directly in the template, so you can make a separate function and use it, like this.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useState } from 'react' export default function App() { const [state, setState] = useState('') const renderStateMessage = () => { if (state === 'loading') { return <h2>Loading...</h2> } else if (state === 'success') { return <h2>Completed successfully.</h2> } else { return <h2>Waiting...</h2> } } return ( <> <button onClick={() => setState('loading')}> Loading! </button> <button onClick={() => setState('success')}> Success! </button> {renderStateMessage()} </> ) }

# List rendering

You can render a list based on data.
So that React can manage reactivity efficiently when the data changes, you must write a key attribute with a unique value for each item on every repeated element.
For the unique value of the key attribute, string or number data is generally recommended.

Object and array data are mutable reference types, so React cannot manage reactivity through the key attribute with them.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function App() { const fruits = ['Apple', 'Banana', 'Cherry'] const renderFruits = fruits.map((fruit, index) => ( <li key={index}>{fruit}</li> )) return ( <> <h1>Fruit list</h1> <ul>{renderFruits}</ul> </> ) }

You can also write it directly in the template without making a separate function, like this.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function App() { const fruits = ['Apple', 'Banana', 'Cherry'] return ( <> <h1>Fruit list</h1> <ul> {fruits.map((fruit, index) => ( <li key={index}>{fruit}</li> ))} </ul> </> ) }

# Computed data

With the useMemo function, you can create separate data that is automatically computed and updated based on other data when that data changes.
The source data must be reactive!

JSX
1
2
3
4
import { useMemo } from 'react' // Declare computed data const computedData = useMemo(() => newValue, [sourceData])
/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState, useMemo } from 'react' export default function App() { const [count, setCount] = useState(0) const double = useMemo(() => count * 2, [count]) return ( <> <h1>count: {count}</h1> <h2>double: {double}</h2> <button onClick={() => setCount(count + 1)}>Increase</button> </> ) }

# Watching data

With the useEffect function, you can create a function (callback) that watches data and runs automatically when the data changes.
The data to watch must be reactive!

JSX
1
2
3
4
import { useEffect } from 'react' // Declare a data watcher useEffect(functionToRun, [dataToWatch])
/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState, useEffect } from 'react' export default function App() { const [count, setCount] = useState(0) useEffect(() => { console.log('The count value changed:', count) }, [count]) return ( <> <h1>Count: {count}</h1> <button onClick={() => setCount(count + 1)}>Increase</button> </> ) }

If the useEffect callback runs twice in a row on the first render, consider not using <React.StrictMode>.
It is a tool that detects and warns about problems in your project, and it can intentionally render twice.
It is only active in development mode and does not affect the build output (production), so it is fine to remove it.

/src/main.js
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react' import ReactDOM from 'react-dom/client' import App from '~/App' // ReactDOM.createRoot(document.getElementById('root')).render( // <React.StrictMode> // <App /> // </React.StrictMode> // ) ReactDOM.createRoot(document.getElementById('root')).render( <> <App /> </> )

# Form input binding

Here are examples of several patterns that connect form elements such as input and textarea to React's reactive data.

# Text

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState } from 'react' export default function App() { const [text, setText] = useState('') return ( <> <input value={text} onChange={(e) => setText(e.target.value)} /> <h2>{text}</h2> </> ) }

# Multiline text

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState } from 'react' export default function App() { const [text, setText] = useState('') return ( <> <textarea value={text} onChange={(e) => setText(e.target.value)} /> <pre>{text}</pre> </> ) }

# Editable elements

With the contenteditable attribute, you can turn a regular element into an editable element and edit its text.

HTML
1
<div contenteditable>Content!</div>

However, using this attribute in React can cause problems such as virtual DOM conflicts and event jumps, so we recommend using the react-contenteditable library, which solves most of the common problems.

BASH
1
npm i react-contenteditable

Line breaks are handled with elements such as <div> and <br/>, so when there is no input value, clear the content.
You can provide a hint with the aria-placeholder attribute instead of the placeholder attribute. (You need to provide the CSS code along with it.)

TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useRef, useState } from 'react' import ContentEditable from 'react-contenteditable' export default function App() { const ref = useRef<HTMLElement>(null) const [text, setText] = useState('') function onChange() { if (ref.current && !ref.current.textContent) { setText('') } } return ( <> <ContentEditable innerRef={ref} html={text} onChange={onChange} aria-placeholder="Enter some text!" /> </> ) }
CSS
1
2
3
4
[contenteditable]:empty::before { content: attr(aria-placeholder); color: #aaa; }

# Single checkbox

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from 'react' export default function App() { const [checked, setChecked] = useState(false) return ( <> <label> <input type="checkbox" checked={checked} onChange={(e) => setChecked(e.target.checked)} /> I agree. </label> <h2>Agreed: {checked ? 'Yes' : 'No'}</h2> </> ) }

# Multiple checkboxes

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { useState } from 'react' export default function App() { const [checkedItems, setCheckedItems] = useState([]) const handleCheckboxChange = event => { const { value, checked } = event.target checked ? setCheckedItems([...checkedItems, value]) // When checked : setCheckedItems(checkedItems.filter((item) => item !== value)) // When unchecked } return ( <> <p>Pick all the foods you like!</p> <label> <input type="checkbox" value="Apple" checked={checkedItems.includes('Apple')} onChange={handleCheckboxChange} /> Apple </label> <br /> <label> <input type="checkbox" value="Banana" checked={checkedItems.includes('Banana')} onChange={handleCheckboxChange} /> Banana </label> <br /> <label> <input type="checkbox" value="Cherry" checked={checkedItems.includes('Cherry')} onChange={handleCheckboxChange} /> Cherry </label> <br /> <h2>{checkedItems.join(', ')}</h2> </> ) }

# Radio buttons

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useState } from 'react' export default function App() { const [text, setText] = useState('') return ( <> <label> <input type="radio" value="Apple" checked={text === 'Apple'} onChange={(e) => setText(e.target.value)} /> Apple </label> <label> <input type="radio" value="Banana" checked={text === 'Banana'} onChange={(e) => setText(e.target.value)} /> Banana </label> <h2>{text}</h2> </> ) }

# Single select

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from 'react' export default function App() { const [selected, setSelected] = useState('') return ( <> <select value={selected} onChange={e => setSelected(e.target.value)}> <option value="">Select one</option> <option>Apple</option> <option>Banana</option> <option>Cherry</option> </select> <h2>{selected}</h2> </> ) }

# Multiple select

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { useState } from 'react' export default function App() { const [selected, setSelected] = useState([]) const handleChange = event => { const { options } = event.target const newSelected = [] Array.from(options).forEach((option) => { if (option.selected) { newSelected.push(option.value) } }) setSelected(newSelected) } return ( <> <select multiple value={selected} onChange={handleChange} style={{ width: '100px' }}> <option>Apple</option> <option>Banana</option> <option>Cherry</option> <option>Orange</option> <option>Strawberry</option> </select> <h2>{selected.join(', ')}</h2> </> ) }

# Template (element) refs

Instead of searching for an element with document.querySelector, you can use the useRef function to refer to the template (element) directly.
The default value is null, which means the element was not found.
You check the referenced element through the current property, like inputRef.current in the following example.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useRef } from 'react' export default function App() { const inputRef = useRef(null) return ( <> <input type="text" ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus!</button> </> ) }

# Components

# Lifecycle hooks

If you write code like the following example to focus the input element automatically when it appears on the screen, an error occurs.
This is because the time when the React component's JavaScript code runs (created) is different from the time when the template is rendered on the screen (mounted).

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useRef } from 'react' export default function App() { const inputRef = useRef(null) // Focus the input element! // Error - cannot read properties of null (reading 'focus') inputRef.current.focus() return ( <> <input type="text" ref={inputRef} /> </> ) }

With the useEffect function, you can run a function after the template is rendered on the screen, like this.
The useEffect function is used to watch data, but it is also used to write a function that runs 'after the screen is ready', like this.
However, since there is no data to watch, the second argument is an empty array.

JSX
1
2
3
import { useEffect } from 'react' useEffect(functionToRun, [])

Now it works well!

JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useRef, useEffect } from 'react' export default function App() { const inputRef = useRef(null) // When the screen is ready, focus the input element! useEffect(() => { inputRef.current.focus() }, []) return ( <div> <input type="text" ref={inputRef} /> </div> ) }

If you do not pass an array as the second argument of the useEffect function, like this, the callback runs every time the component re-renders, for example because reactive data was updated.
This is not recommended in general.

JSX
1
2
3
4
// The callback runs every time the component re-renders useEffect(() => { console.log('The count value changed:') })

# Parent and child components

A component can import and use other components.
An imported component is called a child component of the component that imports it, and the importing component is called its parent component.
In the following example, TextField.jsx is a child component of App.jsx, and App.jsx is the parent component of TextField.jsx.
It is the same concept as the parent/child relationship of HTML elements.

/src/components/TextField.jsx
JSX
1
2
3
4
5
6
7
export default function TextField() { return ( <> <input type="text" /> </> ) }

In a Vite project, you can leave out the .jsx component extension in import paths.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
import TextField from './components/TextField' export default function App() { return ( <> <TextField /> <TextField /> <TextField /> </> ) }

# Props

You can pass data (reactive data, functions, and so on) from a parent component to a child component.
The data passed this way, or the attribute that carries it, is called a prop.

/src/components/TextField.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default function TextField({ label = '', hint = '', value, onChange }) { return ( <div style={{ marginBottom: '10px' }}> <label> {label && `${label}::`} <input type="text" value={value} onChange={e => onChange(e.target.value)} /> </label> {hint && ( <div style={{ marginTop: '4px', fontSize: '11px' }}> {hint} </div> )} </div> ) }

When you use the component (<TextField>), you pass data by writing it like HTML attributes, like this.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { useState } from 'react' import TextField from '@/components/TextField' export default function App() { const [name, setName] = useState('') const [age, setAge] = useState('') const [email, setEmail] = useState('') return ( <> <TextField name="Name" hint="You must enter your name in Korean." value={name} onChange={setName} /> <TextField name="Age" hint="You must enter your age in full years." value={age} onChange={setAge} /> <TextField name="Email" value={email} onChange={setEmail} /> <div> My name is {name}, I am {age} years old, and my email is {email}! </div> </> ) }

# Fallthrough props

Attributes that you write when using a component but that are not defined as props in the component are called fallthrough props.

/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useState } from 'react' import TextField from '@/components/TextField' export default function App() { const [email, setEmail] = useState('') return ( <> <TextField label="Email" value={email} onChange={setEmail} placeholder="Please enter your email." name="Email" minlength="5" maxlength="24" autocomplete="email" required /> </> ) }
Using several attributes that are not defined as props

With the spread operator (...), you can connect fallthrough props to the element you want through rest parameters, like this.

/src/components/TextField.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default function TextField({ label = '', hint = '', value, onChange, ...restProps }) { return ( <div style={{ marginBottom: '10px' }}> <label> {label && `${label}:`} <input {...restProps} value={value} onChange={e => onChange(e.target.value)} /> </label> {hint && ( <div style={{ marginTop: '4px', fontSize: '11px' }}> {hint} </div> )} </div> ) }
...restProps

# Slots

A feature that lets you insert other components or elements at a specific position in a component's template is called a slot.
You can implement slots with children, a prop with a special agreed-upon name.

/src/components/Button.jsx
JSX
1
2
3
4
5
6
7
8
9
export default function Button({ children, loading = false, ...restProps }) { return ( <button className={loading ? 'loading' : ''} {...restProps}> {children} </button> ) }
/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
import { useState } from 'react' import Button from '@/components/Button' export default function App() { const [loading, setLoading] = useState(false) return ( <> <Button>Cancel</Button> <Button loading={loading}>OK</Button> </> ) }

# CSS modules

You can create the styles of a specific component as a separate CSS file and connect it, but by default it is not a module, so it is applied globally, and if a selector matches, the style can be applied by accident to elements of some other component.
The following example shows that the styles written for Hello.jsx (Hello.css) are also applied to World.jsx.

/src/components/Hello.jsx
JSX
1
2
3
4
5
import './Hello.css' export default function Hello() { return <h1 className="title">Hello</h1> }
/src/components/Hello.css
CSS
1
2
3
.title { color: red; }
/src/components/World.jsx
JSX
1
2
3
4
export default function Hello() { // The red text color is applied! return <h1 className="title">World</h1> }
/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
import Hello from '~/components/Hello' import World from '~/components/World' export default function App() { return ( <> <Hello /> <World /> </> ) }

The style is also applied to the .title element in World.jsx

To manage styles efficiently, React lets you create styles that apply only to each individual component.
If you rename the Hello.css file to Hello.module.css, you can write styles that are valid only in that specific component (scoped).

When rendered on the screen, the .title selector is replaced with a new selector such as ._title_7f5lv_1, and the suffix added to the selector, such as _7f5lv_1, is called a style hash.

/src/components/Hello.jsx
JSX
1
2
3
4
5
import styles from './Hello.module.css' export default function Hello() { return <h1 className={styles.title}>Hello</h1> }
/src/components/Hello.module.css
CSS
1
2
3
.title { color: red; }

The style is applied only to the .title element in Hello.jsx

# Setting up SCSS

In a Vite project, once you install the sass package, you can use Sass or SCSS right away without any extra configuration.
In particular, the SCSS nesting syntax lets you manage styles much more flexibly.

BASH
1
npm i -D sass
/src/components/Hello.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import styles from './Hello.module.scss' export default function Hello({ message = '' }) { return ( <> <h1 className={styles.title}> <span>Hello</span> Component~ </h1> <p className={styles.message}> {message} </p> </> ) }
/src/components/Hello.module.scss
SCSS
1
2
3
4
5
6
7
8
9
10
11
.title { font-size: 40px; span { font-weight: bold; color: royalblue; } } .message { font-size: 20px; opacity: .7; }

# Global styles

If you want to apply some global styles inside a module, you can use the :global selector, like this.

/src/components/Hello.module.css
CSS
1
2
3
4
5
6
7
8
9
10
11
.title { font-size: 40px; span { font-weight: bold; color: royalblue; } } :global .message { font-size: 20px; opacity: .7; }