Pass child element as props in React - reactjs

I'm building simple site using Gatsby and styled-components. Here's my navigation component so far:
const Navigation = () => (
<NavigationContainer>
<List>
<NavItem><StyledLink to={'/converter/'}>Go to Converter</StyledLink></NavItem>
<NavItem><StyledLink to={'/about/'}>Go to About</StyledLink></NavItem>
<NavItem><StyledLink to={'/'}>Go to Main Page</StyledLink></NavItem>
</List>
</NavigationContainer>
)
I would like to use this component on every site but with different <NavItem> text and links. I don't have an idea how it could work with this component i.e. :
const About = () => (
<>
<Body>
<Navigation />
</Body>
</>
)
So on different sites i want the Navigation component to have links to different pages and text in it. Is possible to achieve it using props ?

Based on my understanding of the problem you are trying to solve, you could always use a filter function to render routes based on which route the person is currently on:
const routes = [{
to: '/converter/,
message: 'Go to Converter',
},
{
to: '/about/',
message: 'Go to About',
},
{
to: '/'
message: 'Go to Main Page',
}];
const Navigation = (currentLocation) => (
<NavigationContainer>
<List>
{routes.filter(route => {
if (route.to !== currentLocation) {
return <NavItem><StyledLink to={route.to}>{route.message}</StyledLink></NavItem>
}
})
}
</List>
</NavigationContainer>
)
In this case, you would need to determine the current location of where the user is on the site, and simply pass that property in to the navigation component.

Related

how to pass the data from one page to another in react.js and using reach router

There is some data that I want to use on the second page am routing to that page using the reach router Link is there a way we can do so?
We can do this way when using reach-router
An object to put on location state.
Page 1(Navigated from):
const NewsFeed = () => (
<div>
<Link
to="photos/123"
state={{ fromFeed: true }}
/>
</div>
)
page 2(Navigated to):
const Photo = ({ location, photoId }) => {
if (location.state.fromFeed) {
return <FromFeedPhoto id={photoId} />
} else {
return <Photo id={photoId} />
}
}
for more details use this documentation https://reach.tech/router/api/Link[][1]

How to make Link from react-router-dom work further inside the application?

I have a <HashRouter> wrapping the components which holds the <Link> components, however, I still get the error Error: Invariant failed: You should not use <Link> outside a <Router>. Is there some hierarchy that says you have to have the Links on a higher level than I have? I have the <Link>
inside a dropdown.
Something like my app is:
<HashRouter>
<ComponentA>
<ComponentB>
<Link to="/my-path"/>
</ComponentB>
</ComponentA>
</HashRouter>
EDIT:
What I am doing is adding a <Link> inside a custom option inside a dropdown which I have gotten from a library. It says I can add custom elements into my dropdown options. The dropdown I am making is complex with buttons inside it, and I want to link to one place if you click the title. There is where the Link comes in. When I add the Link there, the error message appear, but not when I add the Link directly to the component which is using the dropdown. This is how I have the component, which is not working. This is just mocked data, the data I use are calculated inside a .map:
const CustomLink = () => <Link to="/some-path">Title of link</Link>;
const data = [{
selected_key: 'id1',
content: [<CustomLink />, <CustomButton />]
},
...
]
return <DropdownFromLibrary data={data} />;
I want/need to add the components inside a array, but the above example does not work. This below do work, to have the Link inside the same component as the Dropdown, but is not what I want, since I need to calculate new path to every option:
return (
<>
<CustomLink />
<DropdownFromLibrary data={data} />
</>
);
SOLUTION
import { useHistory } from 'react-router-dom';
...
const history = useHistory();
const CustomLink = ({ id, title }) => <div onClick={() => history.push(`/some-title/${id}`)}>{title}</div>
const data = [{
selected_key: 'id1',
content: [<CustomLink />, <CustomButton />]
},
...
]
return <DropdownFromLibrary data={data} />;
You have an error because the Link has to go inside the < componentB> . I mean inside componentB's function. Wrap ur return elements inside the Link.

How to redirect a link using History to a page

