React Core Patterns
# 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.
12345export 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.
123456789export 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.
123456789<body> <div id="root"> <div> <!-- Unnecessary div element --> <header>HEADER</header> <main>MAIN</main> <footer>FOOTER</footer> </div> </div> </body>
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.
123456789export default function App() { return ( <> <header>HEADER</header> <main>MAIN</main> <footer>FOOTER</footer> </> ) }
Now it renders like this.
12345678<body> <div id="root"> <!-- No div element in between --> <header>HEADER</header> <main>MAIN</main> <footer>FOOTER</footer> </div> </body>
If you need to set attributes on a JSX fragment, you must use the <Fragment> component explicitly.
1234567891011import { 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.
1234567export default function App() { const name = 'ParkYoungWoong' return ( <h1>Hello, {name}!</h1> ) }
You can use simple expressions between the {} symbols in a template.
123456789101112export 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.
1234567import { useState } from 'react' // Declare reactive data const [dataName, setDataFunction] = useState(defaultValue) // Change reactive data setDataFunction(newValue)
12345678910111213141516import { 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.
123456<div onEventName={functionToRun}></div> // Examples <div onClick={handler}></div> <div onChange={handler}></div> <div onKeyDown={handler}></div>
12345678910111213141516import { 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.
12345678910111213import { 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> </> ) }
123.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.
1234567891011121314151617181920212223242526272829303132333435import { 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> </> ) }
123456789.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.
12345678910111213141516171819202122232425import { 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.
123456789101112131415import { 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.
123456789101112131415161718import { 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.
12345678910111213141516171819202122232425262728import { 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.
1234567891011121314export 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.
1234567891011121314export 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!
1234import { useMemo } from 'react' // Declare computed data const computedData = useMemo(() => newValue, [sourceData])
123456789101112131415import { 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!
1234import { useEffect } from 'react' // Declare a data watcher useEffect(functionToRun, [dataToWatch])
12345678910111213141516import { 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.
123456789101112131415import 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
123456789101112131415import { 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
123456789101112131415import { 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.
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.
1npm 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.)
1234567891011121314151617181920212223import { 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!" /> </> ) }
1234[contenteditable]:empty::before { content: attr(aria-placeholder); color: #aaa; }
# Single checkbox
12345678910111213141516171819import { 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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849import { 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
1234567891011121314151617181920212223242526272829import { 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
12345678910111213141516171819import { 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
123456789101112131415161718192021222324252627282930313233import { 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.
123456789101112131415import { 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).
123456789101112131415161718import { 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.
123import { useEffect } from 'react' useEffect(functionToRun, [])
Now it works well!
12345678910111213141516171819import { 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.
1234// 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.
1234567export default function TextField() { return ( <> <input type="text" /> </> ) }
In a Vite project, you can leave out the .jsx component extension in import paths.
1234567891011import 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.
1234567891011121314151617181920212223export 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.
1234567891011121314151617181920212223242526272829303132import { 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.
12345678910111213141516171819202122import { 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 /> </> ) }
With the spread operator (...), you can connect fallthrough props to the element you want through rest parameters, like this.
1234567891011121314151617181920212223export 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> ) }
# 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.
123456789export default function Button({ children, loading = false, ...restProps }) { return ( <button className={loading ? 'loading' : ''} {...restProps}> {children} </button> ) }
123456789101112import { 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.
12345import './Hello.css' export default function Hello() { return <h1 className="title">Hello</h1> }
123.title { color: red; }
1234export default function Hello() { // The red text color is applied! return <h1 className="title">World</h1> }
1234567891011import Hello from '~/components/Hello' import World from '~/components/World' export default function App() { return ( <> <Hello /> <World /> </> ) }
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).
12345import styles from './Hello.module.css' export default function Hello() { return <h1 className={styles.title}>Hello</h1> }
123.title { color: red; }
# 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.
1npm i -D sass
1234567891011121314import 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> </> ) }
1234567891011.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.
1234567891011.title { font-size: 40px; span { font-weight: bold; color: royalblue; } } :global .message { font-size: 20px; opacity: .7; }
Thanks for reading to the end.
Likes and kind comments help keep this blog going!