#RR7 how to call a component

1 messages · Page 1 of 1 (latest)

desert heron
#

I am still struggling to understand how to use React Router. The documentation is very sparse, to say the least. Types are not defined, leaving me guessing how to do anything. Let's say I have a component.tsx

import type { Route } from "./+types/component";


export async function loader({ params }: Route.LoaderArgs) {
  const response = "foo";
  return response;
}

export default function Component({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData} bar</div>;
}

then I can call the component from the browser with localhost:5173/component no problem.

But what if I want to include that component in another page, such as call-component.tsx

import type { Route } from "./+types/call-component";
import Component from "./component";

export default function CallComponent() {
  return <Component />;
}

Now I get this error:

Type '{}' is missing the following properties from type 'ComponentProps': params, loaderData, matchests(2739)
🌟 Highlight Code

(alias) function Component({ loaderData }: Route.ComponentProps): JSX.Element
import Component
vestal dove
#

I would create a component independent of the routing. Use props to pass to it any data you need from loaderData or params. Then you can call that component from any route.

#

e.g. if two routes use that component then you need three components, the main component itself plus the two components for the routes.

desert heron
#

So just reading the documentation, there is no description of what loaderData or params are. So it is not possible to use them to pass data to any RR7 components, but they seem to be expected by all components. That's the basic problem.

The docs also do not discuss the possibility of creating or using components independent of the routing as you say.

In this case, I have a component that retrieves "foo" from the server and prints "foo bar" on the screen, and I can't figure out how to call that component in a RR7 project.

In the actual application, the loader will read data from a URL, but I'm trying to start with the basics. And yes, I've read the tutorial and run the templates on GitHub.

vestal dove
#

loaderData is the data returned from the loader function (if any), params are any dynamic parameters in the path, i.e. if your path is "/foo/:id" and the user visits "/foo/bar" then params will be { id: 'bar' }.

#

By a component independent of the routing I just mean a component that isn't exported as the module component. Just a regular component that takes whatever it needs as props. So for example if you need that id from params you would have something like:

export default function CallComponent({ params }: Route.ComponentProps) {
  return <Component id={params.id} />;
}

This way Component doesn't care about the routing details, it just wants an id string.

desert heron
#

In this case there are no parameters. It is just a static route. I am still getting error messages about params and loaderData missing.

#

And since the docs don't discuss how to not export a component as a module component, I'm not sure what to omit. I'm literally just copying from the docs here.

vestal dove
#

In your example you're trying to call the component exported from the module from another module. What I'm suggesting is that instead of doing that you move that first Component into another file. Something like:

function Component(props: { data: string }) {
  return <div>{props.data} bar.</div>;
}

Then from the first module you set that as:

#
export default RouteComponent({ loaderData }: Route.ComponentProps) {
  return <Component data={loaderData} />;
};
#

And then you can use Component in any other route you like and pass whatever you want in as data.

desert heron
#

The problem with stripping the RR7 stuff from Component is that it does actually call the server to get data, in this case {data}. So I think I need the loaderData stuff, which is what RR7 provides, right?

vestal dove
#

You're still using loaderData

#

You're just separating the routing from the UI implementation.

neat oasis
#

Hi Harold 👋 It seems to me you're associating the "RR7 stuff" to components. RR7 ties to routes, not to components themselves. Routes call their loaders when they're, well, loaded. Then they render their default export. You can render whatever you want in the default export, components from anywhere

#

you define the type of loaderData and what your component needs to render

desert heron
#

Well, in my example above

import type { Route } from "./+types/component";


export async function loader({ params }: Route.LoaderArgs) {
  const response = "foo";
  return response;
}

export default function Component({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData} bar</div>;
}

params must be of type Route.LoaderArgs, which is not described anywhere. And loaderData is of type Route.ComponentProps, which is also not described anywhere. So I'm not sure how I could ever pass anything in to them.

vestal dove
#

In your loader function you're spreading Route.LoaderArgs. params is a property of the argument to the loader function. In your case you don't need it as you're not using any params. That the :id we mentioned earlier.

#

Your loader function can just be

export function loader() {
  return "foo";
}
neat oasis
#

params are any dynamic segment from the url

#

loaderData is whatever your loader returns

vestal dove
#

loaderData in the component props should be typed as the return type of the loader function, in this case string.

desert heron
#

@neat oasis The code is not building without passing params.

Type '{}' is missing the following properties from type 'ComponentProps': params, loaderData, matchests(2739)
🌟 Highlight Code

(alias) function Component({ loaderData }: Route.ComponentProps): JSX.Element
import Component
vestal dove
#

I believe that would be from your second route where you're trying to render it as <Component />.

