best practices

Component Props

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.

Number of Props

The answer to the question about the number of props in a component is subjective. The number of props is related to the number of variables used by the component. The more props a component receives, the higher its responsibility.

A large number of props may indicate excessive functionality of the component. If a component takes more than five props, it's recommended to consider splitting it. In some cases, a component may require a lot of data, such as a text field needing numerous props. However, this may also indicate a need to separate some logic into a different component.

It's important to note that the more props a component receives, the more frequently it will re-render.

Destructuring Props

Most components in React are functions that take an object containing all the passed props as an argument and return JSX markup. When declaring a functional component, it's written as a function that takes this object. In a regular function, arguments are passed directly, and with components, it makes sense to adhere to a similar approach. You should not use only the props object in every component, especially if many props were passed, as this approach complicates the code and its readability. Instead, it's more effective to destructure and use the values explicitly within the component.

Destructuring props is a convenient way to "unpack" the passed props from the props object and assign them directly to variables. This improves code readability, as it allows access to props without the need to constantly use the props object.

Example

To better understand the issue, here is an example:

Code implementation
1interface ChildComponentProps {
2 title: string;
3 content: string;
4}
5
6const ChildComponent: FC<ChildComponentProps> = (props) => {
7 return (
8 <div>
9 <h3>{props.title}</h3>
10 <p>{props.content}</p>
11 </div>
12 );
13};
14
15const ParentComponent: FC = () => {
16 const data = {
17 title: 'Title',
18 content: 'Paragraph content',
19 };
20
21 return (
22 <ChildComponent
23 title={data.title}
24 content={data.content}
25 />
26 );
27};
28

In this example, props are used without destructuring. Note that each time you need to use values from the props, you need to access them through the props object. To get the value of a specific prop, you need to access the corresponding property of the object, such as props.title in this example.

This approach can be useful when you need to pass all props to a child component or keep them together in one object for further processing. However, if each passed value is used separately, the code in JSX becomes less readable and increases the amount of repetition. The situation becomes more complicated when you need to manipulate the passed props or when they have nested objects.

Now let's take a look at the same example with destructuring props.

Code implementation
1interface ChildComponentProps {
2 title: string;
3 content: string;
4}
5
6const ChildComponent: FC<ChildComponentProps> = ({ title, content }) => {
7 return (
8 <div>
9 <h3>{title}</h3>
10 <p>{content}</p>
11 </div>
12 );
13};
14
15const ParentComponent: FC = () => {
16 const data = {
17 title: 'Title',
18 content: 'Paragraph content',
19 };
20
21 return (
22 <ChildComponent
23 title={data.title}
24 content={data.content}
25 />
26 );
27};
28

Here, the props object is destructured right in the function definition, making access to the props more convenient. The required values are explicitly specified in the function parameters — title and content are extracted from the props object and used directly. This improves code readability, making it more concise and understandable, as it clearly shows which props the component accepts.

In cases where a component takes many props, destructuring helps better organize the code and reduce the likelihood of errors related to accessing props through the object. With destructuring, if something is written incorrectly, it's immediately visible and easier to fix.

Using destructuring of props makes the code more readable and easier to maintain. This is especially useful in larger projects where components may take many props, and the need to constantly access the props object can be tedious and increase the risk of errors.

Thus, when designing components in React, consider destructuring props as a standard practice, unless there is a need to pass or process props as a whole object. This improves readability and simplifies working with the code, which is especially important in team development and when scaling applications.

Default Props

When creating components, it's often necessary to make some arguments optional. In such cases, a good practice is to set default values for these arguments. This is especially important if the component's logic doesn't check for the presence of these arguments before using them, as this could result in an error due to receiving undefined if the optional prop was not passed.

Setting default values helps avoid potential errors and improves code readability and maintainability, making its behavior more predictable and stable.

An effective way to set default values is to specify them directly during prop destructuring. This approach allows developers to immediately see which values will be used by default.

Example

Code implementation
1interface PersonBioProps {
2 nickname: string;
3 firstName?: string;
4 lastName?: string;
5}
6
7const PersonBio: FC<PersonBioProps> = ({
8 nickname,
9 firstName = 'John',
10 lastName = 'Doe',
11}) => {
12 const fullName = `${firstName} ${lastName}`;
13
14 return (
15 <Text>
16 Nickname: {nickname}
17 <br />
18 Full Name: {fullName}
19 </Text>
20 );
21};
22
23const data1 = 'jod76';
24
25const data2 = {
26 nickname: 'lilol',
27 firstName: 'Lilo',
28 lastName: 'Olil',
29};
30
31const Container: FC = () => {
32 const { nickname, firstName, lastName } = data2;
33
34 return (
35 <Layer title="Container">
36 {/* Default values are used for `firstName` and `lastName` */}
37 <PersonBio nickname={data1} />
38 {/* Passed values are used for `firstName` and `lastName` */}
39 <PersonBio
40 nickname={nickname}
41 firstName={firstName}
42 lastName={lastName}
43 />
44 </Layer>
45 );
46};
47

