Mastering React-admin Resources to Improve Your App Performance
Morgane Riclet7 min read
Let me tell you the story of how I improved my react-admin app runtime loading performance from 1 minute to 1 second.
I work at Theodo on a project with many websites. To configure and manage all of them, we had an administration tool (let’s call it AdminV1). But AdminV1 was old: it was an Angular project coded with coffee.script and jade, and was not very maintainable. And the product was difficult to use: there were too many menus, UX was bad, it was difficult to onboard new administrators on the application.
The team decided to create a brand new application with modern technologies: AdminV2, with react-admin, a framework for building administration applications.
Everybody was happy: for developers, it was easier to implement new features. With a few lines of code, they could create tables to administrate users, app configurations and privileges. For Product Owners and administrators, the design was more ergonomic and it was easier to manage their applications.
We migrated many features from AdminV1 to AdminV2 and added new administration tools. Sponsors were delighted. But sprint after sprint, AdminV2 became slower to start. After some months, users had to wait more than one minute to access the app. It was painful to develop new features on AdminV2 because each hot reload took 1 minute, and users were frustrated by waiting so long to access the application.
We started to investigate and found that by deleting some Resources in our application, the speed at runtime increased significantly
But what are react-admin Resources ?
”<Resource>
components are fundamental building blocks in react-admin apps. They are strings that refer to an entity type.”, according to react-admin documentation.
To illustrate, on my project, we need to administrate users of websites. We have to create a resource “/user”, associated with components:
- UserList (for table view)
- UserEdit (to edit an user)
- UserCreate (to create a new user).
import UsersList from "components/Users/list";
import UsersEdit from "components/Users/edit";
import UsersCreate from "components/Users/create";
const UserResource = () => (
<Resource
key="/user"
name="/user" // endpoint of the API to call to get users
create={UsersCreate}
list={UsersList}
edit={UsersEdit}
/>
);
React-admin is in charge of storing this information. When the user goes on /user URL, react-admin displays the list, edit or create view and manages all the logic to get, edit and create users.
Sounds like magic, not helping us understand why creating many Resources degrades the performance.
I dived deeper into the react-admin code to understand how the framework creates resources and links them with components.
A <Resource />
component is declared like this:
const Resource = (props: ResourceProps) => {
const { intent = "route", ...rest } = props;
return intent === "registration" ? (
<ResourceRegister {...rest} />
) : (
<ResourceRoutes {...rest} />
);
};
React-admin Code
Your intent can be either registration
or route
:
ResourceRegister
’s purpose is to dispatch an action that saves your Resource into a redux store. Each time you define a<Resource />
component in your app, react-admin will register your Resource’s props into the redux store:- the name, which is the URL of the resource and also the API endpoint to fetch your entity data
- which views are available to manage the entity. For our user entity, available views are list, edit and create
dispatch(
registerResource({
name,
hasList: true,
hasEdit: true,
hasShow: false,
hasCreate: true,
})
);
React-admin Code
ResourceRoutes
is rendered when a user wants to access the URL associated with the Resource. The URL (which is also generally the endpoint of the API) is stored in the Resource’sname
prop. ResourceRoutes renders all<Route />
components associated with each view defined in the Resource. For our Entity “user”, it will be:
const ResourceRoutes = () => (
<ResourceContextProvider value={name}>
<Switch>
<Route
path={`${basePath}/create`} // Create view
render={(routeProps) => (
<WithPermissions component={CreateUser} {...routeProps} />
)}
/>
<Route
path={`${basePath}/:id`} // Edit view
render={(routeProps) => (
<WithPermissions component={EditUser} {...routeProps} />
)}
/>
<Route
path={`${basePath}`} // ListView
render={(routeProps) => (
<WithPermissions component={ListUser} {...routeProps} />
)}
/>
</Switch>
</ResourceContextProvider>
);
React-admin Code
For example, if an administrator connects to a react-admin app and wants to see a list of users, the framework will load resources like this:
This allows calling the API only when the user goes on the associated Menu and to not call all the endpoints available in the application at runtime.
Ok, so what’s going on with AdminV2 ?
In AdminV2, we defined our Resources like this:
const RootComponent = () =>
{
const resources = getResources()
return (
<AdminContext {...props}>
<AdminUI>
{resources}
</AdminUI>
<AdminContext />
}
GetResources
returned all Resources in our application: user Resources and other 600 Resources (yes that’s a lot).
So what’s happening when a user access AdminV2 ?
- First, over 600
<Resources />
components are rendered - Then, each of these
<Resources />
dispatches an action to store their data in redux store (the “registration” intent seen above) - React-admin redux reducer gets these 600 actions and for each of them, it stores the data in the redux store
- Finally, when the reducer has updated the store with all resources action payload, the home page of AdminV2 is displayed
These 4 steps take more than a minute. Eventually, the user has gone for a coffee, otherwise, he left the app and will never come back again
How to avoid this 1 minute of loading time?
We thought about diverse solutions:
1 - Load only resources to which the user has access
On AdminV2, to access each menu, the user needs to have the associated privilege. If the user has access to one menu, we don’t have to load all resources, but only the resources used in the menu.
This solves the problem for regular users with few privileges. But Developers and Product Owners have access to all menus.
2 - Patch react-admin to modify Resource component
We thought of modifying directly the react-admin Resource component, to modify the dispatch action. Instead of dispatching one action for each Resource, we could dispatch one action for multiple resources. This way, we don’t have to update the state 600 times.
But this solution is not very maintainable: it implies updating the patch each time we update react-admin.
3 - Lazy load Resources
We don’t have to create every Resources at the initialization of the application. An administrator will never access all AdminV2 menus and, even if he wanted to, in most cases, he doesn’t have all menus accesses. We need to load Resources only when the user needs them. So we decided to create them when the user accesses the associated url. We modified our RootComponent
like this:
const RootComponent = () => {
const [resources, setResources] = useState([]);
const location = useLocation();
useEffect(() => {
const resourcesToAdd = getResources(location.pathname)
setResources([...resources, ...resourcesToAdd]);
}, [location.pathname])
return (
<AdminContext {...props}>
<AdminUI>
{resources}
</AdminUI>
<AdminContext />
)
}
We modified the getResource function so that it takes the current URL in parameters and returned the Resources associated with the URL.
For example, if an administrator wants to access the user administration menu (URL ‘/user’), getResources
will only return
<Resource name="/user" list={UserList} edit={UserEdit} create={UserCreate} />.
The RootComponent
will render the new Resource:
- The first intent will be
registration
and the Resource will be stored in redux state - The second intent will be
route
. React-admin will create the Routes for user entities and the administrator will access the user management menu
In order not to save resources each time the user goes back on the same URL, we added a filter:
const RootComponent = () => {
const [resources, setResources] = useState([]);
const [loadedResources, setLoadedResources] = useRef([]);
const location = useLocation();
useEffect(() => {
const resourcesToAdd = getResources(location.pathname).filter(
(resource) => !loadedResources.includes(resource.name)
);
setResources([...resources, ...resourcesToAdd]);
setLoadedResources([
...loadedResources,
...resourcesToAdd.map((resource) => resource.name),
]);
}, [location.pathname]);
return <AdminUI>{resources}</AdminUI>;
};
Let’s sum up
Before, the user had to wait more than one minute to access the app. Then the navigation was quite quick.
Now, the user access AdminV2 in one second. And the navigation stays quite quick!
As developers, when using “black box” framework like react-admin, we do not always try to understand how it really works. By taking the time to explore the source code of these frameworks, by understanding what libraries they use, how they are implemented, we can save our time later and help us detect this kind of performance issue sooner.
That is the conclusion of AdminV2 performance story. And developers developed happily ever after and added a lot of features to AdminV2!