I am working on a fitness app, which means I have some nested data that i am using to dynamically generate some pages within pages within pages.
Here is the data i am using along with it's accompanying functions.
const data = [
{
title: "Routine1",
days: [
{
title: "Day1",
exercises: [
{
title: "Bench Press"
}
]
}
]
}
];
const dataMap = data.reduce(function (map, routine) {
routine.daysMap = routine.days.reduce(function (daysMap, day) {
daysMap[day.title] = day
return daysMap
}, {})
map[routine.title] = routine
return map
}, {})
exports.getAll = function () {
return data
}
exports.lookupRoutine = function (title) {
return dataMap[title]
}
exports.lookupDay = function (routine, day) {
return dataMap[routine].daysMap[day]
}
I want to use React Router to go from an index page that displays all the routines, to a page that displays all the days in a routine, to a page that displays all the exercises in that day.
Here is how I have set up my routes to do this:
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="routine/:routine" components={{ content: Routine, header: RoutineHeader }}>
<Route path=":day" components={{ content: Day, header: DayHeader }}>
</Route>
</Route>
</Route>
Let's not worry about the header components because they dont really do anything right now. But I will show how I have set up the App, Index, Routine, and Day components. HINT: they are the same...
app component
export default class App extends Component {
render() {
const { content, header } = this.props
return (
<div>
<header>
{header || <IndexHeader />}
</header>
<div>
{content || <Index />}
</div>
</div>
)
}
}
index component
export default class Index extends Component {
render() {
return (
<div class="main-container">
<h2>Routines</h2>
{data.getAll().map((routine, index) => (
<Link class="main-link" key={index} to={`/routine/${routine.title}`}>{routine.title}</Link>
))}
</div>
)
}
}
Routine component
export default class Routine extends Component {
render() {
const routine = data.lookupRoutine(this.props.params.routine);
return(
<div class="main-container">
<h2>{routine.title} Days</h2>
{routine.days.map((day, index) => (
<Link key={index} class="main-link" to={`/routine/${routine.title}/${day.title}`}>{day.title}</Link>
))}
</div>
);
}
}
Day component
export default class Day extends Component {
render() {
const { routine, day } = this.props.params;
const dayItem = data.lookupDay(routine, day);
return(
<div class="main-container">
<h2>{dayItem.title} Exercises</h2>
</div>
);
}
}
maybe you want some visuals to help out? ok well here are some visuals, notice that the 2nd picture is the same as the 1st except in the URL, because I have actually clicked on the day.
Routine1 Page
What is supposed to be the Day1 Page
Edit: to clarify, what I want the app to do, is display all the days in a routine when i click that routine, and display all the exercises in a day when i click that day. right now it only goes down one level to show all the days in the routine.
I figured out my problem.
What I was doing wrong was nesting the day components in the routine components. The App component is the one that is accepting the conent: and header: components, and since I nested days within routines, the App, which is essentially the view, was not updating because it wasn't finding the day components.
The correct routing looks like this:
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="routine/:routine" components={{ content: Routine, header: RoutineHeader }}></Route>
<Route path="routine/:routine/:day" components={{ content: Day, header: DayHeader }}></Route>
</Route>
</Router>
Related
i have a page with the following code:
// Homepage
return (
<>
<div>HELLO BE YOu</div>
{list.map((video) => {
return <VideoPreview key={video._id} video={video</VideoPreview>
});}
{list.map((video) => {
return <div>TEST</div>
})}
</>
);
VideoPreview is an imported component:
export const VideoPreview = ({video}) => {
const navigate = useNavigate();
function handleClick(){
navigate('/video');
}
return <div onClick={handleClick}>video</div>
}
When a user clicks on <VideoPreview/>, they will be directed to another page which has been defined in App.js to be
<BrowserRouter forceRefresh={true}>
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/video" element={<Videopage />} />
</Routes>
</BrowserRouter>
The bug is that when the user attempts to go back to "/" path from "/video" path, the HomePage component does not render properly.
The items inside the list map do not render. (Other element outside of list.map eg, <div>HELLO BE YOu</div> was rendered properly though). I have verified that list is not empty and when i place console.log statements within the map function, those logs gets printed out.
{list.map((video) => {
return <VideoPreview key={video._id} video={video}></VideoPreview>
});}
{list.map((video) => {
return <div>TEST</div>
})}
May i get some help in resolving this problem? Thank you.
I have a react app with two pages--- the home and trip page. The trip page is rendered with the tripID passed in through the url. My app.js looks like :
function App() {
return (<>
<ThemeProvider theme={theme}>
<Router>
<Routes>
<Route exact path='/' element={<Home />} />
<Route path='/trip/:tripId' element={<TripComponent />} />
</Routes>
</Router>
</ThemeProvider>
</>
);
}
I have a global navbar with a menu of different tripIds as many Link to navigate to the TripComponent. When i am at the "/" path, and I navigate to "/trip/tripA", i have no problems. But when i am at "/trip/tripA" , and i want to navigate to "/trip/tripB", it doesnt work. I can see the url changing accordingly, but my TripComponent doesnt rerender with tripB's data. the code for my menu in my navbar looks like:
ReactDOM.createPortal(<>
<CustomModal setOpen={setShowTripList} title={"Saved Trips"}>
<List>
{trips && trips.length > 0 &&
trips.map((trip, index) => {
return (
<Link to={`/trip/${trip._id}`} >
<ListItemButton>
<ListItemText id={trip._id} primary={trip.title} />
</ListItemButton>
</Link>
)
})
}
</List>
</CustomModal>
</>
, document.getElementById("portal"))
I am confused as to why this is happening. When i press the Link to navigate to another URL, shouldn't it unmount and remount?
When the tripId route path parameter updates the routed element TripComponent isn't remounted, it is only rerendered. If there is some logic that depends on the tripId path parameter then TripComponent needs to "listen" for changes to that value. This is the same thing that would need to happen if a prop value changed.
Use a useEffect hook with a dependency on the tripId path parameter to issue side-effects based on the current value.
Example:
import { useParams } from 'react-router-dom';
...
const TripComponent = () => {
const { tripId } = useParams();
useEffect(() => {
// initial render or tripId updated
// do something with current tripId value
}, [tripId]);
...
};
I think the #Drew's answer is perfect.
But I'd like to put something additionally.
I suggest you to use useMemo hook.
...
const trip = useMemo(() => {
// something you want
}, [tripId])
...
I'm trying to add some animations and smoothness to an app with Framer-motion and I'm struggling to make it all work.
Using react-router 6, I want to trigger some exit animations on route sub-components when the url changes. Following this tutorial, here is what I got for the main layout :
export default function MainWrapper() {
const location = useLocation();
return (
<Main>
<AnimatePresence exitBeforeEnter initial={false}>
<Routes key={location.pathname}>
<Route path="/" element={<Dashboard />} />
<Route path="project/:id/*" element={<Project />} />
</Routes>
</AnimatePresence>
</Main>
);
}
The pages Dashboard and Project are build using some composition of Antd's Row and Col system. I want to animate Row's children to appear one after the other on mount, and to disappear one after the other on unmount :
import React, { ReactNode } from "react";
import { Card, Col, Row } from "antd";
import { motion } from "framer-motion";
// Section
type SectionProps = {
title?: ReactNode;
extra?: ReactNode;
children?: ReactNode;
span?: number;
};
export default function Section({
title,
extra,
children,
span = 24
}: SectionProps) {
return (
<MotionCol span={span} variants={colVariant}>
<Card title={title} extra={extra}>
{children}
</Card>
</MotionCol>
);
}
// Section.Group
type GroupProps = {
children?: ReactNode;
};
Section.Group = function({ children }: GroupProps) {
return (
<MotionRow
gutter={[24, 24]}
variants={rowVariant}
initial="hidden"
animate="show"
exit="close"
>
{children}
</MotionRow>
);
};
// Framer stuff
const MotionRow = motion(Row);
const MotionCol = motion(Col);
const transition = { duration: 0.4, ease: [0.43, 0.13, 0.23, 0.96] };
const rowVariant = {
hidden: {},
show: {
transition: {
staggerChildren: 0.1
}
},
close: {}
};
const colVariant = {
hidden: { opacity: 0, x: 20, transition },
show: { opacity: 1, x: 0, transition },
close: {
opacity: 0,
x: -20,
transition
}
};
Dashboard is then built using these blocks :
<Section.Group>
<Section>
First section...
</Section>
<Section>
Second section...
</Section>
</Section.Group>
The issue : Only hidden and show work. Not close. There is no exit-animation when leaving a page. How could I solve this ? Thank you.
Things I found wrong:
For custom components the motion function requires you to forward the ref
Docs: https://www.framer.com/api/motion/component/#custom-components
const ForwardedAntdRow = React.forwardRef((props, ref) => (
<Row ref={ref} {...props} />
));
const MotionRow = motion(ForwardedAntdRow);
not
const MotionRow = motion(Row);
Route doesn't have an element prop
<Route path="/page1">
<Page1 />
</Route>
is pretty standard notation as far as I know (I don't work with react-router often)
I created a working example here: https://codesandbox.io/s/framer-motion-animate-react-router-transition-kczeg?file=/src/index.js:1308-1372
I can answer any other questions you have when I am online tomorrow. Let me know
I can see two potential problems with your code:
1.
Note: Child motion components must each have a unique key prop so
AnimatePresence can track their presence in the tree.
Note: The custom component being removed from the DOM must still be a
direct descendant of AnimatePresence for the exit animation(s) it
contains to trigger.
source: https://www.framer.com/api/motion/animate-presence/
So your code would become:
export default function MainWrapper() {
const location = useLocation();
return (
<Main>
<Routes key={location.pathname}>
<AnimatePresence exitBeforeEnter initial={false}>
<Route key="dashboard" path="/" element={<Dashboard />} />
<Route key="project" path="project/:id/*" element={<Project />} />
</AnimatePresence>
</Routes>
</Main>
);
}
I am experiencing the following problem:
I have two screens in my application, one if the user has access and one if not.
If the user has access to the system, he will be redirected to the screen A, a private route that has internal states, when the private routes change, the internal state of that screen A should continue until he changes to a non-private or unknown route.
The point is, I have a private routes vector, but when I loop these routes and add a key to each Router component, on each change of route, it will unmount and mount component A (Code sample here), so I lose the internal state of A, and if I add the key to the child component of A, the internal state remains as I would like (Code sample here), however I break the child key rule of react.
Warning: Each child in a list rule should have a unique" key "prop.
Any help would be amazing! :)
#Edit: the code snippet of first sandbox. The difference between the first one and the second is the key prop, instead it be inside Route, it is within the component.
#Edit 2:
I've fixed it cdeclaring all routes statically and letting the access policy come dinamically. Ty for help!
If anyone find a better solution, It'll be wellcome! :)
{ canAccess: true, path: "/home", component: () => <div>Home</div> },
{ canAccess: true, path: "/foo", component: () => <div>Foo</div> },
{ canAccess: false, path: "/blah", component: () => <div>Blah</div> }
];
const Homepage = () => {
return (
<div>
<Link to="/home">Home</Link>
<br />
<Link to="/foo">Foo</Link>
<br />
<Link to="/blah">Blah</Link>
</div>
);
};
const Main = ({ children }) => {
const [innerState, setInnerState] = useState(112);
return (
<div>
{children}
{JSON.stringify(innerState)}
<br />
<button onClick={() => setInnerState(innerState + 1)}>AddNumber</button>
<Homepage />
</div>
);
};
const PrivateRoute = ({ component: Component, path, canAccess, index }) => (
<Route
key={index}
path={path}
render={() =>
canAccess ? (
<Main>
<Component />
</Main>
) : (
<div>Not found :(</div>
)
}
/>
);
function App() {
return (
<div className="App">
<BrowserRouter>
<Switch>
{defaultRoutes.map((route, index) => {
return PrivateRoute({ index, ...route });
})}
<Route path="/" exact component={() => <Homepage />} />
<Route component={() => <div>Not found :(</div>} />
</Switch>
</BrowserRouter>
</div>
);
}
I've fixed it cdeclaring all routes statically and letting the access policy come dinamically. Ty for help!
If anyone find a better solution, It'll be wellcome! :)
I have a react router implemented as such:
ReactDOM.render((
<Router history={createBrowserHistory()}>
<Route path="/" component={App}>
<IndexRoute component={IndexPage} />
<Route name="listView" path="things" component={ThingsList}>
<Route name="expandedView" path=":_id" component={ThingExpanded} />
</Route>
</Route>
</Router>
), document.getElementById("body"));
I'm rendering the list, and registering the route with no problem. with below component.
let ThingsList = React.createClass({
render() {
return (
<div>
<ul>
{things.map((thing) => {
return (
<li key={things._id}>
<Link to={`/things/${thing._id}`}>
<span>{thing.firstName}</span>
</Link>
</li>
)
}
</ul>
</div>
)
}
})
But how can I show additional data, when one clicks the link and registers the route for expanded view of a particular item? I need to read the router info, and accordingly render the additional info. I mean it should not be based on onClick, based on the route. Something like this?:
componentDidMount () {
let rName = Router.getName;
if (rName == "expandedView") {
let rSlug = Router.name("expandedView").slug("_id");
this.setState({expand: true, which: rSlug});
}
}
render() {
let extra;
if (this.state.expand) {
extra = <ThingExpanded />
}
}
and then in the List component:
render() {
return (
...
<Link to={`/things/${thing._id}`}>
<span>{thing.name}</span>
{extra}
</Link>
...)
}
Note: This answer is very close to what I need, but didn't work. :/
Conditional handler based on slug in React Router?