Automatic this.props.children in nested components - reactjs

I'm going through the react-router-tutorial and am on lesson 5 and have a question.
The lesson talks about defining a NavLink component that wraps the Link component, and gives it an activeClassName attribute, which is used as follows:
<NavLink to="/about">About</NavLink>
In the lesson, they define the NavLink component as follows:
// modules/NavLink.js
import React from 'react'
import { Link } from 'react-router'
export default React.createClass({
render() {
return <Link {...this.props} activeClassName="active"/>
}
})
What confuses me is the use of the self closing Link component. No where in the definition of NavLink does it say to put the this.props.children inside of the Link component. I tried it out explicitly as follows:
export default class extends React.Component {
render() {
return (
<Link {...this.props} activeClassName="active">{this.props.children}</Link>
)
}
}
and that also works as expected. My question is why? What allows the self closing Link component in their definition to automatically take the this.props.children of the NavLink and put it inside the Link component?

thats due to the spread attribute on the Link component {...this.props}. This allows all the properties of this.props that includes this.props.children to be passed into the Link component. Here is a reference of that.

Related

React couldn't pass a data from one class component into another using Link

I am trying the pass data to a new page by using Link, in doing so I used the following code.
<Link
className="option"
to={{
pathname: this.state.pathname,
state: id
}}
>
<span className="color-primary"> <button style={{ color: "white" }}
className="transaction-button"><i className="material-icons" style={{ fontSize: "18px" }}>sync_alt</i> Transaction</button>
</span>
</Link>
In the page routed, I tried to handle the data by the following code.
console.log(this.props)
The output is an empty object.
{}
Both pages are class component
I assume you are using react-router.
In the first page, where you use <Link>...</Link> you're doing the right thing.
At this point there are two alternatives: you can use function or class to create the component.
IF YOU USE A FUNCTION
In the second page, to take the data you passed, you have to import useLocation:
import { useLocation } from 'react-router';
And then, inside the function, you have to call it and extract the state from it:
const location = useLocation();
console.log(location.state);
Inside location.state you have the state you passed from the previous page.
IF YOU USE A CLASS
In this case, things are little more complicated, but you can use withRouter in order to inject location inside your component props.
So, first of all you need to import PropsTypes and withRouter:
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
Then you have to write your class like this:
class Child extends React.Component {
static propTypes = {
location: PropTypes.object.isRequired,
};
render() {
const { location } = this.props;
console.log(location.state);
return {
<div> ... </div>
};
}
}
export withRouter(Child);
In this way inside location.state you have the state you passed from the previous page.
If you are using a class, there is no withRouter any more.
What happened to withRouter? I need it!
This question usually stems from the fact that you're using React class components, which don't support hooks. In React Router v6, we fully embraced hooks and use them to share all the router's internal state. But that doesn't mean you can't use the router. Assuming you can actually use hooks (you're on React 16.8+), you just need a wrapper.
So, you'll need to create your own wrapper, as shown in the docs.
Here is my implementation, a little more easy-to-use than doc example:
import React from 'react';
import {useLocation, useNavigate, useParams} from 'react-router-dom';
function withRouter(Component) {
function ComponentWithRouterProp(props) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return <Component {...props} {...{location, navigate, params}} />;
}
return ComponentWithRouterProp;
}
export default withRouter;
Use (when exporting your component):
export default withRouter(Link);
More Use-cases Example -> for other people that came here:
Example of loading batch of components wrapped with withRouter, or just your Link component.
const routingList = [{title: 'Home', search: '/', component: Home, icon: 'fa-home'},{...}]
<Routes>
{
routingList.map((routing) => {
let Child = routing.component;
return <Route key={routing.search} path={routing.search} element={<Child {...routing.compProps} />} />;
})
}
<Route path="/link" element={<Link />} />
</Routes>
And, in your component, you can use:
this.props.location.pathname
this.props.params.paramName

Sending a React.FunctionComponent<React.SVGProps<SVGSVGElement>> as a prop to another component