In my React app, I added useHistory to be able from my page Rovers to be redirect onClick of a button to one specific page. That button is contained inside 3 divs where onClick takes the name of the Rover and open the right page related to that Rover selected.
On that page, I would like to add 2 links which will redirect to the other 2 Rovers so the user no need to back to the Rovers page all the time.
My issue I'm getting this error:
TypeError: Cannot read property 'roverName' of undefined
The code flow is as follow:
The rover button onClick is inside my Card component which shows the rover info and where I'm using History below
export default function CardComponent({
name
// other props
}) {
const history = useHistory();
const redirectToRoverPage = () => {
return history.push({
pathname: `/mars-rovers/rover/${name}`,
state: { roverName: name },
});
};
return (
<Col md={4} className="mb-5">
<Card className="border-black mt-3">
{/* some rover content here */}
<Button variant="success" onClick={() => redirectToRoverPage()}>
ENTER
</Button>
</Card.Body>
</Card>
</Col>
);
}
Then inside my Rover page where I use 2 components
Index
export default function Rover(data) {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
Delayed.delay(() => {
setIsLoading(false);
}, 3000);
});
const roverName = `${data.location.state.roverName}`;
return isLoading ? (
<RoversLoader />
) : (
<Container>
<RoverIntro roverName={roverName} />
</Container>
);
}
RoverIntro
export default function RoverIntro({ roverName }) {
return (
<JumboTronWrapper>
<Jumbotron className="jumbotron-intro">
<div className="d-flex align-items-center">
<h1>{roverName}</h1>
<span className="ml-auto">
<Link to="/mars-rovers">Rovers</Link>
<Link to="/mars-rovers/rover/Curiosity">Curiosity</link> <-- Here clicking give me that error I showed above -->
OTHER LINKS
</span>
</div>
<RoverText roverName={roverName} />
</Jumbotron>
</JumboTronWrapper>
);
}
What I would like to understand as the first time I'm using History what should I do to permit the redirect from one Rover page to another rover page.
I want to avoid going back to mars-rovers main page all the time and allow a user to go directly to the other rover.
If you need it I can show more codes regarding this flow.
The problem is that in your Card component, you navigate using history api and you specifically set state in the push.
state: { roverName: name }
But then, in the Rover pages, you use <Link/> component without setting any state. This is why when you attempt to read data.location.state.roverName it throws.
I advice not to use the history api directly at all, but instead render a <Link/> in your Card.
And then within all your <Link/> components, specify the state inside to={object}, docs.
<Link
to={{
pathname: `/mars-rovers/rover/${name}`,
state: { roverName: name },
}}
/>
To answer your comment:
Why make life difficult? JavaScript is beautiful powerful language without limits.
export default function Links({ roverName: currentRover }) {
const rovers = {
curiosity: 'Curiosity',
opportunity: 'Opportunity',
spirit: 'Spirit',
};
return Object.keys(rovers).map(key => {
if (rovers[key] === currentRover) return null
const roverName = rovers[key]
return (
<Badge pill variant="primary" className="mr-2">
<Link
className="text-white"
to={{
pathname: `/mars-rovers/rover/${roverName}`,
state: { roverName },
}}>
{roverName}
</Link>
</Badge>
)
})
}
Anyway, you should restructure your data a bit, you make adding new items in the future difficult. You should centralize your state and avoid duplication as is the case with your rovers definition in this <Links/> component. Instead, pass it via props and calculate the names from your original data like this:
const roverData = {
curiosity: {
name: 'Curiosity',
landingDate: '',
launchDate: '',
maxDate: '',
maxSol: '',
status: '',
totalPhotos: '',
},
opportunity: {
name: 'Opportunity',
// ...
},
spirit: {
name: 'Spirit',
// ...
},
};
const roverKeys = Object.keys(roverData)
const roverNames = roverKeys.map(key => roverData[key].name)
PS: It is better to pass a key of active rover to the link and in the location.state instead of its name. Keys are unique and also it allows you to manipulate objects directly without having to traverse the items and search for a particular value.

React Navigation Global FAB

