React-admin V4: No More Props Injection

François Zaninotto
François ZaninottoMarch 30, 2022
#react#react-admin#tutorial

React-admin v4 is just around the corner. It's a great time to see how it changes the Developer Experience.

The most striking difference between v3 and v4 is that react-admin no longer injects props. What does that mean? Just look at this diff of a simple Show component between v3 and v4:

-const PostShow = (props) => (
+const PostShow = () => (
-   <Show {...props}>
+   <Show>
        <SimpleShowLayout>
            <TextField source="title" />
        </SimpleShowLayout>
    </Show>
);

This may not look much, but this is a fundamental difference.

The Problem With Injected Props

In v3, react-admin pushed props to components. Which props? This was difficult to know. Even the best documentation and TypeScript types didn't help much.

// v3
// what's "props"? Can I use it?
const PostShow = props => (
    <Show {...props}>
        <SimpleShowLayout>
            <TextField source="title" />
        </SimpleShowLayout>
    </Show>
);

Developers often wondered how these props had to be passed down. This led to a lot of confusion, and to code that had subtle bugs like the following:

// v3
const PostShow = props => (
    <Show {...props}>
        {/* don't do this at home */}
        <SimpleShowLayout {...props}>
            <TextField source="title" />
        </SimpleShowLayout>
    </Show>
);

In order to inject props, react-admin had to clone elements. For instance, to render a custom field:

// v3
// react-admin will inject record as prop
const NameField = ({ record }) => {
    return <span>{record.name}</span>;
};

const PostShow = props => (
    <Show {...props}>
        <SimpleShowLayout>
            <TextField source="title" />
            <NameField />
        </SimpleShowLayout>
    </Show>
);

React-admin had to use code similar to this:

// v3
const SimpleShowLayout = ({ record, children }) => (
    <div className="SimpleShowLayout">
        {Children.map(children, field => cloneElement(field, { record }))}
    </div>
);

This cloneElement call meant that the child was always a new element, and this caused performance problems.

Also, every react-admin component had to pass down props so that some of their descendants (e.g. Field components) could receive these props. This pattern was viral, and not in the good sense. Over time, many react-admin components received props just so that they could pass them down, leading to growing signatures. Reading the react-admin source code didn't help to understand how to use a given component.

Last but not least: type checking was almost impossible. How can a custom Field component benefit from strong typing if it receives the most important props at runtime?

// v3
interface NameFieldProps {
    record: Person;
}

const NameField = ({ record }: NameFieldProps) => {
    return <span>{record.name}</span>;
};

const PostShow = props => (
    <Show {...props}>
        <SimpleShowLayout>
            {/* TypeScript error: required prop `record` is missing */}
            <NameField />
        </SimpleShowLayout>
    </Show>
);

Props injection used to be everywhere in react-admin, and we understood that it was the main cause of frustration for new developers.

Pulling Data With Hooks

We leveraged React contexts and removed props injection altogether. React-admin doesn't pass any prop to your components anymore. You don't have to worry about passing props down, either.

// v4
const PostShow = () => (
    <Show>
        <SimpleShowLayout>
            <TextField source="title" />
        </SimpleShowLayout>
    </Show>
);

If a component needs some of the data or callback defined earlier in the component tree, it has to pull it using purpose-driven hooks. For instance, to show a field or not based on permissions, you need to call usePermissions:

+const { usePermissions } from 'react-admin';

-const PostShow = ({ permissions, ...props }) => {
+const PostShow = () => {
+   const { permissions } = usePermissions();
    return (
-       <Show {...props}>
+       <Show>
            <SimpleShowLayout>
                <TextField source="title" />
                {permissions === 'admin' &&
                    <NumberField source="nb_views" />
                }
            </SimpleShowLayout>
        </Show>
    );
};

To build a custom field, grab the current record using useRecordContext:

-const NameField = ({ record }) => {
+const NameField = () => {
+   const record = useRecordContext();
    return <span>{record?.name}</span>;
};

Not having to inject props means react-admin components no longer need cloneElement:

// v4
const SimpleShowLayout = ({ children }) => (
    <div className="SimpleShowLayout">{children}</div>
);

The react-admin code, and your own code, are now easier to read and understand. The code executes faster, too.

As for type checking, since most hooks are generic, you get all the power of TypeScript:

// v4
const NameField = () => {
    const record = useRecordContext<Person>();
    return <span>{record.nname}</span>;
};
// TypeScript error: Property 'nname' does not exist on type 'Person'

Easier Custom Layouts

Props injection used to forbid custom layouts. For instance, if you tried to put two fields in the same row with a wrapping div, it would fail:

// v3 - does not work
const PostShow = props => (
    <Show {...props}>
        <SimpleShowLayout>
            <div style={{ display: 'flex' }}>
                <TextField source="title" />
                <TextField source="subtitle" />
            </div>
        </SimpleShowLayout>
    </Show>
);

It would fail for 2 reasons:

  • the <div> would receive a record prop, which is not valid HTML, and would cause a console warning
  • the <TextField> would not receive the required record prop, because <div> doesn't pass down unknown props

So you had to add extra code to pass down the record prop:

const Wrapper = ({ record, children }) => (
    <div style={{ display: 'flex' }}>
        {Children.map(children, child => cloneElement(child, { record }))}
    </div>
);

// v3 - works
const PostShow = props => (
    <Show {...props}>
        <SimpleShowLayout>
            <Wrapper>
                <TextField source="title" />
                <TextField source="subtitle" />
            </Wrapper>
        </SimpleShowLayout>
    </Show>
);

In v4, as there is no element cloning, the custom layout works out of the box.

// v4
const PostShow = () => (
    <Show>
        <SimpleShowLayout>
            <div style={{ display: 'flex' }}>
                <TextField source="title" />
                <TextField source="subtitle" />
            </div>
        </SimpleShowLayout>
    </Show>
);

As a consequence, custom layouts in react-admin v4 become straightforward. You don't even have to use a <SimpleShowLayout>! Here is an example:

import { Show, TextField, DateField, ReferenceField } from 'react-admin';
import { Grid } from '@mui/material';
import StarIcon from '@mui/icons-material/Star';

const BookShow = () => (
    <Show emptyWhileLoading>
        <Grid container spacing={2} sx={{ margin: 2 }}>
            <Grid item xs={12} sm={6}>
                <TextField label="Title" source="title" />
            </Grid>
            <Grid item xs={12} sm={6}>
                <ReferenceField
                    label="Author"
                    source="author_id"
                    reference="authors"
                >
                    <TextField source="name" />
                </ReferenceField>
            </Grid>
            <Grid item xs={12} sm={6}>
                <DateField label="Publication Date" source="published_at" />
            </Grid>
            <Grid item xs={12} sm={6}>
                <WithRecord
                    label="Rating"
                    render={record => (
                        <>
                            {[...Array(record.rating)].map((_, index) => (
                                <StarIcon key={index} />
                            ))}
                        </>
                    )}
                />
            </Grid>
        </Grid>
    </Show>
);

Easier Learning Curve

Not having to understand where the injected props come from, what they are, and how to use them is a great accelerator in learning React-admin. Of course, developers now have to learn how to use the hooks like usePermissions or useRecordContext. But these hooks are discoverable, documented, and type-safe.

Take a look at the documentation for react-admin version 4. Hooks are now indexed, searchable, and accessible from the top-level documentation. The documentation explains both the general usage, the inputs and outputs, and recipes for common use cases.

For instance, here the useRecordContext documentation.

Finally, IDEs provide code completion for these hooks, so the actual development is much faster.

Conclusion

Removing props injection wasn't a piece of cake. This pattern was used all over the react-admin codebase, so we had to touch every component to remove it. But the result is a huge boost in developer experience:

  • Less code: No need to pass down props, create wrappers, or clone elements. In our experience, the same application takes 30% less code in react-admin v4 than with v3.
  • Less WTF/min: With less magic, the code just works in all situations. Developers don't lose time trying to understand the inner workings or react-admin, or fight against the framework.
  • Better productivity: Developers can focus on business logic, and deliver features faster
  • More joy: Developing with react-admin v4 is much more fun than with v3. You have to try it to believe it, so start your next project with react-admin v4!
Did you like this article? Share it!