best practices

Component Markup

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Dynamic Approach

Hardcoded markup can lead to inconvenience and code complexity, especially when it's necessary to display multiple identical components in a row. In such cases, it's more reasonable to avoid this approach, which we will call "static". For example, a dynamic approach is more suitable for such UI elements as navigation menus, filters, and lists. It involves creating an array with data and then iterating over it using the map method to generate a complete list of components.

Example

Let's consider both approaches with an example of displaying a list of sorting options. They both accomplish the same task but differ in how they create and manage the data.

Static Approach

Code implementation
1interface SortSelectionProps {
2 onSelectOption: (value: string) => void;
3}
4
5const SortSelection: FC<SortSelectionProps> = ({ onSelectOption }) => {
6 return (
7 <>
8 <h3>Sort By</h3>
9 <ul>
10 <li>
11 <button onClick={() => onSelectOption('byRate')}>
12 ⭐️ Best Rated
13 </button>
14 </li>
15 <li>
16 <button onClick={() => onSelectOption('byLowestPrice')}>
17 💰 Lowest Price
18 </button>
19 </li>
20 <li>
21 <button onClick={() => onSelectOption('byHighestPrice')}>
22 💎 Highest Price
23 </button>
24 </li>
25 </ul>
26 </>
27 );
28};
29

In this approach, the sorting options are directly embedded in the SortSelection component code. Each list item has a hardcoded text and value passed to the onSelectOption function upon clicking. Despite its simplicity, there are several drawbacks.

Disadvantages

  • Maintenance difficulty: If you need to change or add another item, you will have to modify multiple places in the code.
  • High risk of errors: Due to code duplication, bugs can easily be introduced.
  • Increased code volume: Adding new items makes the code bulkier and less readable.

Dynamic Approach

Code implementation
1const sortOptions = [
2 {
3 id: 1,
4 name: '⭐️ Best Rated',
5 value: 'byRate',
6 },
7 {
8 id: 2,
9 name: '💰 Lowest Price',
10 value: 'byLowestPrice',
11 },
12 {
13 id: 3,
14 name: '💎 Highest Price',
15 value: 'byHighestPrice',
16 },
17];
18
19interface SortSelectionProps {
20 onSelectOption: (value: string) => void;
21}
22
23const SortSelection: FC<SortSelectionProps> = ({ onSelectOption }) => {
24 return (
25 <>
26 <h3>Sort By</h3>
27 <ul>
28 {sortOptions.map((option) => (
29 <li key={option.id}>
30 <button onClick={() => onSelectOption(option.value)}>
31 {option.name}
32 </button>
33 </li>
34 ))}
35 </ul>
36 </>
37 );
38};
39

Here, the sorting data is moved outside the component into a separate array called sortOptions. This allows changes or extensions without needing to edit the SortSelection component code. The component dynamically creates a list of elements using the map method to iterate over the sortOptions array. Each array element is assigned the corresponding text and value. This approach centralizes data management in one place — array.

Advantages

  • Easier maintenance: The code becomes more flexible and manageable, allowing easy data handling in one place if the component content needs to be changed.
  • Fewer bugs: The absence of code duplication reduces the likelihood of errors.
  • Improved code readability: Using a data array and a loop makes the code more compact and understandable.

The dynamic approach to component creation in React is more efficient and preferable, significantly simplifying the development process. By using a data array and the map method, the code becomes more structured and easily readable, which is especially important when working with large and complex interfaces.

Lists

Iterating over list elements is a common task, usually solved using the map method. However, in components with a lot of markup, using this method can reduce code readability due to additional indents and syntax. In such cases, it's better to extract the iteration logic into separate component, even if the markup is relatively small. This simplifies the parent component, which now only needs to "know" that a list is being rendered in a specific place.

It's better to leave the iteration of list elements in a component designed exclusively for displaying the list. If the list markup is complex and lengthy, it should also be moved to a separate component.

Example

Consider an example where the loop is combined with other markup in one component, leading to confusing and hard-to-understand code.

Code implementation
1const NewsPage: FC = () => {
2 return (
3 <div>
4 <Header />
5 <h1>News:</h1>
6 <div>
7 {newsList.map((news) => (
8 <div key={news.id}>
9 <h2>{news.title}</h2>
10 <p>{news.content}</p>
11 </div>
12 ))}
13 </div>
14 <div>
15 <h3>Some additional content</h3>
16 <p>
17 Duis vehicula eros eget metus rhoncus, vitae rutrum ex
18 tincidunt.
19 </p>
20 </div>
21 </div>
22 );
23};
24