desert heron
#

Thank you both for your help. I'm sure it will make sense eventually.

vestal dove
#

The exported component in each route module expects a specific set of props. If you try to import that component elsewhere and use it you're going to get typescript errors unless you can match those props exactly.
So, you basically don't do that. The component exported by the route file should not be imported by any of your code (RR code will do that).
If you have multiple routes that use a similar UI, you can abstract that UI into a separate component which is called by all of those routes.

desert heron
#

Again, this is just hard because there is no guidance about how to use these things.

For instance, if component.tsx contains

import type { Route } from "./+types/component";

export async function loader() {
  const response = "foo";
  return response;
}

export default function Component({ loaderData }: Route.ComponentProps) {
  var response : string;
  return <div>{response} bar</div>;
}

and call-component.tsx contains

import type { Route } from "./+types/call-component";
import Component from "./component";

export default function CallComponent() {
  return <Component />;
}

I get two errors. In call-component.tsx, it says Cannot find module './component' or its corresponding type declarations.

And in component.tsx, Variable 'response' is used before being assigned.

It is really unclear what loaderData is or when it is assigned, or how to use it, or what its fields are. I'm just guessing that maybe I should get at the server data by loaderData.response, but who knows?

neat oasis
#

You're missing a loader at call-component. You're creating two independent routes there.

vestal dove
#

You haven't given response any value there, you declared it. loaderData is the return value of the loader in your route, so in this case "foo".

#

That

import Component from "./component"

Is what I was saying you should not do.

desert heron
#

loaderData is the return value of the loader in your route, so in this case "foo".

Ah, that's useful. So now component.tsx is this, and it gives no errors:

import type { Route } from "./+types/component";

export async function loader() {
  const response = "foo";
  return response;
}

export default function Component({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData} bar</div>;
}

Presumably it would return foo bar, correct?
But for call-component.tsx,

import type { Route } from "./+types/call-component";

import Component from "./component";
export default function CallComponent() {
  return <Component />;
}

gives

Type '{}' is missing the following properties from type 'ComponentProps': params, loaderData, matchests(2739)

and if I remove the import Component from "./component" line, it obviously can't find Component any longer.

Adding a loader function as @neat oasis suggested

import type { Route } from "./+types/call-component";

import Component from "./component";

export async function loader() {
  const response = "foo";
  return response;
}

export default function CallComponent() {
  return <Component />;
}

does not seem to do anything to remove the errors.

neat oasis
#
// route1.tsx
import type { Route } from "./+types/component";

export async function loader() {
  const response = "foo";
  return { response };
}

export default function Component({ loaderData }: Route.ComponentProps) {
  var { response } = loaderData;
  return <MyComponent text={response} />;
}

// route2.tsx
import type { Route } from "./+types/component";
import { MyComponent } from "./components/my-component";

export async function loader() {
  const response = "foo";
  return { response };
}

export default function Component({ loaderData }: Route.ComponentProps) {
  var { response } = loaderData;
  return <MyComponent text={response} />;
}