I'm attempting to import a React functionComponent from an SVG and then send that to another component as a prop to render that svg. With the setup below, this compiles fine, but eventually crashes when trying to render the svg in browser with:
Error: Objects are not valid as a React child (found: object with keys {$$typeof, render}). If you meant to render a collection of children, use an array instead.
Classes below are simplified. But the gist of what I'm trying to do is:
In overlay.tsx:
import { ReactComponent as icon } from "/icon.svg";
import CustomItem from "/customItem";
const Overlay: React.FC<OverlayProps> = () => {
return (
<div>
<CustomItem icon={icon}/>
</div>
);
export default Overlay;
}
and in customItem.tsx:
import React from "react";
export interface CustomItemProps {
icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
}
const CustomItem: React.FC<CustomItemProps> = ({icon}) => {
return (
<div>
{icon}
</div>
);
};
export default ApplicationsDropdownItem;
I assume my problem is somewhere around the syntax of {icon}, but I can not for the life of me find out what I'm suppose to use instead.
Answer
The icon you are importing is a component, therefore it must be called to render the JSX.
<Icon {...props}/> (correct) or {Icon(props)} (not recomended)
Since it is a component, you should also name it Icon and not icon.
Take a look at this blog post that explains SVGR.
TL;DR - Best approach for rendering components
A. Call the component in your render method with component syntax <MyComponent/> not MyComponent().
B. Instantiate your component as a variable, and pass that to your render method's JSX block.
More info
#DustInCompetent brought to light the issue of calling a component as a function inside a JSX block.
As explained here and here, that will lead to react not registering a components hooks and lead to state and other problems.
If you are implementing a High Level Component (HOC), then you should not call a component within the render method (return statement in functional components), as this leads to problems for similar registration issues of the component.
import React from "react";
import { ReactComponent as SampleIcon } from "/sample_icon.svg";
export interface CustomItemProps {
Icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
}
const CustomItem: React.FC<CustomItemProps> = (props) => {
const Temp = props.Icon as React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
return (
<div>
<Temp/>
</div>
);
};
<CustomItem Icon={SampleIcon}/>
I think you should use <Icon /> instead of {icon} because it's a component.

how can I dynamically attach sidebar components to multiple instances of sidebar?

I'm new to React and building out a design a ran into a problem.
I have a component called SideBar. I am using this component two times, one on each side of the page.
The problem is that I would like to add different components to each instance of the SideBar component. These would be lists of various items and etc. I assumed I could next component tags but the sidebar component doesn't output.
import React, { Component } from "react";
import SideBar from "./WorkspaceComponents/SideBar";
import ScrollerBox from "./WorkspaceComponents/SideBarComponents/ScrollerBox";
class Workspace extends Component {
render() {
return (
<main className="reely-workspace">
<SideBar position="SideBarLeft">
<ScrollerBox />
</SideBar>
<SideBar position="SideBarRight" />
</main>
);
}
}
export default Workspace;
Your sidebar component should receive a children prop and render it out.
Something like this:
class Sidebar extends Component {
render() {
const {children} = this.props;
return (
<div className="sidebar">
<h1>Sidebar</h1>
{children}
</div>
)
}
}
Check out this post on react docs to understand how to compose react components: https://reactjs.org/docs/composition-vs-inheritance.html
You can make your SideBar Component a wrapper component which wraps around the content given in it.
Making SideBar Component a Wrapper Component :
class Sidebar extends Component {
render() {
return (
<div className="sidebar">
// You can add any custom element here //
{this.props.children}
</div>
)
}
}
All your element passed inside the SideBar Component will now be rendered as a part of SideBar along with what it contains.
Way to consume the wrapper component:
<SideBar>
<Content1></Content1>
<Content2></Content2>
<Content3></Content3>
</SideBar>

Can't navigate to other component

i've got 2 components.
They are each on their own path.
I have a <Link> component in the first component, and when i click that, the second component is supposed to be rendered.
The url after the #, changes fine, but nothing happens in the ui.
This is the main component:
export default class IntegrationApp extends React.PureComponent {
render() {
return (
<>
<Route path="/details/:encryptedId/:integrationType/" component={DetailsOverview} />
<Route exact path="/:integrationSource?/" component={IntegrationsOverview} />
</>
);
}
}
ReactDOM.render(
<HashRouter>
<IntegrationApp />
</HashRouter>
, document.getElementById('integrationsOverviewContainer'));
The <Link> is a custom component.
It looks like this:
export default class LinkForRouting extends React.PureComponent {
render() {
let cssClass = this.props.isButton ? ' btn ' : '';
cssClass += this.props.isPrimary ? ' btn-primary ' : '';
cssClass += this.props.customCssClass ? this.props.customCssClass : '';
const clickEvent = this.props.handleClick ? () => this.props.handleClick() : ()=> function () { return null };
return (
<Link to={this.props.path} replace={true} className={cssClass} onClick={clickEvent}>
{this.props.children}
</Link>
);
}
}
The <Link> component is used inside the <IntegrationsOverview> component, like this:
<LinkForRouting path={`/details/${integration.encryptedId}/${integration.integrationType}`} isButton={false} isPrimary={false} > Vis </LinkForRouting>
If i click the link, and then hit F5 afterwards, then the <DetailsOverview> component renders fine, but if i just click the link, then the URL just changes, and nothing happens.
Any idea of how to trigger the <DetailsOverview> component, to trigger when i click on the <Link> component?
By reading the code posted above all of your components are extending PureComponent.
As React docs says:
React.PureComponent’s shouldComponentUpdate() only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.Furthermore, React.PureComponent’s shouldComponentUpdate() skips prop updates for the whole component subtree. Make sure all the children components are also “pure”.
In your case:
export default class IntegrationApp extends React.PureComponent {
render() {
return (
<>
<Route path="/details/:encryptedId/:integrationType/" component={DetailsOverview} />
<Route exact path="/:integrationSource?/" component={IntegrationsOverview} />
</>
);
}
}
The IntegrationApp Component it is not expecting simple props and state also you should use a Switch instead of Fragment to wrap your Routes, then it becomes:
import React from "react";
import {
Switch,
Route
} from "react-router-dom";
export default class IntegrationApp extends React.Component {
render() {
return ( <
Switch >
<
Route path = "/details/:encryptedId/:integrationType/"
component = {
DetailsOverview
}
/> <
Route exact path = "/:integrationSource?/"
component = {
IntegrationsOverview
}
/> <
/Switch>
);
}
}
I wrote a simple example on: https://codesandbox.io/s/5vk79jlrn4
Hope it can help you.
The problem has been solved!
My react solution, was part of a much larger, and older solution.
This solution also had some references to angular, solution wide.
I made those references, specific to the views that need them, and then routing in react started working!
Lesson learned, check included javascript.