In this NewsPage component code, the iteration over the newsList elements is done directly within the main component. Among other things, there are other components present. This not only complicates the markup but also makes the component less readable. In such cases, it's better to extract the list into a separate component.

Let's move on to a reworked example that demonstrates how to improve readability and code structure by breaking it into more specialized components:

Code implementation
1interface NewsListProps {
2 newsList: INewsItems;
3}
4
5const NewsList: FC<NewsListProps> = ({ newsList }) => {
6 return (
7 <>
8 <h1>News:</h1>
9 <div>
10 {newsList.map((news) => (
11 <div key={news.id}>
12 <h2>{news.title}</h2>
13 <p>{news.content}</p>
14 </div>
15 ))}
16 </div>
17 </>
18 );
19};
20
21const NewsPage: FC = () => {
22 return (
23 <div>
24 <Header />
25 <NewsList newsList={newsList} />
26 <div>
27 <h3>Some additional content</h3>
28 <p>
29 Duis vehicula eros eget metus rhoncus, vitae rutrum ex
30 tincidunt.
31 </p>
32 </div>
33 </div>
34 );
35};
36

Here, we created a separate NewsList component that accepts the newsList array as a prop. It uses the map method to iterate over the list items and render them. This method stays within the context of the component whose sole task is to display the news list, adhering to the Single Responsibility Principle. Now, the main NewsPage component only contains the high-level page structure. Its additional content remains unchanged, while the news display logic is delegated to the NewsList component. This allows for easy modification and scaling of the code in the future. Extracting the list display logic into a separate component significantly improves code readability and maintainability. One can go further and create a separate News component to return in the map method instead of bulky markup.

Dividing the code into smaller components with clearly defined tasks improves code structure and readability, making it easier to maintain and test. Using separate components for iterating and displaying list elements allows focusing on the logic of each component individually, without overloading the main component with unnecessary details. Implementing the Single Responsibility Principle simplifies the main component, making it easier to understand and modify. This is especially important when working on large and complex projects where code maintainability and scalability are key. Following these recommendations can lead to more efficient and clean code that will be easier to maintain and extend in the future.

Using the Children Prop

Using the children prop is a useful practice for passing nested components. This approach is particularly useful when it's necessary to avoid unnecessary re-renders of nested components caused by updates to the parent component. However, it's only applicable if the nested component doesn't directly depend on the state or props of the parent but receives the necessary data through the parent's props.

The essence of this approach is that React optimizes the rendering process by knowing that if a component contains children that remain unchanged, there is no need to re-render them every time the parent component updates. This contributes to stability and predictability of application behavior, as well as improving performance.

Example

Without using the children prop:

Code implementation
1interface GrandChildComponentProps {
2 data: string;
3}
4
5const GrandChildComponent: FC<GrandChildComponentProps> = ({ data }) => {
6 console.log('GrandChildComponent --- Render');
7
8 return (
9 <Layer
10 title="GrandChild"
11 colored
12 >
13 <Text>Hello, {data}!</Text>
14 </Layer>
15 );
16};
17
18interface ChildComponentProps {
19 data: string;
20}
21
22const textContent = 'Some additional content 😻';
23
24const ChildComponent: FC<ChildComponentProps> = ({ data }) => {
25 console.log('ChildComponent --- Render');
26
27 const [isOpen, setIsopen] = useState(false);
28
29 const handleToggle = () => {
30 setIsopen((prev) => !prev);
31 };
32
33 return (
34 <Layer
35 title="Child"
36 colored
37 >
38 <GrandChildComponent data={data} />
39 <div className="flex w-full justify-center">
40 <Button onClick={handleToggle}>
41 {isOpen ? 'Hide' : 'Show'}
42 </Button>
43 </div>
44 {isOpen && <Text>{textContent}</Text>}
45 </Layer>
46 );
47};
48
49const initialData = 'null';
50
51const dataToSet = 'John Doe';
52
53const ParentComponent: FC = () => {
54 console.log('ParentComponent --- Render');
55
56 const [data, setData] = useState(initialData);
57
58 const isDataSet = data !== initialData;
59
60 const handleSetData = () => {
61 setData(dataToSet);
62 };
63
64 return (
65 <Layer
66 title="Parent"
67 colored
68 >
69 <ChildComponent data={data} />
70 <div className="flex w-full justify-center">
71 <Button
72 disabled={isDataSet}
73 onClick={handleSetData}
74 >
75 {isDataSet ? 'Data Set' : 'Set Data'}
76 </Button>
77 </div>
78 </Layer>
79 );
80};
81

