Search code examples
reactjsreact-router-dom

How to render a subroute and trigger react router parents loader?


I have this router:

const router = createBrowserRouter(
  [
    {
      path: '/',
      element: <Navigate to={'/dashboards'}/>
    },
    {
      path: '/dashboards',
      element: <Dashboards/>,
      loader: () => store.dispatch(retrieveWarehouses()),
      children: [
        {
          path: ':warehouse',
          element: <Dashboard/>,
          loader: ({ params }) => store.dispatch(monitorWarehouse(params.warehouse))
        }
      ]
    }
  ]
)

Defined as is, the <Dashboard/> component is not rendered, only its parent dashboard list (Dashboards, notice the plural). The loader of the child Dashboard is still triggered though.

If I don't use a nested route:

const router = createBrowserRouter(
  [
    {
      path: '/',
      element: <Navigate to={'/dashboards'}/>
    },
    {
      path: '/dashboards',
      element: <Dashboards/>,
      loader: () => store.dispatch(retrieveWarehouses()),
    },
    {
      path: '/dashboards/:warehouse',
      element: <Dashboard/>,
      loader: ({ params }) => store.dispatch(monitorWarehouse(params.warehouse))
    }
  ]
)

The child component Dashboard is rendered properly, but the loader of the parent is not triggered.

Here are the components:

Dashboards

const Dashboards: React.FC<any> = () => {
  const {
    warehouses,
    loading
  } = useAppSelector(selectWarehouseListState)

  if (loading) {
    return (
      <div className={'warehouse-list'}>
        <h1>Select warehouse</h1>
        <Spinner/>
      </div>
    )
  }

  return (
    <div className={'warehouse-list'}>
      <h1>Select warehouse</h1>
      {
        warehouses.map((wh: Warehouse) => (
          <NavLink to={`/dashboards/${wh.name}`} key={wh.name}>
            <div className={'selectable-warehouse container'}>
              {wh.name}
            </div>
          </NavLink>
        ))
      }
    </div>
  )
}

Dashboard

const Dashboard: React.FC<any> = () => {
  const { loading } = useAppSelector(selectWarehouseState)
  const { warehouse } = useParams()
  const dispatch = useAppDispatch()

  useEffect(() => {
    return () => {
      dispatch(stopMonitorWarehouse(warehouse))
    }
  }, [dispatch])

  if (loading) {
    return (
      <div className={'dashboard loading shrinkable'}>
        <div className={'header'}>
          <NavLink to={'/dashboards'} className={'nav-back'}>
            <ArrowBack/>
          </NavLink>
          <div className={'selected-warehouse-name'}>{warehouse}</div>
        </div>
        <div className={'status connecting'}>status: connecting</div>
        <Spinner/>
      </div>
    )
  }

  return (
    <div className={'dashboard active shrinkable'}>
      <div className={'header'}>
        <NavLink to={'/dashboards'} className={'nav-back'}>
          <ArrowBack/>
        </NavLink>
        <div className={'selected-warehouse-name'}>{warehouse}</div>
      </div>
      <div className={'status connected'}>status: connected</div>
      <div className={'logs-metrics'}>
        <Logs/>
      </div>
    </div>
  )
}

How can I access /dashboards/foo and trigger both loaders ?


Solution

  • If the Dashboards component is rendered as a layout route then it necessarily should render an Outlet for its nested routes to render their content into.

    Example:

    import { Outlet } from 'react-router-dom';
    
    const Dashboards: React.FC<any> = () => {
      const {
        warehouses,
        loading
      } = useAppSelector(selectWarehouseListState)
    
      return (
        <div className={'warehouse-list'}>
          <h1>Select warehouse</h1>
          {loading ? (
            <Spinner />
          ) : (
            <>
              {warehouses.map((wh: Warehouse) => (
                <NavLink to={`/dashboards/${wh.name}`} key={wh.name}>
                  <div className={'selectable-warehouse container'}>
                    {wh.name}
                  </div>
                </NavLink>
              ))}
              <Outlet />
            </>
          )}
        </div>
      );
    };
    

    However in my use case I don't want both components to be rendered, the latter should take precedence of the other.

    If I understand this part you want the Dashboards and Dashboard components rendered independently, but the Dashboards's loader function to still be called even while on a nested route. For this you'll render Dashboards as a nested index route where the loader function is on the parent layout route.

    Example:

    const router = createBrowserRouter(
      [
        {
          path: '/',
          element: <Navigate to="/dashboards" />
        },
        {
          path: '/dashboards',
          loader: () => store.dispatch(retrieveWarehouses()),
          children: [
            {
              index: true,
              element: <Dashboards />,
            },
            {
              path: ':warehouse',
              element: <Dashboard />,
              loader: ({ params }) => {
                store.dispatch(monitorWarehouse(params.warehouse));
              },
            }
          ]
        }
      ]
    );
    

    The "/dashboards" route will render an Outlet by default when no element is specified, and the Dashboards component will be rendered when the parent route is matched. No changes to the Dashboards component would be required.