// components/my-component.tsx
export function MyComponent({ text: string }) {
  return (
    <div>{text} bar</div>
  );
}```
#

the export default function Component that receives Route.ComponentProps isn't meant to be exported. That's just the interface for RR to know what to render with the loaded data

#

You can make reusable components like in any other React app

#

And you can create functions that run in the server to reuse code in loaders and actions

#

possibly with syntax errors 😛

desert heron
#

my call-component.tsx file is the same as your route2.tsx file

import type { Route } from "./+types/call-component";

import Component from "./component";

export async function loader() {
  const response = "foo";
  return response;
}

export default function CallComponent() {
  return <Component />;
}

and it gives this error:

Type '{}' is missing the following properties from type 'ComponentProps': params, loaderData, matchests(2739)
neat oasis
#

it isn't quite the same, you're trying to import a default export from another route

#

that doesn't work

#
// route1.tsx
import type { Route } from "./+types/component";
import { MyComponent } from "./components/my-component";
import { returnFoo } from "./foo.server";

export async function loader() {
  const response = returnFoo()
  return { response };
}

export default function Component({ loaderData }: Route.ComponentProps) {
  var { response } = loaderData;
  return <MyComponent text={response} />;
}

// route2.tsx
import type { Route } from "./+types/component";
import { MyComponent } from "./components/my-component";
import { returnFoo } from "./foo.server";

export async function loader() {
  const response = returnFoo()
  return { response };
}

export default function Component({ loaderData }: Route.ComponentProps) {
  var { response } = loaderData;
  return <MyComponent text={response} />;
}

// components/my-component.tsx
export function MyComponent({ text: string }) {
  return (
    <div>{text} bar</div>
  );
}

// foo.server.ts
export function returnFoo() {
  return "foo";
}
#

example with reusing a function in the loader

#

note that the imported component is a regular component, not a default export from a route

neat oasis
#

the loader, action and Component exports of routes need to have specific call signatures to work

#

but only these, you can add whatever you need inside them

desert heron
#

OK, this works now. If call-component.tsx is

import type { Route } from "./+types/call-component";

import { Component } from "./component";

export async function loader() {
  const response = "foo";
  return response;
}

export default function CallComponent({ loaderData }: Route.ComponentProps) {
  return (
    <>
      {loaderData} <Component />
    </>
  );
}

and component.tsx is

export function Component() {
  return "bar";
}

then localhost:5173/call-component returns foo bar and localhost:5173/component returns bar

So what we've learned is that

  1. every tsx file that has things like import type { Route } from "./+types/call-component"; has to have a loader function and a export default function. and the signature of the export default function has to look like this: export default function CallComponent({ loaderData }: Route.ComponentProps). , even though the component itself will not ever have loaderData ever passed to it.

  2. the loader function will return a variable called loaderData, even though loaderData is never defined anywhere, just use it and don't think about it too much.

  3. You can't use components imported from routes.

neat oasis
#
  1. those would be your routes, so you'll probably want to load data and render something, but it isn't mandatory. You can have resource routes (that only have a loader and/or action without a component), and you can have a loader that does nothing (not 100% sure if you can have a component without a loader, I'm still catching up to RR7, I'm more used to Remix).
#
  1. Yes
#
  1. I don't see why you would want to, honestly 😅 the route interfaces are what plug into RR and its APIs
desert heron
#

How would you use the data from a resource route if it seems so difficult to call routes from other routes?

neat oasis
#

why do you need to "call routes" from other routes? Think of a route as a URL, or HTML page

#

I'm trying to understand what you want to do

desert heron
#

Let's say you have a "resource route" that just gets data from your backend, and then you want to use that data in a few other routes.

#

Maybe you'd just call that url from within your other routes....

neat oasis
#

Or you can have a function that accesses your DB or your backend, and call that function on the loaders of each route that need the data

#

it depends on the use case

desert heron
#

I like your second suggestion. The Fetcher API link you gave above seems to be defining the API by giving a few examples, which is suboptimal. Is there actually a reference for Fetchers, or do I have to read the source code?

neat oasis
desert heron
#

Yep. Thanks for your help. Hopefully I'll be able to get data from my backend now and put it in a datagrid. 🤞

neat oasis
#

not sure they're 100% the same but it might help

desert heron
#

It wouldn't be web dev without your favorite framework becoming obsolete every few weeks in favor of a new, undocumented framework.

neat oasis
ancient cave
#

i would extract it into its own react componment

#

accept props

#

and that way use it in both places

desert heron
# ancient cave thats because it is a route component, its is not a normal react component?

@ancient cave I think you're correct, but I needed a more basic discussion. Please read the long thread above, if you're interested. I am coming into React Router with a lower skill level than I think the documentation authors expected. So concepts that perhaps are obvious to others and don't need to be spelled out were insufficient for me. But @neat oasis and @vestal dove were helpful and patient. Result: I now can retrieve data and load it into my components. I'm sure it won't be the last time I scratch my head on the basics, but it is enough to display data from my backend now!

desert heron
#

Well actually I lied. I can see the data in my React component but can't actually display it in a data grid. I'll put together a code sandbox and see what you all have to say. Stay tuned. 📺

ancient cave
desert heron
vestal dove
#

Disclaimer: I'm not particularly familiar with using react-router with ssr enabled. But, if you're talking about the document is not defined error ... that's coming from the tabulator library which is expecting to be run in a browser (client) only. In this case you're running it on node (the server) and node doesn't know anything about document.

desert heron
#

@vestal dove I switched to using client-side rendering and now the error is

Objects are not valid as a React child (found: object with keys {options, columnManager, rowManager, footerManager, alertManager, vdomHoz, externalEvents, eventBus,

Do I have have to use <Outlet> ?

That's strange that Tabulator wouldn't work in SSR. I was able to get Syncfusion to work (with heavy support from their customer support group) using SSR. The problem was that I had included the react() plugins and cjsInterop() in vite.config.ts

I was under the impression that it is always better to use SSR to hide implementation details and also for performance.

neat oasis
#

It's a matter of the type of library you use. If you use any library that rely on browser APIs (document, window, etc) you can't try to render them on the server. They can only be rendered on the client. the rest of the page can be rendered on the server, though.

#

I've used guard clauses in the past such as if(typeof document === undefined) return null before calling anything that is client-side only, such as that Tabulator you have

vestal dove
#

The table needs to be done something like this:

export default function Spreadsheet({ loaderData }: Route.ComponentProps) {
  const table = useRef(null);
  const tabulator = useRef();

  useEffect(() => {
    tabulator.current = new Tabulator(table.current, {
      data: exampledata,
      columns: [
        { title: "OrderID", field: "OrderID" },
        { title: "CustomerID", field: "CustomerID" },
      ],
    });
  }, [exampledata]);

  return <div ref={table} />;
}
#

In this case exampledata doesn't need to be in the dep array as it's stable, but I assume you're going to change that to loaderData and that will need to be there.

#

As Tabulator is now inside a useEffect you should be able to use it with ssr enabled.

neat oasis
#

By the way Harold, it might be worth mentioning that this use case you have isn't a simple one because of the type of component you're trying to render. It would be much simpler if you were trying to use something like a regular HTML table, for example.

desert heron
#
import type { Route } from "./+types/spreadsheet";

import { TabulatorFull as Tabulator } from "tabulator-tables";
import exampledata from "../data/datasource.json";
import "tabulator-tables/dist/css/tabulator.min.css";
import { useRef, useEffect } from "react";

export async function Clientloader() {
  const data = "foo";
  return data;
}

export default function Spreadsheet({ loaderData }: Route.ComponentProps) {
  // Create column definitions

  // Create data array
  const table = useRef(null);
  const tabulator = useRef();

  useEffect(() => {
    tabulator.current = new Tabulator(table.current, {
      data: exampledata,
      columns: [
        { title: "OrderID", field: "OrderID" },
        { title: "CustomerID", field: "CustomerID" },
      ],
    });
  }, [exampledata]);
  /* console.log("columns", columns);
  console.log("transformedData", transformedData);
  console.log("loaderData", loaderData); */
  return <div ref="table"></div>;
}

This produces Function components cannot have string refs. We recommend using useRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref

Clearly I have a lot more reading to do about React.

neat oasis
#

Yes, this kind of component requires more React knowledge

vestal dove
#

<div ref={table} /> not <div ref="table" />.

desert heron
#

That worked.

vestal dove
desert heron
#

Now I just have to figure out what is going on. Do you think that would work with SSR, and is that in general preferred?

vestal dove
#

There is a react-tabulator module, but it failed dep checks when I tried to install it.

desert heron
#

React-tabulator seems very old to me and I was not able to get that to work.

vestal dove
#

The manually including part section is old, still using class components. I converted that to hooks, which is what I posted for you.

desert heron
#

SSR worked fine with Tabulator. I just changed Clientloader to loader and changed ssr: true, in react-router.config.ts just as @vestal dove said.

vestal dove
#

I imagine your next step is return the data from the loader function. You could simply return exampledata from that and it would work, however this means that the json file is only read when you build the app and that's always the data it will return. Instead you'd likely want to use node readFile from fs.promises within the loader. https://nodejs.org/api/fs.html#fspromisesreadfilepath-options

desert heron
#

The data will be read from a Go backend.

#

It is supposed to be an interactive application, so the user would enter data into the table, which would pass that to the backend, which would process the data and send it back to the table. Like a spreadsheet.

vestal dove
#

Also worth noting that with this method, the table is not server side rendered. All the server side rendering will give you is the empty div, and the table isn't rendered until the client does it. If that's something you want/need you would have to look into alternative table libraries or create it yourself. I don't have any particular recommendations in that regard.

desert heron
#

So the useEffect is executed on the client, or is that just because Tabulator only works on the client?

vestal dove
#

Any code in useEffect only runs on the client. That is what is allowing Tabulator to work in this case.

vestal dove
#

By the way, if you do continue to use tabulator, this might be more optimal with dynamic data:

export default function Spreadsheet({ loaderData }: Route.ComponentProps) {
  const table = useRef(null);
  const tabulator = useRef();

  useEffect(() => {
    tabulator.current = new Tabulator(table.current, {
      columns: [
        { title: "OrderID", field: "OrderID" },
        { title: "CustomerID", field: "CustomerID" },
      ],
    });
  }, []);

  useEffect(() => {
    if (! tabulator.current) return;
    tabulator.current.setData(loaderData);
  }, [loaderData]);

  return <div ref={table} />;
}

With the previous code when the data changed it would create a brand new Tabulator every time. In this updated code the first useEffect only runs once when the component is mounted, creates the tabulator and attaches it to the div. When the data is updated the second useEffect runs and updates the data in the existing tabulator.