In this example, GrandChildComponent doesn't directly depend on its parent component (ChildComponent) but merely receives the data prop through it. Clicking the "Hide / Show" button changes the isOpen state, leading to re-rendering of ChildComponent and consequently GrandChildComponent as well. Thus, even though nothing changed for GrandChildComponent, it will still be re-rendered. If GrandChildComponent were a heavyweight component, unnecessary re-renders could negatively impact performance. To confirm this, log messages in the console about re-renders of ChildComponent and GrandChildComponent will appear after clicking the "Hide / Show" button.

An interactive output of the code above

Using the children prop:

Code implementation
1interface GrandChildComponentProps {
2 data: string;
3}
4
5const GrandChildComponent: FC<GrandChildComponentProps> = ({ data }) => {
6 console.log('GrandChildComponent --- Render');
7
8 return (
9 <Layer
10 title="GrandChild"
11 colored
12 >
13 <Text>Hello, {data}!</Text>
14 </Layer>
15 );
16};
17
18interface ChildComponentProps {
19 children: ReactNode;
20}
21
22const textContent = 'Some additional content 😸';
23
24const ChildComponent: FC<ChildComponentProps> = ({ children }) => {
25 console.log('ChildComponent --- Render');
26
27 const [isOpen, setIsopen] = useState(false);
28
29 const handleToggle = () => {
30 setIsopen((prev) => !prev);
31 };
32
33 return (
34 <Layer
35 title="Child"
36 colored
37 >
38 {children}
39 <div className="flex w-full justify-center">
40 <Button onClick={handleToggle}>
41 {isOpen ? 'Hide' : 'Show'}
42 </Button>
43 </div>
44 {isOpen && <Text>{textContent}</Text>}
45 </Layer>
46 );
47};
48
49const initialData = 'null';
50
51const dataToSet = 'John Doe';
52
53const ParentComponent: FC = () => {
54 console.log('ParentComponent --- Render');
55
56 const [data, setData] = useState(initialData);
57
58 const isDataSet = data !== initialData;
59
60 const handleSetData = () => {
61 setData(dataToSet);
62 };
63
64 return (
65 <Layer
66 title="Parent"
67 colored
68 >
69 <ChildComponent>
70 <GrandChildComponent data={data} />
71 </ChildComponent>
72 <div className="flex w-full justify-center">
73 <Button
74 disabled={isDataSet}
75 onClick={handleSetData}
76 >
77 {isDataSet ? 'Data Set' : 'Set Data'}
78 </Button>
79 </div>
80 </Layer>
81 );
82};
83

In this variant, thanks to the children prop, the behavior is different. Clicking the "Hide / Show" button only triggers a re-render of ChildComponent, while GrandChildComponent remains untouched. This is because there were no changes to the variables that GrandChildComponent depends on (data prop ). The console will log a message only about the re-rendering of ChildComponent. However, if you click the "Set Data" button, the console will log a message about the re-rendering of GrandChildComponent, as the data prop passed to this component will change.

An interactive output of the code above

This approach allows creating more maintainable and efficient components, improving the overall application architecture and optimizing performance.

Conditional Rendering

When developing with React, conditional rendering of components is often necessary. One popular way is using the logical && operator. This approach allows for reducing the amount of code and is convenient for quick rendering. However, in some cases, this method can lead to unexpected results. For example, if a variable value equals 0, this value will also be displayed in the UI, which is usually undesirable. To avoid such situations, it's recommended to use the ternary operator. Although it's more verbose, it ensures correct display even when the value is 0. Moreover, the ternary operator facilitates adding alternative rendering options if needed.

Example

Consider an example using the && operator for conditional rendering of a message about unread messages:

Code implementation
1const totalMessages = 0;
2
3const MessageNotifier: FC = () => {
4 return (
5 <div>
6 <h3>MessageNotifier</h3>
7 {totalMessages && (
8 <span>You have {totalMessages} unread messages</span>
9 )}
10 <p>Some content</p>
11 </div>
12 );
13};
14

In this example, if the totalMessages variable equals 0, the value 0 will be displayed in the UI, which is undesirable. This happens due to type coercion in JavaScript, where the value 0 is considered false. The && operator returns a false value if one of the operands is false. In this case, when totalMessages equals 0, the condition is false, and React doesn't render the second argument (the message); instead, it displays the value of totalMessages, which is a number.

Now, consider a more elegant solution to prevent displaying 0 in the UI using the ternary operator:

