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
Related
Here is my problem is that I need to change the value of the useState in parent component, from the child component(s). but the child components are Routes, so I passed them through like this;
const NurseInterface = () => {
const [wristbandID, setWristbandID] = useState(1);
...
<Switch>
<Route
exact path={`${path}/default-view`}
render={(props)=> (
<NurseDefaultView {...props} setWristbandID={setWristbandID}/>// Alternative way of Routing Components, allowing you to pass props to it
)}
/>
...
</Switch>
...
}
inside the Route is the NurseDefaultView which passes the state to Wristbands:
const NurseDefaultView = ({ setWristbandID }) => {
...
{wristbands ? <Wristbands wristbands={wristbands} setWristbandID={setWristbandID}/> : null}
}
and then Wristbands passes the state down to wristband cards through map()
//React-Spring
const transitions = useTransition(wristbands, wristband => wristband.id, {
from: { opacity: 0, transform:"translate(0px, -50px)" },
enter: { opacity: 1, transform:"translate(0px, 0px)" },
leave: { opacity: 0, transform:"translate(0px, -50px)" },
})
if (wristbands) {
return (
<>
{transitions.map(({item, key, props})=> (
item ? (
<WristbandCard key={parseInt(item.id)}
props={props}
nurseView={true}
setWristbandID={setWristbandID}
wristband={item}/> ) : null
))}
</>
)} else return <div>No Wristbands</div>
and WristbandCard looks this this:
const WristbandCard = ({ wristband, setWristbandID, setShowDetail, nurseView, props }) => {
...
const handleClick = () => {
if (nurseView) {
console.log(wristband.id);
setWristbandID(wristband.id);
} else {
console.log("SetShowDetail")
setShowDetail(true);
setWristbandID(wristband.id);
}
}
...
if (nurseView) return (
<Link className="right" to={`patient-cart`}>
<animated.article style={props} onClick={handleClick} className="card">
<header>
{battery(data)}
<img alt="Wifi Icon by Google Inc (placeholder)" src={Connection}/>
</header>
<h1>{wristband.name}</h1>
<div><img alt="transfer" src={Arrow}/><h3>{description(wristband, nurseView)}</h3></div>
<footer><img alt="transfer" src={Transfer}/></footer>
</animated.article>
</Link>
); else return (
<animated.figure style={props} onClick={handleClick} className={wristband.connected ? "card": "card red"}>
<header>
{battery(data)}
<img alt="Wifi Icon by Google Inc (placeholder)" src={Wifi}/>
</header>
<h3>{description(wristband)}</h3>
<h2>{wristband.name}</h2>
</animated.figure>
)
}
but it doesnt work. And ive console logged the value of it set at the top level inside the Route, and the value. But in the parent, it doesn't work.
Does anyone have any examples of this working in react-router? I havent seen any sadly.
This makes me think that maybe Routes cant handle Hooks as props and I need to implement Redux for this, but i'm hoping that there is maybe somewhere I went wrong? because this is the only case where I need something like this.
I would be grateful for any help. Thanks.
You can use context with a context hook to update a parent's state (wristband id) in a nested child component (setWristbandId).
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.
I am using redux to track which screen user is at.
I have a button tab component:
Tab.js:
class Tab extends Component {
render() {
return (
<Button onPress={() => navigation.navigate(route)}>
<Icon
style={selected ? deviceStyle.tabSelectedColor : deviceStyle.tabDefaultColor}
type="Ionicons"
name={icon}
/>
<Text style={selected ? deviceStyle.tabSelectedColor : deviceStyle.tabDefaultColor}>
{title}
</Text>
</Button>
);
}
}
Then I call it from another component:
const items = [
{
screen: 'home',
title: 'home',
icon: 'apps',
route: 'Home'
}]
renderTab = () => {
return items.map((tabBarItem, index) => {
return (
<Tab
key={index}
title={tabBarItem.title}
icon={tabBarItem.icon}
route={tabBarItem.route}
/>
);
});
};
Now, when user change screen, how can I change the selected props in tab component?
There are two ways to achieve this.
1) Onclick / onpress of your Tab you can dispatch an action, which will update user's current screen, as currently, you are navigating user on that page.
2) On load of screen, which is going to load after tab click/press.
You just need an action which will update user's current screen
I am trying to create a search bar built on the top of Semantic UI React like this:
resource: standard search
I am currently using create-react-app with Link and Switch from react-router-dom. I have already tried similar solutions with history.push, but didn't redirect the page with my list. I could not found any example on how to redirect the page after I click on an item and outputs a div with a list in the new page containing some objects from Search result.
Here are some templates of my files structure:
###App.js###
export default function App() {
return (
<React.Fragment>
<Navbar />
<Switch>
<Route exact path="/" component={ProductList} />
<Route path ="/details" component={Details} />
<Route path="/cart" component={Cart} />
<Route path="/form" component={Form} />
<Route component={Default} />
</Switch>
...
</React.Fragment>
);
}
I need to redirect to /details page after I click on the suggestive item.
###SearchForItem.js###
class SearchForItem extends Component {
componentWillMount() {
this.resetComponent()
}
goToDetailsPageDiv(e, result) {
{console.log(result)}
return (
<Link to="/details">
<ui>
<li>{result.title}</li>
<li>{result.description}</li>
<li>{result.image}</li>
</ui>
</Link>
);
}
resetComponent = () => this.setState({ isLoading: false, results: [], value: '' })
handleResultSelect = (e, { result }) => this.goToDetailsPageDiv(e, result);
handleSearchChange = (e, { value }) => {
this.setState({ isLoading: true, value })
setTimeout(() => {
if (this.state.value.length < 1) return this.resetComponent()
const re = new RegExp(_.escapeRegExp(this.state.value), 'i')
const isMatch = result => re.test(result.title)
this.setState({
isLoading: false,
results: _.filter(source, isMatch),
})
}, 300)
}
render() {
const { isLoading, value, results } = this.state
return (
<Search
onResultSelect={this.handleResultSelect}
onSearchChange={_.debounce(this.handleSearchChange, 500, { leading: true })}
loading={isLoading}
results={results}
value={value}
//
input={{placeholder: "Search..."}}
noResultsMessage={"Nothing found"}
{...this.props}
/>
)
}
}
onResultSelect is supposed to handle the data after I click on these items.
The console.log outputs correctly all the object's properties after I clicked on the target item:
{
description: "description-template"
image: "img/products/product-template"
title: "title-template"
}
But the issue I am trying to solve is to redirect these properties to the page /details inside a div with a list with these 3 properties similar to what I tried to produce here:
goToDetailsPageDiv(e, result) {
{console.log(result)}
return (
<Link to="/details">
<ui>
<li>{result.title}</li>
<li>{result.description}</li>
<li>{result.image}</li>
</ui>
</Link>
);
}
Can someone please shed light on how can I redirect the page correctly?
Currently working on this right now as well, the only way I have come up with doing it (not without its flaws) is using this.props.history.push("{LINK TO REDIRECT HERE}") in the handleResultSelect arrow function. Will probably come back and update this answer after I have hashed out the issues but I believe this is the best way to do it.
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.