Search code examples
reactjsreact-admin

Multiple React Admin Lists with filters on one screen


We've built an administration UI based on React Admin and in a couple of occasions we want to use multiple lists on a single screen for convenience. E.g. on a user detail view, we want to show the multiple addresses the user has on file, and the purchases the user has made. User, address and purchase are all "resources" in RA terminology.

It roughly looks like this:

/* file <...>/resources/users/UserShow.js */

import AddressList from '../addresses/AddressList';
import PurchaseList from '../purchases/PurchaseList';

const UserShow = ({ id, classes, translate, ...props }) => (
    <Fragment>
        <Show id={id} {...props}>
            ...render user details here...
        </Show>
        <AddressList {...props} basePath={'/addresses'} resource={'addresses'} filter={{ userId: id }} />
        <PurchaseList {...props} basePath={'/purchases'} resource={'purchases'} filter={{ userId: id }} />
    </Fragment>
);

Most of it seems to work nicely but the filters give us some headache. Everything around it seems to be built for exactly one filter at a time, without any means to customize (like e.g. specifying a property to use for the filter in the redux and react-router stores, instead of using a hardcoded one)

Has anyone ever made two filterable lists on the same screen work and has some more pointers?


Solution

  • I eventually came up with a workaround that I would like to share.

    The problem is that RA propagates the filter/page/sort values into the query, and it maintains a "signature" of the filter object (a string representation of the corresponding JSON) to detect changes in the current filter in case of URL changes. With this approach, filter and pagination values remain active when the user navigates back and forth, and it's only updated if an actual field value changes.

    That’s a good thing if there’s one list per screen, but in our case a filter update of one list leads to a back-propagation of the values into every other list on the screen.

    First, I tried to keep all filters by modifying the query so that every filter uses their resource name as a key. Unfortunately, this caused a huge list of changes including copying entire RA components due to tinkering with RAs internals.

    NB: RA already keeps filter values per resource in its Redux store, so it's beyond me why they're not using a similar syntax/approach for the query.

    Another option is to cut off the propagation into the query entirely. The tradeoff is that you can't bookmark specific searches, and you need to reactivate filters when you navigate, but this seems to be a small and acceptable disadvantage.

    Here's the relevant code in RA 3 which updates the query: https://github.com/marmelab/react-admin/blob/master/packages/ra-core/src/controller/useListParams.ts#L164

    history.push({
        search: `?${stringify({
            ...newParams,
            filter: JSON.stringify(newParams.filter),
            displayedFilters: JSON.stringify(newParams.displayedFilters),
        })}`,
    });
    

    There's no way to directly modify this behavior, but we can supply our own history implementation to RA and override the search parameter. The corresponding monkey patch looks like that:

    const history = createBrowserHistory({
        basename: process.env.PUBLIC_URL
    });
    
    const oldPush = history.push;
    history.push = state => {
        oldPush({
            ...state,
            search: null,
        });
    };
    

    It could then be used like this:

    const App = () => (
        <Admin
            history={history}
            ...
        >
            ...
        </Admin>
    );