Code implementation
1const totalMessages = 0;
2
3const MessageNotifier: FC = () => {
4 return (
5 <div>
6 <h3>MessageNotifier</h3>
7 {totalMessages ? (
8 <span>You have {totalMessages} unread messages</span>
9 ) : null}
10 <p>Some content</p>
11 </div>
12 );
13};
14

Here, in the absence of unread messages (i.e., when totalMessages equals 0), null is returned. Thus, no additional elements will be displayed in the UI, avoiding the unwanted display of 0. This method makes the code more compact and readable, as the condition and rendering result are in one place. This solution improves the understanding of logic and simplifies code maintenance.

When developing React components, avoid using short-circuit evaluation with the logical && operator for conditional rendering. Instead, use the ternary operator. Although it requires slightly more code, it ensures more correct and predictable behavior in the UI, simplifying the addition of alternative rendering options. This is especially important when developing reliable and user-friendly interfaces.

This is one way to solve such a problem. For more information on other methods, check out the separate article.

Ternary Operators

Ternary operators represent a concise form of writing conditional expressions in JavaScript, which can be useful for making simple decisions based on conditions. In React, they're also used for conditional rendering. However, their use at multiple levels of nesting can significantly impair code readability and complicate its maintenance in the future. Despite their compactness, in such cases, it's preferable to explicitly separate the logic into more understandable components. Extracting nested logic into separate components helps reduce the use of ternary operators to a single level.

Example

Let's take a look at the following code example demonstrating the use of nested ternary operators to determine which greeting message to display based on the user's login status and administrative rights.

Code implementation
1interface HomePageProps {
2 isLoggedIn: boolean;
3 isAdmin: boolean;
4}
5
6const HomePage: FC<HomePageProps> = ({ isLoggedIn, isAdmin }) => {
7 return (
8 <div>
9 <Header />
10 {isLoggedIn ? (
11 isAdmin ? (
12 <span>Welcome, administrator!</span>
13 ) : (
14 <span>Welcome, user!</span>
15 )
16 ) : (
17 <span>Please log in.</span>
18 )}
19 <div>
20 <h3>Some additional content</h3>
21 <p>
22 Duis vehicula eros eget metus rhoncus, vitae rutrum ex
23 tincidunt.
24 </p>
25 </div>
26 </div>
27 );
28};
29

Even in this simplified example, it's already difficult to immediately understand what logic is implemented in the markup. Maintaining it would be even more challenging if the logic were more complex. For instance, instead of simple paragraphs, more complex constructions consisting of multiple elements could be used. This would create a fertile ground for bugs. In such a scenario, making changes in the future would be extremely difficult.

Now, let's consider an improved solution with the logic extracted into a separate component, which will help reduce nesting.

Code implementation
1interface WelcomeMessageProps {
2 isAdmin: boolean;
3}
4
5const WelcomeMessage: FC<WelcomeMessageProps> = ({ isAdmin }) => {
6 const person = isAdmin ? 'administrator' : 'user';
7
8 return <span>Welcome, {person}!</span>;
9};
10
11interface HomePageProps {
12 isLoggedIn: boolean;
13 isAdmin: boolean;
14}
15
16const HomePage: FC<HomePageProps> = ({ isLoggedIn, isAdmin }) => {
17 return (
18 <div>
19 <Header />
20 {isLoggedIn ? (
21 <WelcomeMessage isAdmin={isAdmin} />
22 ) : (
23 <span>Please log in.</span>
24 )}
25 <div>
26 <h3>Some additional content</h3>
27 <p>
28 Duis vehicula eros eget metus rhoncus, vitae rutrum ex
29 tincidunt.
30 </p>
31 </div>
32 </div>
33 );
34};
35

In this case, we created a separate WelcomeMessage component that displays the appropriate greeting message depending on the administrator status. The component creates a variable during the render, which is then inserted into the JSX. Now, the main HomePage component uses just one ternary operator to check the login status. If the user is logged in, the WelcomeMessage component is called with the isAdmin prop; otherwise, a message about needing to log in is displayed.

This approach improves the structure of HomePage, makes the code more readable, and simplifies its maintenance in the long term, especially when further expanding the logic or changing requirements, which is crucial for efficient development.

Introducing such components not only enhances code readability but also simplifies its testing and evolution. Breaking down logic into more understandable components increases development efficiency and reduces the likelihood of errors.

Thus, avoiding nested ternary operators by extracting logic into separate components contributes to creating cleaner, more understandable, and maintainable code in web development.

Last updated on by @skrykateSuggest an improvement on Github repository.