Skip to content

Props Recommendations

Published on October 1, 2024

Props are data that is passed to a component from it’s parent component.
Here is a list of tips how to use props properly in React

#1. Avoid prop drilling

Prop drilling means passing props along the tree of React Components
take a look at the following example where <App> component is passing a prop to <Child> component, and <Child> component is passing it to <GrandChild> component.

import Child from './Child';
export default function App() {
return (
  <Child title="hello from parent"  />
)
}

this is called prop drilling and it’s considered a bad practice when passing props in React.

Ways to avoid prop drilling

To avoid prop drilling you can do the following:

  1. store data in context

In this example instead of passing the prop down the tree of the components, we are creating a context and storing that prop in the context.

import Child from './Child';
import titleContext from './title.context';
export default function App() {
return (
  <titleContext.Provider value={{title: 'hello from parent'}}>
      <Child />
  </titleContext.Provider>
)
}
  1. use a state management library

Another way to avoid prop drilling is to use a state management library like Redux or Mobx to store the data in a global state.

While in this example I will demonstrate with Redux, Redux is not the only state management library out there, there are many popular alternatives where each one has it’s pros and cons.

import Parent from './Parent';
import { Provider } from 'react-redux';
import { store } from './store';
export default function App() {
return (
  <Provider store={store}>
      <Parent />
  </Provider>
)
}

We need to wrap our entire application with the react-redux Provider component so that the tree of components can access the Redux store, and we are doing that in App.js.
Parent.js can store data in redux that GrandChild can access without the need for Props drilling
GrandChild.js to access the data in redux using useSelector hook.

#2. Use conventions in props naming

If you look at Material Design and examine the names of the props that you pass to the components, you will notice that the props names are repeating between different components.
For example: variant, sx, disabled, color, etc.

When you create an application, you have to imagine that you not only creating an application, but you are creating an entire language.
That language reflects how your application looks like: repeating style guide, repeating components and ux.
That language also reflects how your code looks like: reusing functions, class, creating your own libraries, creating toolset that you will use throughout your application.
That language you create enforces you to build your application like lego blocks using good infastructure, and reusing your code.

That same language will apply to certain convention you will have to follow, those conventions will help you reuse your language.

For example in your database, in the tables columns, strive to use similar naming for similar columns, if for example a certain entity (A) contains a name colum, and another entity (B) contains also a name keeping the same conventions will direct you to place the column for B that is named name and not for examle using in A the column name and in B the column title.

Why is it so important to keep naming similar?
In one word Grouping
When creating a software we like to place similar things in a group, from those groups, you can divide it to larger groups, and larger ones and larger ones.
For example maybe you take functions that are relating to each other and placing them in a class, maybe you take that class and combine it with helping function and utilities and place in a library package, maybe you take that library package and combine it with other library packages and create a dependency graph, and from that create different applications.

We love to group things together when developing software, and that is why the names are important.
It’s easier to group things together if a lot of their names are identical to another element.

same applies to the names of the props in your components, try to keep them similar, it will help you organize, inherit, use different patterns, find your DRY violations.
It will also make your components more understandable to your team.

#3. Prop types, optional and default value

Ask yourself, what is the type of each prop passed? Is that prop required or optional? If the prop is optional does the prop have a default value?

Using TypeScript answering these questions might look like this:

import type { FC } from 'react'
type ChildProps = {
age? number;
name: string;
}
export const Child: FC<ChildProps> = ({
age = 33,
name
}) => {
// ...
}

notice how we specify every type of prop that is passed, we also write if that prop is optional or required, and if needed we give that prop a default value in the destructor.

Same rule applies to JavaScript as well where the same code will look like so:

import PropTypes from 'prop-types';
export const Child = ({
age = 33,
name
}) => {
// ...
}
Child.propTypes = {
age: PropTypes.number,
name: PropTypes.string.isRequired
}

#4. Add comments

Be a team player, document your component including the props that component are getting.
Developers on your team will not simply understand right away how to use your component, help them out with proper JSDocs

/**
* This component will do this and that
* @param {Object} props
* @param {number} props.age - the age of the child
* @param {string} props.name - the name of the child
* @returns {React.ReactElement}
*/
export const Child = ({
age = 33,
name
}) => {
// ...
}