In this example, default values for firstName and lastName are set during prop destructuring. This allows the component to function correctly even if these props were not passed, avoiding potential errors and improving component stability.

Here's what the code will output if no default value is set:

Instead of the fullName variable, which is computed during the component's render, "undefined undefined" will be displayed because the arguments were not passed, and there is no check for their presence. This is an undesirable result that should be avoided.

Following this practice allows creating more reliable and predictable components, ultimately improving code quality and maintainability in the long term.

Passing an Object Instead of Primitives

An effective way to reduce the number of passed props is to pass an object instead of primitives. A common practice is to group related data into a single object instead of passing each value separately. This simplifies data management and allows for easier addition of new props in the future.

Example

Consider an example in which a component is passed many individual props.

Code implementation
1type IUser = {
2 nickname: string;
3 firstName: string;
4 lastName: string;
5 age: number;
6};
7
8interface PersonBioProps extends IUser {
9 isVisible: boolean;
10}
11
12const PersonBio: FC<PersonBioProps> = ({
13 nickname,
14 firstName,
15 lastName,
16 age,
17 isVisible,
18}) => {
19 if (!isVisible) return null;
20
21 const fullName = `${firstName} ${lastName}`;
22
23 return (
24 <di>
25 <span>Nickname: {nickname}</span>
26 <span>Full Name: {fullName}</span>
27 <span>Age: {age}</span>
28 </di>
29 );
30};
31
32interface UserProfileProps {
33 user: IUser;
34}
35
36const UserProfile: FC<UserProfileProps> = ({ user }) => {
37 const [isBioVisible, setIsBioVisible] = useState(false);
38
39 const handleToggle = () => {
40 setIsBioVisible((prev) => !prev);
41 };
42
43 return (
44 <div>
45 <button onClick={handleToggle}>
46 {isBioVisible ? 'Hide' : 'Show'}
47 </button>
48 <PersonBio
49 nickname={user.nickname}
50 firstName={user.firstName}
51 lastName={user.lastName}
52 age={user.age}
53 isVisible={isBioVisible}
54 />
55 <span>Some additional content</span>
56 </div>
57 );
58};
59

In this example, the PersonBio component takes isVisible and many individual related props — nickname, firstName, lastName, age. While this works, passing all these values separately can become cumbersome, especially if the number of props increases. In the UserProfile component, user data is already passed as an object (user), so this object can be passed entirely to the PersonBio component, making the code cleaner and easier to maintain in the long run.

An alternative approach using a single object to pass all related data.

Code implementation
1type IUser = {
2 nickname: string;
3 firstName: string;
4 lastName: string;
5 age: number;
6};
7
8interface PersonBioProps {
9 user: IUser;
10 isVisible: boolean;
11}
12
13const PersonBio: FC<PersonBioProps> = ({ user, isVisible }) => {
14 if (!isVisible) return null;
15
16 const { nickname, firstName, lastName, age } = user;
17
18 const fullName = `${firstName} ${lastName}`;
19
20 return (
21 <div>
22 <span>Nickname: {nickname}</span>
23 <span>Full Name: {fullName}</span>
24 <span>Age: {age}</span>
25 </div>
26 );
27};
28
29interface UserProfileProps {
30 user: IUser;
31}
32
33const UserProfile: FC<UserProfileProps> = ({ user }) => {
34 const [isBioVisible, setIsBioVisible] = useState(false);
35
36 const handleToggle = () => {
37 setIsBioVisible((prev) => !prev);
38 };
39
40 return (
41 <div>
42 <button onClick={handleToggle}>
43 {isBioVisible ? 'Hide' : 'Show'}
44 </button>
45 <PersonBio
46 user={user}
47 isVisible={isBioVisible}
48 />
49 <span>Some additional content</span>
50 </div>
51 );
52};
53

Here, the PersonBio component takes a user object containing the same related four props. This approach simplifies the prop structure and makes the code more readable and maintainable.

Thus, passing a single object instead of several primitives helps better organize the code and makes it easier to maintain. This approach not only makes the code cleaner and more structured but also reduces the likelihood of errors related to data passing. In the long run, it also facilitates application scalability.

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