The article summarizes the techniques of React component design, state management, code organization and optimization, covering the use of Fragment, props deconstruction, defaultProps, key and ref usage, rendering performance optimization, etc.
1. Component related
1. Use self-closing components
// Bad writing<Component></Component> // Recommended writing method<Component />
2. It is recommended to use the Fragment component instead of the DOM element to group elements.
In React, each component must return a single element. Instead of wrapping multiple elements in <div> or <span> , use <Fragment> to keep the DOM tidy.
Bad writing: Using divs will mess up the DOM and may require more CSS code.
import Header from "./header"; import Content from "./content"; import Footer from "./footer"; const Test = () => { return ( <div> <Header /> <Content /> <Footer /> </div> ); };
Recommended writing: <Fragment> wraps elements without affecting the DOM structure.
import Header from "./header"; import Content from "./content"; import Footer from "./footer"; const Test = () => { return ( // If the element does not need to add attributes, you can use the abbreviation <></> <Fragment> <Header /> <Content /> <Footer /> </Fragment> ); };
3. Use React fragment abbreviation <></> (unless you need to set a key attribute)
Not easy to write: The following code is a bit redundant.
const Test = () => { return ( <Fragment> <Header /> <Content /> <Footer /> </Fragment> ); };
Recommended writing method:
const Test = () => { return ( <> <Header /> <Content /> <Footer /> </> ); };
Unless you need a key attribute.
const Tools = ({ tools }) => { return ( <Container> { tools?.map((item, index) => { <Fragment key={`${}-${index}`}> <span>{ }</span> <span>{ }</span> <Fragment> }) } </Container> ) }
4. Prefer props to be diversified rather than accessing each props separately
Bad writing: The following code is harder to read (especially when the project is larger).
const TodoLists = (props) => ( <div className="todo-list"> {?.map((todo, index) => ( <div className="todo-list-item" key={}> <p onClick={() => ?.(todo)}> {todo?.uuid}:{} </p> <div className="todo-list-item-btn-group"> <button type="button" onClick={() => ?.(todo, index)}> edit </button> <button type="button" onClick={() => ?.(todo, index)} > delete </button> </div> </div> ))} </div> ); export default TodoLists;
Recommended writing method: The following code is more concise.
const TodoLists = ({ todoList, seeDetail, handleEdit, handleDelete }) => ( <div className="todo-list"> {todoList?.map((todo, index) => ( <div className="todo-list-item" key={}> <p onClick={() => seeDetail?.(todo)}> {todo?.uuid}:{} </p> <div className="todo-list-item-btn-group"> <button type="button" onClick={() => handleEdit?.(todo, index)}> edit </button> <button type="button" onClick={() => handleDelete?.(todo, index)}> delete </button> </div> </div> ))} </div> ); export default TodoLists;
5. When setting the default value of props, it will be performed during deconstruction.
Bad writing: You may need to define default values in multiple places and introduce new variables.
const Text = ({ size, type }) => { const Component = type || "span"; const comSize = size || "mini"; return <Component size={comSize} />; };
Recommended writing method, directly give the default value in object destruction.
const Text = ({ size = "mini", type: Component = "span" }) => { return <Component size={comSize} />; };
6. Remove curly braces when passing string type attributes.
Bad writing: Braces
<button type={"button"} className={"btn"}> Button </button>
Recommended writing: no curly braces are required
<button type="button" className="btn"> Button </button>
7. Make sure the value is a Boolean before using value && <Component {...props}/> to prevent unexpected values from being displayed.
Bad writing: When the length of the list is 0, it is possible to display 0.
const DataList = ({ data }) => { return <Container>{ && <List data={data} />}</Container>; };
Recommended writing: When the list has no data, nothing will be rendered.
const DataList = ({ data }) => { return <Container>{ > 0 && <List data={data} />}</Container>; };
8. Use functions (inlined or non-inlined) to avoid intermediate variables contaminating your context
Bad writing: variables totalCount and totalPrice confuses the context of the component.
const GoodList = ({ goods }) => { if ( === 0) { return <>No data yet</>; } let totalCount = 0; let totalPrice = 0; ((good) => { totalCount += ; totalPrice += ; }); return ( <> Total quantity:{totalCount};Total price:{totalPrice} </> ); };
Recommended writing: Control the variables totalCount and totalPrice within a function.
const GoodList = ({ goods }) => { if ( === 0) { return <>No data yet</>; } //Use functions const { totalCount, totalPrice, } = () => { let totalCount = 0, totalPrice = 0; ((good) => { totalCount += ; totalPrice += ; }); return { totalCount, totalPrice }; }; return ( <> Total quantity:{totalCount};Total price:{totalPrice} </> ); };
My personal favorite writing method: encapsulate it into hooks for use.
const useTotalGoods = ({ goods }) => { let totalCount = 0, totalPrice = 0; ((good) => { totalCount += ; totalPrice += ; }); return { totalCount, totalPrice }; }; const GoodList = ({ goods }) => { if ( === 0) { return <>No data yet</>; } const { totalCount, totalPrice } = useTotalGoods(goods); return ( <> Total quantity:{totalCount};Total price:{totalPrice} </> ); };
9. Reuse logic using curry functions (and cache callback functions correctly)
Bad writing: The form update field is repeated.
const UserLoginForm = () => { const [{ username, password }, setFormUserState] = useState({ username: "", password: "", }); return ( <> <h1>Log in</h1> <form> <div class="form-item"> <label>username:</label> <input placeholder="Please enter a username" value={username} onChange={(e) => setFormUserState((state) => ({ ...state, username: , })) } /> </div> <div class="form-item"> <label>password:</label> <input placeholder="Please enter your password" value={username} type="password" onChange={(e) => setFormUserState((state) => ({ ...state, password: , })) } /> </div> </form> </> ); };
Recommended writing method: Introduce the createFormValueChangeHandler method to return the correct processing method for each field.
Note: This trick is especially useful if you enable the ESLint rule jsx-no-bind. You just wrap the curry function in useCallback.
const UserLoginForm = () => { const [{ username, password }, setFormUserState] = useState({ username: "", password: "", }); const createFormValueChangeHandler = (field: string) => { return (e) => { setFormUserState((state) => ({ ...state, [field]: , })); }; }; return ( <> <h1>Log in</h1> <form> <div class="form-item"> <label>username:</label> <input placeholder="Please enter a username" value={username} onChange={createFormValueChangeHandler("username")} /> </div> <div class="form-item"> <label>password:</label> <input placeholder="Please enter your password" value={username} type="password" onChange={createFormValueChangeHandler("password")} /> </div> </form> </> ); };
10. Move data that does not depend on component props/state to outside the component for cleaner (and more efficient) code
Bad writing: OPTIONS and renderOption do not need to be inside the component because they do not depend on any props or state. Also, keeping them internally means we get a new object reference every time the component renders. If we pass renderOption to a child component wrapped in a memo, it will break the cache function.
const ToolSelector = () => { const options = [ { label: "html tool", value: "html-tool", }, { label: "css tool", value: "css-tool", }, { label: "js tools", value: "js-tool", }, ]; const renderOption = ({ label, value, }: { label?: string; value?: string; }) => <Option value={value}>{label}</Option>; return ( <Select placeholder="Please select a tool"> {((item, index) => ( <Fragment key={`${}-${index}`}>{renderOption(item)}</Fragment> ))} </Select> ); };
Recommended writing: Remove them from components to keep components clean and reference stable.
const options = [ { label: "html tool", value: "html-tool", }, { label: "css tool", value: "css-tool", }, { label: "js tools", value: "js-tool", }, ]; const renderOption = ({ label, value }: { label?: string; value?: string }) => ( <Option value={value}>{label}</Option> ); const ToolSelector = () => { return ( <Select placeholder="Please select a tool"> {((item, index) => ( <Fragment key={`${}-${index}`}>{renderOption(item)}</Fragment> ))} </Select> ); };
Note: In this example, you can simplify it further by using option elements inline.
const options = [ { label: "html tool", value: "html-tool", }, { label: "css tool", value: "css-tool", }, { label: "js tools", value: "js-tool", }, ]; const ToolSelector = () => { return ( <Select placeholder="Please select a tool"> {((item, index) => ( <Option value={} key={`${}-${index}`}> {} </Option> ))} </Select> ); };
11. When storing the selected object in the list component, the object ID is stored, not the entire object
Bad writing: If an object is selected but then it changes (i.e., we receive a brand new object reference with the same ID), or the object no longer exists in the list, the selectedItem will retain the outdated value or become incorrect.
const List = ({ data }) => { // The reference is the entire selected object const [selectedItem, setSelectedItem] = useState<Item | undefined>(); return ( <> {selectedItem && <div>{}</div>} <List data={data} onSelect={setSelectedItem} selectedItem={selectedItem} /> </> ); };
Recommended writing: We store the selected list object through ID (should be stable). This ensures that the UI should be correct even if the list object is removed from the list or if one of its properties changes.
const List = ({ data }) => { const [selectedItemId, setSelectedItemId] = useState<string | number>(); // We find the selected list object based on the selected id from the list const selectedItem = ((item) => === selectedItemId); return ( <> {selectedItemId && <div>{}</div>} <List data={data} onSelect={setSelectedItemId} selectedItemId={selectedItemId} /> </> ); };
12. If you need to use the value in prop many times, then introduce a new component.
Bad way to write: The code is confused due to the check of type === null.
Note: Due to hooks rules, we cannot return null in advance.
const CreatForm = ({ type }) => { const formList = useMemo(() => { if (type === null) { return []; } return getFormList({ type }); }, [type]); const onHandleChange = useCallback( (id) => { if (type === null) { return; } // do something }, [type] ); if (type === null) { return null; } return ( <> {(({ value, id, ...rest }, index) => ( < value={value} onChange={onHandleChange} key={id} {...rest} /> ))} </> ); };
Recommended writing: We have introduced a new component, FormLists, which takes defined form item components and is more concise.
const FormList = ({ type }) => { const formList = useMemo(() => getFormList({ type }), [type]); const onHandleChange = useCallback( (id) => { // do something }, [type] ); return ( <> {(({ value, id, ...rest }, index) => ( < value={value} onChange={onHandleChange} key={id} {...rest} /> ))} </> ); }; const CreateForm = ({ type }) => { if (type === null) { return null; } return <FormList type={type} />; };
13. Group all states and contexts to the top of the component
When all states and contexts are on top, it's easy to find out which factors trigger component re-rendering.
Bad writing: The state and context are scattered and difficult to track.
const LoginForm = () => { const [username, setUsername] = useState(""); const onHandleChangeUsername = (e) => { setUserName(); }; const [password, setPassword] = useState(""); const onHandleChangePassword = (e) => { setPassword(); }; const theme = useContext(themeContext); return ( <div class={`login-form login-form-${theme}`}> <h1>login</h1> <form> <div class="login-form-item"> <label>username:</label> <input value={username} onChange={onHandleChangeUsername} placeholder="Please enter a username" /> </div> <div class="login-form-item"> <label>password:</label> <input value={password} onChange={onHandleChangePassword} placeholder="Please enter your password" type="password" /> </div> </form> </div> ); };
Recommended writing: All states and contexts are concentrated at the top for quick positioning.
const LoginForm = () => { // context const theme = useContext(themeContext); // state const [password, setPassword] = useState(""); const [username, setUsername] = useState(""); // method const onHandleChangeUsername = (e) => { setUserName(); }; const onHandleChangePassword = (e) => { setPassword(); }; return ( <div class={`login-form login-form-${theme}`}> <h1>login</h1> <form> <div class="login-form-item"> <label>username:</label> <input value={username} onChange={onHandleChangeUsername} placeholder="Please enter a username" /> </div> <div class="login-form-item"> <label>password:</label> <input value={password} onChange={onHandleChangePassword} placeholder="Please enter your password" type="password" /> </div> </form> </div> ); };
2. Effective design patterns and techniques
14. Use children attributes to get clearer code (and performance benefits)
Leverage child component props for cleaner code (and performance benefits). There are several benefits to using child components props:
- Benefit 1: You can avoid props mixing by passing props directly to child components instead of routing them through parent components.
- Benefit 2: Your code is more extensible because you can easily modify child components without changing parent components.
- Benefit 3: You can use this trick to avoid re-rendering the component (see the example below).
Bad writing: Whenever Timer renders, OtherSlowComponent renders, which happens every time the current time is updated.
const Container = () => <Timer />; const Timer = () => { const [time, setTime] = useState(0); useEffect(() => { const intervalId = setInterval(() => setTime(new Date()), 1000); return () => { clearInterval(intervalId); }; }, []); return ( <> <h1>Current time:{dayjs(time).format("YYYY-MM-DD HH:mm:ss")}</h1> <OtherSlowComponent /> </> ); };
Recommended writing: When Timer is rendered, OtherSlowComponent will not be rendered.
const Container = () => ( <Timer> <OtherSlowComponent /> </Timer> ); const Timer = ({ children }) => { const [time, setTime] = useState(0); useEffect(() => { const intervalId = setInterval(() => setTime(new Date()), 1000); return () => { clearInterval(intervalId); }; }, []); return ( <> <h1>Current time:{dayjs(time).formate("YYYY-MM-DD HH:mm:ss")}</h1> {children} </> ); };
15. Build composite code using composite components
Use composite components like building blocks, piece them together to create a custom UI. These components work great when creating libraries, producing expressive and highly scalable code. Here is a code that is considered an example:
<Menu> <MenuButton> Let's do it <span aria-hidden>▾</span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert("download")}>download</MenuItem> <MenuItem onSelect={() => alert("copy")}>Create a copy</MenuItem> <MenuLink as="a" href="/menu-button/" rel="external nofollow" > Jump link </MenuLink> </MenuList> </Menu>
16. Use rendering functions or component functions to make your code more extensible
Suppose we want to display various lists, such as messages, profiles, or posts, and each list should be sortable.
To achieve this, we introduced a List component for reuse. We can solve this problem in two ways:
Bad writing: Option 1.
List handles the rendering of each project and how it sorts. This is problematic because it violates the principle of openness and closure. This code is modified whenever a new project type is added.
:
export interface ListItem { id: string; } // Bad list component writing// We also need to understand these interfacestype PostItem = ListItem & { title: string }; type UserItem = ListItem & { name: string; date: Date }; type ListNewItem = | { type: "post"; value: PostItem } | { type: "user"; value: UserItem }; interface BadListProps<T extends ListNewItem> { type: T["type"]; items: Array<T["value"]>; } const SortList = <T extends ListNewItem>({ type, items }: BadListProps<T>) => { const sortItems = [...items].sort((a, b) => { // We also need to pay attention to the comparison logic here, or directly use the comparison function exported below return 0; }); return ( <> <h2>{type === "post" ? "Post" : "user"}</h2> <ul className="sort-list"> {((item, index) => ( <li className="sort-list-item" key={`${}-${index}`}> {(() => { switch (type) { case "post": return (item as PostItem).title; case "user": return ( <> <span>{(item as UserItem).name}</span> <span> - </span> <em> Join time: {(item as UserItem).()} </em> </> ); } })()} </li> ))} </ul> </> ); }; export function compareStrings(a: string, b: string): number { return a < b ? -1 : a == b ? 0 : 1; }
Recommended writing method: Option 2.
List takes rendering functions or component functions and calls them only if needed.
:
export interface ListItem { id: string; } interface ListProps<T extends ListItem> { items: T[]; // List data header: ; // Header components itemRender: (item: T) => ; // List item itemCompare: (a: T, b: T) => number; // Custom sorting function for list items} const SortList = <T extends ListItem>({ items, header: Header, itemRender, itemCompare, }: ListProps<T>) => { const sortedItems = [...items].sort(itemCompare); return ( <> <Header /> <ul className="sort-list"> {((item, index) => ( <li className="sort-list-item" key={`${}-${index}`}> {itemRender(item)} </li> ))} </ul> </> ); }; export default SortList;
The complete sample code can be viewed here.
17. When handling different situations, use value === case && <Component /> to avoid retaining the old state
Bad writing: In the following example, the counter count does not reset when switching. This happens because when rendering the same component, its state remains unchanged after the currentTab changes.
:
const tabList = [ { label: "front page", value: "tab-1", }, { label: "Details Page", value: "tab-2", }, ]; export interface TabItem { label: string; value: string; } export interface TabProps { tabs: TabItem[]; currentTab: string | TabItem; onTab: (v: string | TabItem) => void; labelInValue?: boolean; } const Tab: <TabProps> = ({ tabs = tabList, currentTab, labelInValue, onTab, }) => { const currentTabValue = useMemo( () => (labelInValue ? (currentTab as TabItem)?.value : currentTab), [currentTab, labelInValue] ); return ( <div className="tab"> {tabs?.map((item, index) => ( <div className={`tab-item${ currentTabValue === ? " active" : "" }`} key={`${}-${index}`} onClick={() => onTab?.(labelInValue ? item : )} > {} </div> ))} </div> ); }; export default Tab;
:
export interface ResourceProps { type: string; } const Resource: <ResourceProps> = ({ type }) => { const [count, setCount] = useState(0); const onHandleClick = () => { setCount((c) => c + 1); }; return ( <div className="tab-content"> You are currently{type === "tab-1" ? "front page" : "Details Page"}, <button onClick={onHandleClick} className="btn" type="button"> Click me </button> Increase visits{count}frequency </div> ); };
Recommended writing: Render components according to currentTab or force rerendering with key when type changes.
function App() { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> {currentTab === "tab-1" && <Resource type="tab-1" />} {currentTab === "tab-2" && <Resource type="tab-2" />} </> ); } // Use key attributefunction App() { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> <Resource type={currentTab} key={currentTab} /> </> ); }
The complete sample code can be viewed here.
18. Always use error boundaries to handle component rendering errors
By default, if your application encounters an error during rendering, the entire UI will crash.
To prevent this, use the error boundary to:
- Keep some parts of the application running properly even if an error occurs.
- Displays user-friendly error messages and optionally tracks errors.
Tip: You can use the react-error-boundary library.
3. key and ref
19. Use or generate key
JSX elements in map calls (that is, list rendering) always require a key.
Assume that your element does not have a key yet. In this case, you can use the , or the uuid library to generate a unique ID.
Note: Please note that it is not defined in the old browser.
20. Make sure your list item ids are stable (ie: they will not change in rendering)
Make id/key stable as much as possible.
Otherwise, React may uselessly re-render certain components, or trigger some functional exceptions, as shown in the following example.
Bad writing: selectItemId changes every time the App component renders, so the value of setting id will never be correct.
const App = () => { const [items, setItems] = useState([]); const [selectItemId, setSelectItemId] = useState(undefined); const loadItems = () => { fetchItems().then((res) => setItems(res)); }; // Request list useEffect(() => { loadItems(); }, []); // Add list id, this is a bad way to do it const newItems = ((item) => ({ ...item, id: () })); return ( <List items={newItems} selectedItemId={selectItemId} onSelectItem={setSelectItemId} /> ); };
Recommended writing method: Add id when we get list items.
const App = () => { const [items, setItems] = useState([]); const [selectItemId, setSelectItemId] = useState(undefined); const loadItems = () => { // Get the list data and save it with id fetchItems().then((res) => // Once the result is obtained, we will add "id" setItems(((item) => ({ ...item, id: () }))) ); }; // Request list useEffect(() => { loadItems(); }, []); return ( <List items={items} selectedItemId={selectItemId} onSelectItem={setSelectItemId} /> ); };
21. Strategically use the key attribute to trigger component re-rendering
Want to force components to re-render from scratch? Just change its key attribute.
In the following example, we use this trick to reset the error boundary when switching to a new tab. (This example is based on the example shown in point 17 earlier)
:
export interface ResourceProps { type: string; } const Resource: <ResourceProps> = ({ type }) => { const [count, setCount] = useState(0); const onHandleClick = () => { setCount((c) => c + 1); }; // Added code to throw exception useEffect(() => { if (type === "tab-1") { throw new Error("This option is not toggle"); } }, []); return ( <div className="tab-content"> You are currently{type === "tab-1" ? "front page" : "Details Page"}, <button onClick={onHandleClick} className="btn" type="button"> Click me </button> Increase visits{count}frequency </div> ); };
:
import { ErrorBoundary } from "react-error-boundary"; const App = () => { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> <ErrorBoundary fallback={<div className="error">Some errors occurred in component rendering</div>} key={currentTab} // If there is no key attribute, an error will also be presented when the currentTab value is "tab-2". > <Resource type={currentTab} /> </ErrorBoundary> </> ); };
The complete sample code can be viewed here.
22. Use the ref callback function to perform tasks such as monitoring size changes and managing multiple node elements.
Did you know that you can pass a function to a ref property instead of a ref object?
It works as follows:
- When a DOM node is added to the screen, React calls the function with the DOM node as a parameter.
- When the DOM node is removed, React calls the function with null.
In the following example, we use this trick to skip useEffect.
Bad writing: Use useEffect to focus on the input frame
const FocusInput = () => { const ref = useRef<HTMLInputElement>(); useEffect(() => { ?.focus(); }, []); return <input ref={ref} type="text" />; };
Recommended writing: We focus on the input immediately when it is available.
const FocusInput = () => { const ref = useCallback((node) => node?.focus(), []); return <input ref={ref} type="text" />; };
IV. Organize react code
23. Put React components together with their resources (such as styles, images, etc.)
Always put each React component together with relevant resources such as styles and images.
This way, it is easier to delete components when they are no longer needed.
It also simplifies code navigation because everything you need is centered in one place.
24. Limit component file size
Large files containing a lot of components and exported content can be confusing.
Additionally, as more content is added, they tend to get bigger.
So, target a reasonable file size and split the component into separate files if it is reasonable.
25. Limit the number of return statements in the functional component file
Multiple return statements in a functional component make it difficult to see what the component returns.
This is not a problem for class components that we can search for rendering terms.
A convenient trick is to use arrow functions without brackets whenever possible (VSCode has an operation for this).
Bad writing: It is even harder to find component return statements.
export interface UserInfo { id: string; name: string; age: number; } export interface UserListProps { users: UserInfo[]; searchUser: string; onSelectUser: (u: UserInfo) => void; } const UserList: <UserListProps> = ({ users, searchUser, onSelectUser, }) => { // Extra return statement const filterUsers = users?.filter((user) => { return (searchUser); }); const onSelectUserHandler = (user) => { // Extra return statement return () => { onSelectUser(user); }; }; return ( <> <h2>User List</h2> <ul> {((user, index) => { return ( <li key={`${}-${index}`} onClick={onSelectUserHandler(user)}> <p> <span>userid</span> <span>{}</span> </p> <p> <span>user名</span> <span>{}</span> </p> <p> <span>user年龄</span> <span>{}</span> </p> </li> ); })} </ul> </> ); };
Recommended writing: The component only has one return statement.
export interface UserInfo { id: string; name: string; age: number; } export interface UserListProps { users: UserInfo[]; searchUser: string; onSelectUser: (u: UserInfo) => void; } const UserList: <UserListProps> = ({ users, searchUser, onSelectUser, }) => { const filterUsers = users?.filter((user) => (searchUser)); const onSelectUserHandler = (user) => () => onSelectUser(user); return ( <> <h2>User List</h2> <ul> {((user, index) => ( <li key={`${}-${index}`} onClick={onSelectUserHandler(user)}> <p> <span>userid</span> <span>{}</span> </p> <p> <span>user名</span> <span>{}</span> </p> <p> <span>user年龄</span> <span>{}</span> </p> </li> ))} </ul> </> ); };
26. Prefer named exports instead of default exports
Let's compare these two methods:
//Default exportexport default function App() { // Component content} // Name and exportexport function App() { // Component content}
We now import the components as follows:
// Default importimport App from "/path/to/App"; // Name importimport { App } from "/path/to/App";
There are some problems with default export:
- If the component is renamed, the editor will not automatically rename and export.
For example, if you rename the App to Index, we will get the following:
// The default import name has not been changedimport App from "/path/to/Index"; // Name import name has been changedimport { Index } from "/path/to/Index";
- It's hard to see what is exported from a file with default export.
For example, in the case of naming import, once we enterimport { } from "/path/to/file"
, when I put the cursor in brackets I get autocomplete.
- It is difficult to export again by default.
For example, if I want to re-export the App component from the index file, I have to do the following:
export { default as App } from "/path/to/App";
The solution using named export is more straightforward.
export { App } from "/path/to/App";
Therefore, it is recommended to use named export by default.
Note: Even if you are using React lazy, you can still export using named. See the introduction example here.
V. Efficient status management
27. Never create a new state for values that can be derived from other states or props
The more state = the more trouble.
Each state may trigger re-rendering and make resetting state troublesome.
Therefore, if you can derive the value from state or props, add a new state by skipping.
Bad practice: filteredUsers do not need to be in state.
const FilterUserComponent = ({ users }) => { const [filters, setFilters] = useState([]); // Created a new state const [filteredUsers, setFilteredUsers] = useState([]); const filterUsersMethod = (filters, users) => { // Filter logic method }; useEffect(() => { setFilteredUsers(filterUsersMethod(filters, users)); }, [users, filters]); return ( <Card> <Filters filters={filters} onChangeFilters={setFilters} /> { > 0 && <UserList users={filteredUsers} />} </Card> ); };
Recommended practices: filteredUsers are determined by users and filters.
const FilterUserComponent = ({ users }) => { const [filters, setFilters] = useState([]); const filterUsersMethod = (filters, users) => { // Filter logic method }; const filteredUsers = filterUsersMethod(filters, users); return ( <Card> <Filters filters={filters} onChangeFilters={setFilters} /> { > 0 && <UserList users={filteredUsers} />} </Card> ); };
28. Create state inside components that only need to be updated to reduce component re-rendering
Whenever the state inside the component changes, React re-renders the component and all its subcomponents (except for subcomponents wrapped in memo).
This happens even if these subcomponents do not use the changed state. To minimize rerendering, move the state below the component tree as much as possible.
Bad practice: When the type changes, the LeftList and RightList components that do not depend on the type state will also trigger re-rendering.
const App = () => { const [type, setType] = useState(""); return ( <Container> <LeftList /> <Main type={type} setType={setType} /> <RightList /> </Container> ); }; const mainBtnList = [ { label: "front page", value: "home", }, { label: "Details Page", value: "detail", }, ]; const Main = ({ type, setType }) => { return ( <> {((item, index) => ( <Button className={`${ === type ? "active" : ""}`} key={`${}-${index}`} onClick={() => setType()} > {} </Button> ))} </> ); };
Recommended practice: Couple state into the Main component, affecting only the re-rendering of the Main component.
const App = () => { return ( <Container> <LeftList /> <Main /> <RightList /> </Container> ); }; const mainBtnList = [ { label: "front page", value: "home", }, { label: "Details Page", value: "detail", }, ]; const Main = () => { const [type, setType] = useState(""); return ( <> {((item, index) => ( <Button className={`${ === type ? "active" : ""}`} key={`${}-${index}`} onClick={() => setType()} > {} </Button> ))} </> ); };
29. Definitions need to clarify the difference between the initial state and the current state
Bad practice: Not clear that userInfo is just the initial value, which can cause confusion or errors in state management.
const UserInfo = ({ userInfo }) => { const [userInfo, setUserInfo] = useState(userInfo); return ( <Card> <Title>Current user: {userInfo?.name}</Title> <UserInfoDetail detail={userInfo?.detail} /> </Card> ); };
Recommended practice: Naming can clearly indicate what is the initial state and what is the current state.
const UserInfo = ({ initialUserInfo }) => { const [userInfo, setUserInfo] = useState(initialUserInfo); return ( <Card> <Title>Current user: {userInfo?.name}</Title> <UserInfoDetail detail={userInfo?.detail} /> </Card> ); };
30. Update status based on previous status, especially when using useCallback for cache
React allows you to pass update functions from useState to set functions.
This update function uses the current state to calculate the next state.
This behavior can be used whenever a state needs to be updated based on previous states, especially inside functions wrapped with useCallback. In fact, this approach avoids using state as one of the hook dependencies.
Bad practice: Whenever todoList changes, onHandleAddTodo and onHandleRemoveTodo will change.
const App = () => { const [todoList, setTodoList] = useState([]); const onHandleAddTodo = useCallback( (todo) => { setTodoList([...todoList, todo]); }, [todoList] ); const onHandleRemoveTodo = useCallback( (todo) => { setTodoList([...todoList].filter((item) => !== )); }, [todoList] ); return ( <div className="App"> <TodoInput onAddTodo={onHandleAddTodo} /> <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} /> </div> ); };
Recommended practice: Even if todoList changes, onHandleAddTodo and onHandleRemoveTodo remain the same.
const App = () => { const [todoList, setTodoList] = useState([]); const onHandleAddTodo = useCallback((todo) => { setTodoList((prevTodoList) => [...prevTodoList, todo]); }, []); const onHandleRemoveTodo = useCallback((todo) => { setTodoList((prevTodoList) => [...prevTodoList].filter((item) => !== ) ); }, []); return ( <div className="App"> <TodoInput onAddTodo={onHandleAddTodo} /> <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} /> </div> ); };
31. Use functions in useState for delay initialization and improve performance because they are called only once.
Using functions in useState ensures that the initial state is calculated only once.
This can improve performance, especially when the initial state comes from a "expensive" operation (such as reading from local storage).
Bad practice: Every time the component renders, we read the topic from local storage.
const THEME_LOCAL_STORAGE_KEY = "page_theme_key"; const Theme = ({ theme, onChangeTheme }) => { // .... }; const App = ({ children }) => { const [theme, setTheme] = useState( (THEME_LOCAL_STORAGE_KEY) || "dark" ); const onChangeTheme = (theme: string) => { setTheme(theme); (THEME_LOCAL_STORAGE_KEY, theme); }; return ( <div className={`app${theme ? ` ${theme}` : ""}`}> <Theme onChange={onChangeTheme} theme={theme} /> <div>{children}</div> </div> ); };
Recommended practice: When the component is mounted, we will only read the local storage once.
// ... const App = ({ children }) => { const [theme, setTheme] = useState( () => (THEME_LOCAL_STORAGE_KEY) || "dark" ); const onChangeTheme = (theme: string) => { setTheme(theme); (THEME_LOCAL_STORAGE_KEY, theme); }; return ( <div className={`app${theme ? ` ${theme}` : ""}`}> <Theme onChange={onChangeTheme} theme={theme} /> <div>{children}</div> </div> ); };
32. Use React context to handle widely needed static states to prevent prop drilling
Whenever I have some data, I use the React context:
- Need in multiple places (e.g. topic, current user, etc.)
- Mainly static or read-only (i.e., the user cannot/does not change the data frequently)
- This approach helps avoid prop drilling (i.e., passing data or state through multiple layers of the component hierarchy).
Let's see some code for an example:
// UserInfo interface comes from test data export const userInfoContext = createContext<string | UserInfoData>("loading"); export const useUserInfo = <T extends UserInfoData>() => { const value = useContext(userInfoContext); if (value == null) { throw new Error("Make sure to wrap the userInfoContext inside provider"); } return value as T; };
function App() { const [userInfoData, setUserInfoData] = useState<UserInfoData | string>( "loading" ); useEffect(() => { getCurrentUser().then(setUserInfoData); }, []); if (userInfoData === "loading") { return <Loading />; } return ( <div className="app"> < value={userInfoData}> <Header /> <Sidebar /> <Main /> </> </div> ); }
:
const Header: <HeaderProps> = (props) => { // Use context const userInfo = useUserInfo(); return ( <header className="header" {...props}> Welcome back{userInfo?.name} </header> ); };
:
const Main: <MainProps> = ({ title }) => { const { posts } = useUserInfo(); return ( <div className="main"> <h2 className="title">{title}</h2> <ul className="list"> {posts?.map((post, index) => ( <li className="list-item" key={`${}-${index}`}> {} </li> ))} </ul> </div> ); };
33. React Context: Divide the react context into frequently changing parts and infrequently changing parts to improve application performance
A challenge with React contexts is that as long as the context data changes, all components that use that context will be rerendered, even if they do not use the changed context section.
What is the solution? Use a separate context.
In the following example, we create two contexts: one for operations (constant) and the other for state (can be changed).
export interface TodosInfoItem { id?: string; title?: string; completed?: boolean; } export interface TodosInfo { search?: string; todos: TodosInfoItem[]; } export const todosStateContext = createContext<TodosInfo>(void 0); export const todosActionContext = createContext<Dispatch<ReducerActionParams>>( void 0 ); export interface ReducerActionParams extends TodosInfoItem { type?: string; value?: string; } export const getTodosReducer = ( state: TodosInfo, action: ReducerActionParams ) => { switch () { case TodosActionType.ADD_TODO: return { ...state, todos: [ ..., { id: (), title: , completed: false, }, ], }; case TodosActionType.REMOVE_TODO: return { ...state, todos: [...].filter((item) => !== ), }; case TodosActionType.TOGGLE_TODO_STATUS: return { ...state, todos: [...].map((item) => === ? { ...item, completed: ! } : item ), }; case TodosActionType.SET_SEARCH_TERM: return { ...state, search: , }; default: return state; } };
The complete sample code is available here.
34. React Context: When the value calculation is not direct, the Provider component is introduced
Bad practice: There is too much logic inside the App to manage theme context.
const THEME_LOCAL_STORAGE_KEY = "current-project-theme"; const DEFAULT_THEME = "light"; const ThemeContext = createContext({ theme: DEFAULT_THEME, setTheme: () => null, }); const App = () => { const [theme, setTheme] = useState( () => (THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME ); useEffect(() => { if (theme !== "system") { updateRootElementTheme(theme); return; } // We need to obtain the theme class to be applied based on the system theme const systemTheme = ("(prefers-color-scheme: dark)") .matches ? "dark" : "light"; updateRootElementTheme(systemTheme); // Then observe the changes in the system theme and update the root element accordingly const darkThemeMq = ("(prefers-color-scheme: dark)"); const listener = (event) => { updateRootElementTheme( ? "dark" : "light"); }; ("change", listener); return () => ("change", listener); }, [theme]); const themeContextValue = { theme, setTheme: (theme) => { (THEME_LOCAL_STORAGE_KEY, theme); setTheme(theme); }, }; const [selectedUserId, setSelectedUserId] = useState(undefined); const onUserSelect = (id) => { // To be done: some logic setSelectedUserId(id); }; const users = useSWR("/api/users", fetcher); return ( <div className="App"> < value={themeContextValue}> <UserList users={users} onUserSelect={onUserSelect} selectedUserId={selectedUserId} /> </> </div> ); };
Recommended: Theme context-related logic is encapsulated in ThemeProvider.
const THEME_LOCAL_STORAGE_KEY = "current-project-theme"; const DEFAULT_THEME = "light"; const ThemeContext = createContext({ theme: DEFAULT_THEME, setTheme: () => null, }); const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState( () => (THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME ); useEffect(() => { if (theme !== "system") { updateRootElementTheme(theme); return; } // We need to obtain the theme class to be applied based on the system theme const systemTheme = ("(prefers-color-scheme: dark)") .matches ? "dark" : "light"; updateRootElementTheme(systemTheme); // Then observe the changes in the system theme and update the root element accordingly const darkThemeMq = ("(prefers-color-scheme: dark)"); const listener = (event) => { updateRootElementTheme( ? "dark" : "light"); }; ("change", listener); return () => ("change", listener); }, [theme]); const themeContextValue = { theme, setTheme: (theme) => { (THEME_LOCAL_STORAGE_KEY, theme); setTheme(theme); }, }; return ( <div className="App"> < value={themeContextValue}> {children} </> </div> ); }; const App = () => { const [selectedUserId, setSelectedUserId] = useState(undefined); const onUserSelect = (id) => { // To be done: some logic setSelectedUserId(id); }; const users = useSWR("/api/users", fetcher); return ( <div className="App"> <ThemeProvider> <UserList users={users} onUserSelect={onUserSelect} selectedUserId={selectedUserId} /> </ThemeProvider> </div> ); };
35. Consider using useReducer hook as a lightweight state management solution
I use useReducer whenever there are too many values in my state or complex state and don't want to rely on external libraries.
It is particularly effective for a wider range of state management needs when used in conjunction with context.
Example: Here.
36. Use useImmer or useImmerReducer to simplify status updates
When using hooks like useState and useReducer, the state must be immutable (i.e., all changes require the creation of a new state, rather than modifying the current state).
This is usually difficult to achieve.
This is where useImmer and useImmerReducer provide simpler alternatives. They allow you to write "mutable" code that is automatically converted to immutable updates.
Bad practice: We have to be careful to make sure we are creating a new state object.
export const App = () => { const [{ email, password }, setState] = useState({ email: "", password: "", }); const onEmailChange = (event) => { setState((prevState) => ({ ...prevState, email: })); }; const onPasswordChange = (event) => { setState((prevState) => ({ ...prevState, password: })); }; return ( <div className="App"> <h1>Welcome to log in</h1> <div class="form-item"> <label>Email number: </label> <input type="email" value={email} onChange={onEmailChange} /> </div> <div className="form-item"> <label>password:</label> <input type="password" value={password} onChange={onPasswordChange} /> </div> </div> ); };
Recommended practice: More direct, we can directly modify draftState.
import { useImmer } from "use-immer"; export const App = () => { const [{ email, password }, setState] = useImmer({ email: "", password: "", }); const onEmailChange = (event) => { setState((draftState) => { = ; }); }; const onPasswordChange = (event) => { setState((draftState) => { = ; }); }; // Remaining code};
37. Use Redux (or other state management solutions) to access complex client states across multiple components
I turn to Redux whenever the following happens:
I have a complex FE application with a lot of shared client states (e.g., dashboard application)
- I want the user to be able to go back in time and resume changes.
- I don't want my components to be rerendered unnecessarily like using React context.
- I have too many contexts that are difficult to control at first.
For a simplified experience, I recommend using redux-tooltkit.
💡 Note: You can also consider other alternatives to Redux, such as Zustand or Recoil.
38. Redux: Debug your status using Redux DevTools
The Redux DevTools browser extension is a useful tool for debugging Redux projects.
It allows you to visualize your state and operations in real time, keep state persistent when refreshed, and more.
To see what it is for, watch this amazing video.
6. React code optimization
39. Use memo to prevent unnecessary re-rendering
When dealing with components that are expensive to render and whose parent components are frequently updated, wrapping them in a memo may change the rendering rules.
memo ensures that the component re-renders only when its props change, not just because its parent component re-renders.
In the following example, I get some data from the server via useGetInfoData. If the data has not changed, wrapping UserInfoList in a memo will prevent it from re-rendering when other parts of the data are updated.
export const App = () => { const { currentUserInfo, users } = useGetInfoData(); return ( <div className="App"> <h1>Information panel</h1> <CurrentUserInfo data={currentUserInfo} /> <UserInfoList users={users} /> </div> ); }; const UserInfoList = memo(({ users }) => { // Remaining implementation});
Once the React compiler becomes stable, this trick may no longer be useful.
40. Use memo to specify an equality function to indicate how React compares props.
By default, memo uses each prop to its previous value.
However, for more complex or specific scenarios, specifying a custom equality function may be more efficient than default comparison or re-rendering.
Examples are as follows:
const UserList = memo( ({ users }) => { return <div>{(users)}</div>; }, (prevProps, nextProps) => { // Re-render only if the last user or list size changes const prevLastUser = [ - 1]; const nextLastUser = [ - 1]; return ( === && === ); } );
41. When declaring cached components, use named functions instead of arrow functions first.
When defining cache components, using named functions instead of arrow functions can improve clarity in React DevTools.
Arrow functions usually cause_c2
Such a common name, which makes debugging and analysis more difficult.
Bad practice: Using arrow functions for cache components can result in less name information in React DevTools.
const UserInfoList = memo(({ users }) => { // Remaining implementation logic});
Recommended practice: The name of this component will be visible in DevTools.
const UserInfoList = memo(function UserInfoList({ users }) { // Remaining implementation logic});
42. Use useMemo to cache expensive calculations or retain references
I usually use useMemo:
- When I have expensive calculations, these calculations should not be repeated every time I render.
- If the calculated value is a non-primitive value, it is used as a dependency in hooks such as useEffect.
- The calculated non-primitive value will be passed as props to the component wrapped in the memo; otherwise, this will destroy the cache because React uses to detect if props have changed.
Bad practice: UserInfoList's memo does not prevent re-rendering, because the style is re-created every time it is rendered.
export const UserInfo = () => { const { profileInfo, users, baseStyles } = useGetUserInfoData(); // Every time we re-render we get a style object const styles = { ...baseStyles, margin: 10 }; return ( <div className="App"> <h1>User Page</h1> <Profile data={profileInfo} /> <UserInfoList users={users} styles={styles} /> </div> ); }; const UserInfoList = memo(function UserInfoListFn({ users, styles }) { /// Remaining implementation});
Recommended practice: The use of useMemo ensures that styles will change only when baseStyles change, so that memo can effectively prevent unnecessary re-rendering.
export const UserInfo = () => { const { profileInfo, users, baseStyles } = useGetUserInfoData(); // Every time we re-render we get a style object const styles = useMemo(() => ({ ...baseStyles, margin: 10 }), [baseStyles]); return ( <div className="App"> <h1>User Page</h1> <Profile data={profileInfo} /> <UserInfoList users={users} styles={styles} /> </div> ); }; const UserInfoList = memo(function UserInfoListFn({ users, styles }) { /// Remaining implementation});
43. Use useCallback to cache functions
useCallback is similar to useMemo, but is designed for cache functions.
Bad practice: Whenever the theme changes, handleThemeChange will be called twice, and we will push the log to the server twice.
const useTheme = () => { const [theme, setTheme] = useState("light"); // Every rendering of `handleThemeChange` will change // Therefore, this effect will be triggered after each render const handleThemeChange = (newTheme) => { sendLog(["Theme changed"], { context: { theme: newTheme, }, }); setTheme(newTheme); }; useEffect(() => { const dqMediaQuery = ("(prefers-color-scheme: dark)"); handleThemeChange( ? "dark" : "light"); const listener = (event) => { handleThemeChange( ? "dark" : "light"); }; ("change", listener); return () => { ("change", listener); }; }, [handleThemeChange]); return theme; };
Recommended practice: Wrapping handleThemeChange in useCallback ensures that it is recreated only if necessary, thus reducing unnecessary execution.
const handleThemeChange = useCallback((newTheme) => { sendLog(["Theme changed"], { context: { theme: newTheme, }, }); setTheme(newTheme); }, []);
44. Cache callback functions or use the value returned by the program hook to avoid performance problems
When you create custom hooks to share with others, it is crucial to remember the returned values and functions.
This practice can make your hook more efficient and prevent unnecessary performance issues for anyone using it.
Bad practice: loadData is not cached and has caused performance issues.
const useLoadData = (fetchData) => { const [result, setResult] = useState({ type: "pending", }); const loadData = async () => { setResult({ type: "loading" }); try { const data = await fetchData(); setResult({ type: "loaded", data }); } catch (err) { setResult({ type: "error", error: err }); } }; return { result, loadData }; };
Recommended practice: We cache everything so there are no unexpected performance issues.
const useLoadData = (fetchData) => { const [result, setResult] = useState({ type: "pending", }); // Wrap in `useRef` and use the `ref` value so that the function does not change const fetchDataRef = useRef(fetchData); useEffect(() => { = fetchData; }, [fetchData]); // Wrap in `useCallback` and use the `ref` value so that the function does not change const loadData = useCallback(async () => { setResult({ type: "loading" }); try { const data = await (); setResult({ type: "loaded", data }); } catch (err) { setResult({ type: "error", error: err }); } }, []); // Use useMemo to cache values return useMemo(() => ({ result, loadData }), [result, loadData]); };
45. Use lazy loading and Suspense to make your app load faster
When building your app, consider using lazy loading and Suspense for the following code:
- High loading cost.
- Related only to certain users (such as advanced features).
- It is not immediately necessary for initial user interaction.
In the following example, the Slider resource (JS + CSS) is loaded only after you click the card.
//... const LazyLoadedSlider = lazy(() => import("./Slider")); //... const App = () => { // .... return ( <div className="container"> {/* .... */} {selectedUser != null && ( <Suspense fallback={<div>Loading...</div>}> <LazyLoadedSlider avatar={} name={} address={} onClose={closeSlider} /> </Suspense> )} </div> ); };
46. Restrict the network to simulate slow networks
Did you know that you can simulate a slow internet connection directly in Chrome?
This is especially useful when:
- User reports slow loading times and you can't replicate on faster networks.
- You are implementing lazy loading and want to observe how the file is loaded under slower conditions to ensure proper loading status.
47. Use react-window or react-virtuoso to efficiently render lists
Never render a long list of items at once, such as chat messages, logs, or unlimited lists.
Doing so may cause the browser to crash. Instead, virtualized lists can be used, meaning only a subset of items that may be visible to the user is rendered.
Library such as react-window, react-virtuoso, or @tanstack/react-virtual are designed for this purpose.
Bad practice: NonVirtualList renders all 50,000 log lines at the same time, even if they are not visible.
const NonVirtualList = ({ items }: { items: LogLineItem[] }) => { return ( <div style={{ height: "100%" }}> {items?.map((log, index) => ( <div key={} style={{ padding: "5px", borderBottom: index === - 1 ? "none" : "1px solid #535455", }} > <LogLine log={log} index={index} /> </div> ))} </div> ); };
Recommended practices:VirtualList
Render only items that may be visible.
const VirtualList = ({ items }: { items: LogLineItem[] }) => { return ( <Virtuoso style={{ height: "100%" }} data={items} itemContent={(index, log) => ( <div key={} style={{ padding: "5px", borderBottom: index === - 1 ? "none" : "1px solid #535455", }} > <LogLine log={log} index={index} /> </div> )} /> ); };
You can switch between the two options in this complete example and pay attention to usingNonVirtualList
How bad is the performance of the application?
7. Summary
This is the end of this article about React components, state management, and code optimization techniques. For more related React practice tips, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!