I am using react-navigation v5 and ended up with App.js like this
const MainStack = createStackNavigator();
export default () => (
<NavigationNativeContainer>
<MainStack.Navigator
screenOptions={{
header: () => <AppbarHeader />,
}}
>
<MainStack.Screen name="index" component={index} />
<MainStack.Screen name="alternate" component={alternate} />
</MainStack.Navigator>
</NavigationNativeContainer>
);
I would like to add a Floating Action(FAB) Button that would be visible on the bottom of both the index and alternate page that would allow the user to show a Modal that is currently omitted. Right now I feel like the only solution is to put my FAB component inside both the index and alternate components but this doesn't seem right because it shouldn't re-render and transition in with the page. It should float above the page transition. I feel like it should be more of a global navigator of some sort but I am not sure how that should work with the existing StackNavigator shown above.
I am Looking forward to any solutions provided.
As #satya164 noted, you can put FAB in your root component. In addition, to access to navigation actions, you can use a ref to your navigation container.
const App = () =>{
const ref = React.useRef(null);
return (
<>
<NavigationNativeContainer ref={ref}>
// ...
</NavigationNativeContainer>
<FAB onPress={() => ref.current && ref.current.navigate('A SCREEN')}/>
</>
);
If you really want it to be global, put it in your root component and not inside a screen.
const App = () => (
<>
<NavigationNativeContainer>
// ...
</NavigationNativeContainer>
<FAB />
</>
);

Keeping Material UI tabs and React Router in sync

Is there a non hacky way to keep Material UI tabs and React router in sync?
Basically, I want to change the URL when the user clicks on a tab [1] and the tabs should change automatically when the user navigates to a different page with a non-tab link or button, and of course on direct access [2] and page refresh too.
Also, it would be nice to have the react router's non exact feature too, so the /foo tab should be active both for /foo and /foo/bar/1.
[1] Other SO answers recommend using the history api directly, is that a good practice with react-router?
[2] I'm not sure what it's called, I meant when the user loads for example /foo directly instead of loading / and then navigating to /foo by a tab or link
Edit:
I created a wrapper component which does the job, but with a few problems:
class CustomTabs extends React.PureComponent {
constructor() {
super();
this.state = {
activeTab: 0
}
}
setActiveTab(id) {
this.setState({
activeTab: id
});
return null;
}
render() {
return (
<div>
{this.props.children.map((tab,index) => {
return (
<Route
key={index}
path={tab.props.path||"/"}
exact={tab.props.exact||false}
render={() => this.setActiveTab(index)}
/>
);
})}
<Tabs
style={{height: '64px'}}
contentContainerStyle={{height: '100%'}}
tabItemContainerStyle={{height: '100%'}}
value={this.state.activeTab}
>
{this.props.children.map((tab,index) => {
return (
<Tab
key={index}
value={index}
label={tab.props.label||""}
style={{paddingLeft: '10px', paddingRight: '10px', height: '64px'}}
onActive={() => {
this.props.history.push(tab.props.path||"/")
}}
/>
);
})}
</Tabs>
</div>
);
}
}
And I'm using it like this:
<AppBar title="Title" showMenuIconButton={false}>
<CustomTabs history={this.props.history}>
<Tab label="Home" path="/" exact/>
<Tab label="Foo" path="/foo"/>
<Tab label="Bar" path="/bar"/>
</CustomTabs>
</AppBar>
But:
I get this warning in my console:
Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
I think it's because I set the state immediately after render() is called - because of Route.render, but I have no idea how to solve this.
The tab changing animations are lost: http://www.material-ui.com/#/components/tabs
Edit #2
I finally solved everything, but in a bit hacky way.
class CustomTabsImpl extends PureComponent {
constructor() {
super();
this.state = {
activeTab: 0
}
}
componentWillMount() {
this.state.activeTab = this.pathToTab(); // eslint-disable-line react/no-direct-mutation-state
}
componentWillUpdate() {
setTimeout(() => {
let newTab = this.pathToTab();
this.setState({
activeTab: newTab
});
}, 1);
}
pathToTab() {
let newTab = 0;
this.props.children.forEach((tab,index) => {
let match = matchPath(this.props.location.pathname, {
path: tab.props.path || "/",
exact: tab.props.exact || false
});
if(match) {
newTab = index;
}
});
return newTab;
}
changeHandler(id, event, tab) {
this.props.history.push(tab.props['data-path'] || "/");
this.setState({
activeTab: id
});
}
render() {
return (
<div>
<Tabs
style={{height: '64px'}}
contentContainerStyle={{height: '100%'}}
tabItemContainerStyle={{height: '100%'}}
onChange={(id,event,tab) => this.changeHandler(id,event,tab)}
value={this.state.activeTab}
>
{this.props.children.map((tab,index) => {
return (
<Tab
key={index}
value={index}
label={tab.props.label||""}
data-path={tab.props.path||"/"}
style={{height: '64px', width: '100px'}}
/>
);
})}
</Tabs>
</div>
);
}
}
const CustomTabs = withRouter(CustomTabsImpl);
Firstly, thanks for replying to your very question.
I have approached this question differently, I decided to post here for the community appreciation.
My reasoning here was: "It would be simpler if I could tell the Tab instead the Tabs component about which one is active."
Accomplishing that is quite trivial, one can do that by setting a known fixed value to the Tabs component and assign that very value to whatever tab is supposed to be active.
This solution requires that the component hosting the tabs has access to the props such as location and match from react-router as follows
Firstly, we create a function that factory that removes bloated code from the render method. Here were are setting the fixed Tabs value to the Tab if the desired route matches, other wise I'm just throwing an arbitrary constant such as Infinity.
const mountTabValueFactory = (location, tabId) => (route) => !!matchPath(location.pathname, { path: route, exact: true }) ? tabId : Infinity;
After that, all you need is to plug the info to your render function.
render() {
const {location, match} = this.props;
const tabId = 'myTabId';
const getTabValue = mountTabValueFactory(location, tabId);
return (
<Tabs value={tabId}>
<Tab
value={getTabValue('/route/:id')}
label="tab1"
onClick={() => history.push(`${match.url}`)}/>
<Tab
value={getTabValue('/route/:id/sub-route')}
label="tab2"
onClick={() => history.push(`${match.url}/sub-route`)}
/>
</Tabs>
)
}
You can use react routers NavLink component
import { NavLink } from 'react-router-dom';
<NavLink
activeClassName="active"
to="/foo"
>Tab 1</NavLink>
When /foo is the route then the active class will be added to this link. NavLink also has an isActive prop that can be passed a function to further customize the functionality which determines whether or not the link is active.
https://reacttraining.com/react-router/web/api/NavLink

Resources