Passing props to child component with react-router v6 - reactjs

I am using React Router v6 with all routes in a single config file, hence, making use of <Outlet /> to render the child. However I don't know how to pass the appropriate props depending on the component that matches when having my routes in a separate config file.
// route.ts
export const routes = [
{
path: '/*',
element: <Parent />,
children: [
{
path: 'child1',
elements: <Child1 />, // Typescript complains: Property 'firstName' is missing in type '{}' but required in type 'IProps'
},
{
path: 'child2',
elements: <Child2 />, // Property 'lastName' is missing in type '{}' but required in type 'IProps'
}
]
}
]
// App.ts
const App = () => {
const _routes = useRoutes(routes)
return _routes
}
// Parent.ts
const Parent = () => {
const firstName = 'John'
const lastName = 'Doe'
return (
<h1>Parent Component</h1>
<Outlet /> // How to pass the appropriate props?
)
}
interface IFirstNameProps {
firstName: string
}
interface ILastNameProps {
lastName: string
}
export const Child1 = (props: IProps) => {
return (
<h2>First Name</h2>
{props.firstName}
)
}
export const Child2 = (props: IProps) => {
return (
<h2>Last Name</h2>
{props.lastName}
)
}
Using the <Route> component it would be something like this:
const Parent = () => {
const firstName = 'John'
const lastName = 'Doe'
return (
<Route path='child1' element={
<Child1 firstName={firstName} />
} />
<Route path='child2' element={
<Child2 lastName={lastName} />
} />
)
}
How can I achieve the same thing with a routes.ts config file and <Outlet />?

The best way to deal with this situation is by using outlet context.
In the parent component pass context via Outlet.
// parent.ts
const Parent = () => {
const firstName = 'John'
const lastName = 'Doe'
return (
<h1>Parent Component</h1>
<Outlet context=[firstName, lastName]/> // How to pass the appropriate props?
)
}
Then, in the children components, get those context data using useOutletContext hook.
import { useOutletContext } from "react-router-dom";
export const Child1 = () => {
const [firstName] = useOutletContext();
return (
<h2>First Name</h2>
{props.firstName}
)
}
export const Child2 = () => {
const [lastName] = useOutletContext();
return (
<h2>Last Name</h2>
{props.lastName}
)
}

Related

Pass generic type from a parent to its children in React TS

i would like to know if it's possible to pass a generic type from a parent component to its children like this:
interface ParentProps<T> extends PropsWithChildren {
data: T;
}
const Parent = <T,>(props: ParentProps<T>) => {
return <div>{props.children}</div>;
};
interface ChildProps<T> {
fieldName: keyof T;
}
const Child = <T,>({ fieldName }: ChildProps<T>) => {
return <div>{fieldName}</div>;
};
const Example = () => {
return (
<Parent data={{ firstName: 'John' }}>
<Child fieldName='firstName' />
<Child fieldName='lastName' /> // would like to get an error here
</Parent>
);
};
And I don't want to manually cast the children like this:
type Data = {
firstName: string;
}
const Example = () => {
return (
<Parent data={{ firstName: 'John' }}>
<Child<Data> fieldName='firstName' />
<Child<Data> fieldName='lastName' /> // error triggered bc the component is casted and the filedname doesn't exist
</Parent>
);
};

Why are there two different ways to use it?