Pattern for react components that require specific contained components

What's the idiomatic React way to write a component that nests specific child components? I know how to write a component that simply wraps props.children, like this example from React's own docs:
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
But what if I wanted to create a pair of components that can be used like this:
<TabSet>
<Tab name="Mammals">
content for <b>warm blooded</b> creatures here
</Tab>
<Tab name="Fish">
content for <b>cold blooded</b> creatures here
</Tab>
</TabSet>
Here's my initial implementation of TabSet (using reactstrap), simplified to remove styling, selected-tab management, and other stuff not related to this question.
import React, {Fragment, Component} from 'react';
import { TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
export default class TabSet extends Component {
render(props) {
return (
<Fragment>
<Nav tabs>
{props.children.map((tab,i) =>
<NavItem key={i}>
<NavLink>
{ tab.name }
</NavLink>
</NavItem>
)}
</Nav>
<TabContent>
{props.children.map((tab,i) =>
<TabPane key={i} tabId={i}>
{ tab.children }
</TabPane>
)}
</TabContent>
</Fragment>
);
}
}
Where I'm stuck is how to implement the Tab component. Logically, it's part of the TabSet component-- it should never stand alone. And its implementation should be painfully simple because it doesn't actually do anything-- it's just a container for a name attribute and child elements.
So, here's a few questions:
Should I create a separate JS file for the Tab component, or is it so simple that I should just export it as part of the implementation of the TabSet component? If the latter, how?
In classes that use TabSet, will I need two import statements, or is there a way I can import both TabSet and Tab with one import, kinda like import React, {Fragment} from 'react' works ? If the latter, then how would the export statement look in TabSet.js?
Apologies for what's probably an obvious question-- I'm a newbie to both React and ES6.
If the component is only used in context of another component it is logical to put them both in same module and many libraries do that. The way to achieve this is use multiple export statements without default. You are allowed to use one export default and as many export statements as you need. Like this
export default class TabSet
...
export class Tab
and to import
import TabSet, {Tab} from './Tab'
The general syntax being
import defaultExport, { namedExport1, namedExport2 } from "module"
The syntax might seem a bit confusing here is the reference
Where you are using export default class ... you can actually export your own object here. With that in mind, you are able to something like:
const TabSet = props => (
<Your Tabset markup here>
)
const Tab = props => (
<Your Tab markup here>
)
export {
Tabset,
Tab
}
Doing it like this will allow you to import both components in the one line by doing:
import { Tabset, Tab } from 'wherever'
Now while this is one way to do it, and although you think Tab is quite simple, I still believe they belong in their own files. So just create the two class files for Tabset and Tab, but then make a third file called tabs.js or something. It should contain the link to both, like:
import Tabset from './tabset'
import Tab from './tab'
export {
Tabset,
Tab
}
This way you have designated files for each component, and you can import them as a single import.
Also for bonus, if you use the PropTypes ability of react, you can restrict the children of the Tabset to actually be your Tabs. Here is the overview, but as an example you can do something like:
// untested
static propTypes = {
children: PropTypes.arrayOf(PropTypes.instanceOf(Tab))
}
You will have to import the Tab component into the set component to do this.

Resources