Working with JSDoc will play nicely with your IDE which would be able to present you comment whenever someone hover on that function.

#5. Generic types

Are you using TypeScript? If so then make sure to use all the power that TypeScript is giving you to define the exact types for your props.
For example you can use Generic Type <T> to dictate typescript checks based on a type passed a <T>.

Let’s take this component as an example that gets an instance of a class and prints one of the properties of the class, according to the props passed to the component:

type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];
type Props<T, U> = {
entity: T;
typeThis: FilteredKeys<T, U>;
}
export const Generic = <T extends unknown, U extends ReactNode>({
entity,
typeThis
} : Props<T, U>): ReactElement => {
const hello = entity[typeThis] as U;
return (
<div>
<h1>{hello}</h1>
</div>
);
}

Notice that FilterKeys filters only the keys that extends U, so if I’m passing the object:

const a = {
hello: 'world',
age: 33,
meaning: 42
}

then FilteredKeys<typeof a, number> will return age | meaning.

The props of this component will get 2 generic types so to call this component we can do the following:

class Person {
name = 'academeez';
age = {};
}
const me = new Person();
function App() {
return (
<Generic<Person, string> entity={me} typeThis='name' />
);
}

Did you know that most of the components in mui have a generic type?
But we don’t really need to pass them when using their component right?
Actually in our example we don’t have to pass those generic too, and we could have written the same as:

<Generic entity={me} typeThis='name' />

typescript can infer the generic types based on the other props we are passing, that is why usually you don’t need to pass those generics to mui.

Generics are just a subset of the power of typescript which I see a lot of experianced developers miss out when writing their components.

#6 To memo or not to memo?

You should understand when you should memoize the props you are passing.
As a general rule you should memoize the props you are passing in the following cases:

  1. When the component you are transfering the props to is a pure component that is wrapped with React.memo, and the components you are passing are non primitives, and you want to avoid rendering this component when the parent renders:
Child.js
import {memo} from 'react';
function Child({
person,
onClick
}) {
return (
<div>
<h1>
Hello {person.name}
</h1>
<button onClick={onClick}>
click me
</button>
</div>
)
}
export default memo(Child);

you would memoize the props passed to Child if you don’t want him to render every time the parent renders.

Child is wrapped with memo so it will only render when the props change.

  1. the props are passed in the child is some dependency array for example in useEffect, useCallback, useMemo and you don’t want them to run again
import {useMemo} from 'react';
export default function Child({
student,
}) {
const grade = useMemo(() => {
// when student is changed calculate his grade
}, [student])
return (
<h1>
You got {grade}
</h1>
)
}

notice that I got a prop and I placed that prop in a dependency array, so the parent can control when he is generating that student to avoid expensive calculation in the Child

#7 If you can obtain them, then don’t pass them

At the beginning of Redux in their docs, they specified a pattern to split your components to Container components and Dumb presentation components.
It’s a pattern that helped Dan Abramov in the past and he wrote about it in this article

In the start of this article you will also see his comment:

Update from 2019: I wrote this article a long time ago and my views have since evolved. In particular, I don’t suggest splitting your components like this anymore. If you find it natural in your codebase, this pattern can be handy. But I’ve seen it enforced without any necessity and with almost dogmatic fervor far too many times. The main reason I found it useful was because it let me separate complex stateful logic from other aspects of the component. Hooks let me do the same thing without an arbitrary division. This text is left intact for historical reasons but don’t take it too seriously.

The era of smart containers and dumb components is not fitting the world of the new components with custom hooks.

Many times I see developers passing props that can be obtained by simply calling some kind of hook.

Let’s examine the following example using NextJS

function App() {
const router = useRouter();
const id = router.query['id']
return (
<Article id={id} />
);
}

The <Article /> component will display an article based on the id taken from the url.
But why exactly are we passing it? why don’t the article obtain it himself by calling the useRouter() hook himself?

The more props you will have to pass down the harder it is to understand you component and the objective of each prop, think simple and you will see you component reused more.

Summary

We learn about props very early when we started learning about React.
Seems like such a simple topic, yet I see so much bad practices and mistakes of even the most experianced React developers. Not following best practices for props will lead to more bugs, more renders, components api which is not clear and thus less used by other developers, it’s even effects testing and making your components hard to test.

Hopefully this lesson will improve you React prop passing ability.