the same component is stored in state and ref respectively, but the calling methods of the two are different.
export default function getAsyncComponent(load) {
const AsyncComponent = () => {
const [component, setComponent] = useState(null)
const com = useRef(null)
const loadComponent = async () => {
const _component = await load()
com.current = _component.default
setComponent(_component.default)
}
useEffect(() => {
loadComponent()
}, [])
console.log(component, com.current)
const Com = com.current
return (
<div>
// Both ways are works.
{component ? component : null}
{Com ? <Com /> : null}
</div>
)
}
return <AsyncComponent />
}
print:
{$$typeof: Symbol(react.element), type: 'div', key: null, ref: null, props: {…}, …}
and
ƒ IndexPage() {
return ((0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", { children: [(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", { className: _index_less__WEBPACK_I…
The use of getAsyncComponent is like this.
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={getAsyncComponent(() => import('src/pages/index'))}
/>
</Routes>
</BrowserRouter>
)
}
Are the above two calling methods different?
react#18.1
react-router-dom#6.3

What type to use when implement RouteComponentProps on react typescript component

I am trying to make my first component that receive parameter using react-router-dom.
This is my component:
const ProductDetailsPage: React.FC<IPage & RouteComponentProps<any>> = (props: Props) => {
useEffect(() => {
const id = props.match.params.id;
}
return <h1>product details</h1>
}
interface Props {
match: {
params: {
id: string | undefined;
};
};
}
Here eslint show me a warning that I should not use any but I don't understand what type to set here.
Also I wrote props: Props but I don't know if this is correct way to do it like this.
UPDATE more code for preview:
export default interface IPage {
name: string;
}
const routes: IRoute[] = [
{
path: '/products/:id',
name: 'details',
component: ProductDetailsPage,
exact: true,
},
];
const ProductsPage: React.FC<IPage> = () => {
const match = useRouteMatch();
return (
<div>
<h4>This is products page</h4>
<div>
<Link to={`${match.url}/44`}>Product 44</Link>
<Link to={`${match.url}/45`}>Product 44</Link>
<Link to={`${match.url}/46`}>Product 44</Link>
</div>
<Switch>
{routes.map((route, index) => {
return (
<Route
key={index}
path={route.path}
render={(props: RouteComponentProps<any>) => (
<route.component name={route.name} {...props} {...route.props} />
)}
/>
);
})}
</Switch>
</div>
);
};
interface Props {
match: {
params: {
id: string | undefined;
};
};
}
const ProductDetailsPage: React.FC<IPage & RouteComponentProps<Props>> = (props) => {
const [message, setMessage] = useState<string>('');
useEffect(() => {
const id = props.match.params.id;
if (id) {
setMessage(`Show details for product: ${id}`);
} else {
setMessage(`No parameter passed`);
}
}, [props.match.params.id]);
return <h4>{message}</h4>;
};
export default withRouter(ProductDetailsPage);
The generic (T) in RouteComponentProps<T> defines the params type.
A param value is always a string or undefined.
If the params are unknown it would probably be better to use Record<string, string | undefined> to type the params.
type Params = { id: string | undefined; };
const Test: React.FC<RouteComponentProps<Params>> = ({
match: { params: { id } }
}) => {
const idIsStringOrUndefined: string | undefined = id;
return null;
}
Also props should be typed automatically by typing the component with React.FC<RouteComponentProps<Params>>.
You can refer to this Playground to understand RouteComponentProps.
type TParams = {id:string | undefined};
type TProductDetailsProps = IPage & RouteComponentProps<TParams>
const ProductDetailsPage: React.FC<TProductDetailsProps> = (props) => {
useEffect(() => {
const id = props.match.params.id;
});
return <h1>product details</h1>
}

Problem when dynamically registering routes in an application with microfrontends concept

I have an Typescript + Redux (with RTK) application using the microfrontends concept. All the steps for the construction came from this tutorial: Microfrontends tutorial.
The main component is Microfrontend.tsx (omitted imports):
interface Manifest {
files: {
'main.js': string
'main.js.map': string
'index.html': string
}
entrypoints: string[]
}
const MicroFrontend = ({
name,
host,
module
}: {
name: string
host: string | undefined
module: string
}) => {
const history = useHistory()
useEffect(() => {
const renderMicroFrontend = () => {
// #ts-ignore
window[`render${name}`] && window[`render${name}`](`${name}-container`, history)
}
if (document.getElementById(name)) {
renderMicroFrontend()
return
}
const manifestUrl = `${
isDevProfile ? host : ''
}/${module}/view/asset-manifest.json`
fetch(manifestUrl)
.then(res => res.json())
.then((manifest: Manifest) => {
const script = document.createElement('script')
script.id = name
script.crossOrigin = ''
script.src = `${host}${manifest.files['main.js']}`
script.onload = () => {
renderMicroFrontend()
}
document.head.appendChild(script)
})
return () => {
// #ts-ignore
window[`unmount${name}`] && window[`unmount${name}`](`${name}-container`)
}
})
return (
<main id={`${name}-container`} style={{ height: '100%' }} />
)
}
MicroFrontend.defaultProps = {
document,
window
}
export default MicroFrontend
I'm trying to render the routes of the child components in a dynamic way, however, when I do this, I have a very strange effect: Bug.
The code snippet that generates this effect is this (omitted imports):
const App = () => {
const dispatch = useAppDispatch()
const { loadWithSuccess } = useSelector(moduleSelectors)
const avaibleModuleLinks = useSelector(avaibleModuleLinksWhitoutHome)
useEffect(() => {
dispatch(fetchAvaibleModules()).then(response =>
dispatch(fetchAvaibleModuleLinks(response.payload as string[]))
)
}, [dispatch])
return (
<BrowserRouter>
<Template>
<Switch>
<Route exact={true} path="/" component={Home} />
{loadWithSuccess ? avaibleModuleLinks?.map(
(subMenuPath: SubMenuPath | undefined, index: number) => {
const subMenuPathKey = subMenuPath ? subMenuPath.key : ''
let micro = () => (
<MicroFrontend
module={subMenuPathKey}
host="127.0.0.1"
name={subMenuPath ? subMenuPath.key.charAt(0).toUpperCase() : ''}
/>
)
return (
<Route
key={index}
path={`/dfe/view/${subMenuPathKey}`}
component={micro}
/>
)
}
): <></>}
</Switch>
</Template>
</BrowserRouter>
)
}
export default App
Only when I don't render routes dynamically do I have the desired effect: desired behavior
The code snippet that generates this effect is this (omitted imports):
const ModuleNfe = () => (
<MicroFrontend host="127.0.0.1" name="Nfe" module="nfe" />
)
const App = () => {
const dispatch = useAppDispatch()
const { loadWithSuccess } = useSelector(moduleSelectors)
const avaibleModuleLinks = useSelector(avaibleModuleLinksWhitoutHome)
useEffect(() => {
dispatch(fetchAvaibleModules()).then(response =>
dispatch(fetchAvaibleModuleLinks(response.payload as string[]))
)
}, [dispatch])
return (
<BrowserRouter>
<Template>
<Switch>
<Route exact={true} path="/" component={Home} />
<Route path="/dfe/view/nfe" component={ModuleNfe} />
</Switch>
</Template>
</BrowserRouter>
)
}
export default App
As you may have noticed, the desired behavior is for my page to be rendered inside the Template component. But for some reason, this is not the case.

How to pass data queried in one component to another component to use as a query variable?

I'm stuck trying to pass a value queried with Apollo Client in one route component to another route component to use as a variable in a query. The exact error is: "Uncaught TypeError: Cannot read property 'name' of undefined".
There are three components:
App, the root component with the router.
ComponentA, that queries a group of data by id and name to show a list of Cards for each item. Every item has a link to a to ComponentB.
Component B, which has to query more data using the name referenced by ComponentA as a variable to show more data from that item.
App.tsx
export const App: React.FunctionComponent = () => {
return (
<BrowserRouter>
<>
<Main>
<Switch>
<Route exact path="/" component={ComponentA} />
<Route path="/:name" component={ComponentB} />
</Switch>
</Main>
</>
</BrowserRouter>
);
};
ComponentA.tsx
const GET_DATAS = gql`
query GetDatas {
getDatas {
_id
name
}
}
`;
interface Data {
_id: string;
name: string;
}
export const Home: React.FunctionComponent = () => {
const { data } = useQuery(GET_DATAS);
return (
<>
<div>
{data.getDatas.map((data: Data) => (
<Link to={`/${data.name}`} key={data._id}>
<Card name={data.name} />
</Link>
))}
</div>
</>
);
};
ComponentB.tsx
const GET_DATA = gql`
query GetData($name: String!) {
getData(name: $name) {
_id
name
year
color
}
}
`;
interface Props {
name: string;
}
export const DataDetails: React.FunctionComponent<Props> = (props: Props) => {
const { data } = useQuery(GET_DATA, {
variables: { name },
});
return (
<>
<div>
<H1>{data.getData.name}</H1>
<p>{data.getData.year}</p>
<p>{data.getData.color}</p>
</div>
</>
);
};
The queries work well as I tested them in Playground, and I tried using local state and passing the props with Link with no results, but I still can't figure out how to pass the value to use in the query of ComponentB.
Thanks in advance!
Fixed 🎉 I finally opted for just taking the URL, cleaning it a bit, and using it as a variable for the query, and also adding the loading and error states:
export const DataDetails: React.FunctionComponent = () => {
const dirtyPath = location.pathname;
const cleanPath = dirtyPath.replace(/%20/g, ' ').replace(/\//g, '');
const { data, loading, error } = useQuery(GET_DATA, {
variables: { name: cleanPath },
});
return (
...
);
};
Another solution, available when using React Router, would be:
export const DataDetails: React.FunctionComponent = (props) => {
const { data, loading, error } = useQuery(GET_DATA, {
variables: { name: props.match.params.name },
});
return (
...
);
};

Resources