Sergio Xalambrí

Sending data from layout to leaf routes in Remix

Let's say you are building a invoicing app, you have a URL /invoices which loads a list of invoices with their ID, date and maybe one or two more fields, you render a sidebar with this list and in the main area you rending some image inviting the user to click on an invoice.

Then the user navigates to /invoices/:id and here you have another loader which will fetch only that invoices and return more data.

But let's say you need to pass something from the /invoices to the /invoices/:id, maybe you want to whole list there to show links to the next and previous invoice after the currently open invoice.

The easiest way to do this right now in Remix, is to either get that data again in the /invoices/:id route loader or render a context provider wrapping the Outlet component from React Router DOM in the layout route and pass the data you want to share there, then in the leaf route (/invoices/:id) you can call useContext to read that data.

If this is something your app needs to do a lot you could directly wrap the Outlet component in a custom one already integrated to the context provider and receiving the data as props.

import { createContext, useContext } from "react";
import { Outlet as RROutlet } from "react-router-dom";

type OutletProps<Data> = { data?: Data };

let context = createContext<unknown>(null);

export function Outlet<Data = unknown>({ data }: OutletProps<Data>) {
  return (
    <context.Provider value={data}>
      <RROutlet />
    </context.Provider>
  );
}

Then you can create a useParentData hook, similar to the useRouteData of Remix, which will use the context defined above to access to the data. Since the context value could be null we need to throw an error if it's null.

export function useParentData<ParentData>() {
  let parentData = useContext(context) as ParentData | null;
  if (parentData === null) throw new Error("Missing parent data.");
  return parentData;
}

Now we can use it, in the layout route we can do something like this:

export default function InvoicesList() {
  let { invoices } = useRouteData<{ invoices: Invoice[] }>();
  return (
    <div>
      <InvoicesSidebar list={invoices} />
      <Outlet data={invoices} />
    </div>
  );
}

And in the leaf route we can access to the route data and the parent data.

export default function InvoicesList() {
  let { invoice } = useRouteData<{ invoice: Invoice }>();
  let { invoices } = useParentData<{ invoices: Invoice[] }>();
  return (
    <div>
      <Details invoice={invoice} />
      <Suggestions current={invoice} list={invoices}>
    </div>
  )
}

This way, with a small wrapper we can create a wrapper of Outlet to avoid creating one context and doing the whole provider + hook setup everytime we need to send data from a layout to a leaf route.