Frontend React
Declarative Paradigm
One of the most important things to understand early on about React is that it follows a declarative programming paradigm. This sets it apart from earlier, imperative approaches like jQuery, as well as from traditional object-oriented programming commonly found in languages like Java and C++.
When writing a User Interface (UI) in React, the goal is to define the minimal but complete data model for it, map the model to UI components and then change the data upon user interaction and the UI will be updated automatically (on next re-render).
To give a practical example:
Instead of directly changing a button’s background color to red on click, as done in imperative code, React encourages defining a state variable that represents the background color.
This variable is then bound to the button’s style in JSX, and its value is updated in response to the click event — allowing React to handle the UI update declaratively.
There are so-called escape hatches from the declarative approach in React:
- useRef - for keeping non-reactive values between re-renders
- useEffect - for hooking into component's lifecycle
- useImperativeHandle - for defining imperative API of components
Escape hatches are completely fine to use when appropriate, and sometimes they are the only (performant) way of achieving desired functionality, however the declarative approach is much less error-prone and should be preferred in most cases.
Further reading:
- Imperative vs Declarative (React vs jQuery)
- How To: Interactivity in Declarative React
- React Escape Hatches
Components
React is built around the concept of composing reusable components.
When implementing a new part of the UI, the first step should be to examine the mockup and break it down into small, focused components — each ideally with a single responsibility.
For example, this could mean creating one component for displaying text, another for rendering a list of rows, and so on.
Some of the best practices regarding writing components in React are:
- Use only functional components (no class components, which reasonable exceptions e.g. ErrorBoundary)
- Component should be focused on a single responsibility
- Encourage reusability and extensibility
- Don't define components during render
Don't define components during render
The last point deserves a closer look. When working with container components — such as grids, lists, or context menus — it’s often important to allow customization of individual elements like rows or cells, depending on how the component is used. This is typically done in one of two ways:
- Render function prop that accepts arguments that Container provides (index, total count etc) and returns JSX:
renderRow: (args) => ReactNode
- (Functional) component definition prop:
RowTemplate: React.FC<TArgs>
The first approach is simpler, but it doesn't allow for performance optimizations using memoization. That means any time the container re-renders, the renderRow
function is called again for all visible rows.
In contrast, the second approach supports memoization by defining RowTemplate
as a React functional component. When wrapped with React.memo
, React can avoid re-rendering rows whose props haven’t changed. However, because RowTemplate
is treated as a full component, it follows React’s rendering rules. If RowTemplate
is defined inline during render — for example, as an arrow function — its reference will change on every render. This causes React to treat it as a new component type, which leads to the entire subtree being discarded and re-rendered from scratch.
type ListProps = {
items: unknown[];
ItemTemplate: React.FC<{ item: unknown }>;
};
const List = ({ items, ItemTemplate }: ListProps) => {
return (
<>
{items.map((item, index) => (
<ItemTemplate key={index} item={item} />
))}
</>
);
};
// ItemTemplate's reference is changed on each rerender of MyList
const MyList = () => {
return (
<List
items={[1, 2, 3]}
ItemTemplate={(item) => <div>{String(item)}</div>}
/>
);
};
Further reading:
State Management
- Keep a single source of truth. Duplication of data very often leads to inconsistent application state.
- Avoid contradictions in state. E.g. holding
isMonday
andisTuesday
can lead to contradicting state when both become true. The better alternative would be to hold one statedayOfWeek
. - Avoid redundant state. If some information can be efficiently derived from component’s props or its other state variables during rendering, it should not be put into that component’s state.
- When using
zustand
store, select only strictly necessary data. Utilizing selectors correctly can remove the need for a great deal of memoization. - Avoid using
React.Context
for state of complex container components (Lists, Grids, Carousel).React.Context
API provides no way of optimizing re-renders by picking only part of state. For such components, where re-renders really matter,zustand
store is better suited.
Further reading:
Hooks
- Don't overuse
useEffect
.
Always understand when exactly youruseEffect
will be called. Know the difference between no dependencies and empty array of dependencies. - Use return value of
useEffect
for cleanup when:- adding event listener (
element.addEventListener()
) - using
setTimeout
orsetInterval
- adding event listener (
- Don't use
useState
for non-reactive values. Instead,useRef
can be used. - When performing layout calculations in
useEffect
and experiencing flickering, consider usinguseLayoutEffect
instead ofuseEffect
. The former is executed BEFORE the render, while the latter is executed AFTER render.
Note: overuse ofuseLayoutEffect
negatively affects performance since it slows down render cycle. - Use
useTransition
for non-blocking but expensive state updates. - There is an experimental
useEffectEvent
hook, however it can be quite easily constructed and some libraries implement it themselves. Its purpose is similar to auseEffect
which can READ the latest values of state and props without necessarily adding all of them as dependencies and thus triggeringuseEffect
when any of them change. - Extract reusable logic to custom hooks, but always understand the effect that using a custom hook has on component re-renders. A helpful way to understand this effect is to simply copy-paste the code inside a custom hook into the component that uses it and evaluate how it affects re-renders.
- Keep in mind React's rules of hooks
Further reading:
- React Hooks Docs
- You Might Not Need an Effect
- Don't over useState
- Possible implementation of useEventCallback
- Rules of Hooks
Memoization
Memoization (optimization of re-renders) is by far the most common way of optimizing performance of React apps.
It is true that the render process is much faster than changing the actual DOM and most of the time an extra re-render would not matter at all. However, performance problems do arise when too many re-renders of too many components want to happen at the same time.
- Always
React.memo
child components of Lists, Grids, Rows. Since there can be many rows in a list visible at the same time, re-render of each row at each re-render of list usually causes noticeable lag. - When using
React.memo
, define component'sdisplayName
(e.g.Input.displayName = "Input";
at the end of your file.) - If memoized component has non-primitive props (e.g. whole or partial DTOs), provide equality function as second argument of
React.memo
. Usually, deep equals suffices. - Never pass functions or objects defined inline — during render — to memoized components.
- Don't memoize primitive values with
useMemo
, unless their computation is extremely inefficient.
Further reading: