React Router 6.4 nested deferred showing only parent loading indicator - reactjs

I'm trying to understand how suspense / await work with nested routes.
The scenario I have:
I have a collection page that shows a list of items.
I have a detail page that shows the details of a single item (this page is a nested route)
What I want to achieve:
When the collection page is loaded, the page shows a waiting indicator while the data is being fetched
(I have added a random delay to simulate this).
Once the data is loaded, if I click in one of the items it will navigate to the nested detail page
(this page is also wrapped in a suspense / await), so we get on the left side the list and the right side
the detail page for the selected character, ... here I'm showing a loading indicator.
The expected behavior:
Collection loading indicator is shown when the collection page is loaded.
Detail loading indicator is shown when the detail page is loaded.
The actual behavior:
Collection loading indicator is shown when the collection page is loaded.
Detail loading indicator is shown when the detail page is loaded, plus list page is reloaded.
How I have defined the routes:
routes.tsx
import React from "react";
import { defer, createBrowserRouter } from "react-router-dom";
import { ListPage, DetailPage } from "./pages";
import { getCharacter, getCharacterCollection } from "./api";
export const applicationRoutes = createBrowserRouter([
{
path: "/",
loader: () => defer({ characterCollection: getCharacterCollection("") }),
element: <ListPage />,
children: [
{
path: ":id",
loader: ({ params }) =>
defer({ character: getCharacter(params.id as string) }),
element: <DetailPage />
}
]
}
]);
How I have implemented each page:
list.tsx
import React from "react";
import { Await, Link, Outlet, useLoaderData } from "react-router-dom";
import { Character } from "../api/model";
export const ListPage = () => {
const characters = useLoaderData() as { characterCollection: Character[] };
return (
<div>
<h2>Character Collection</h2>
<React.Suspense
fallback={<h4 style={{ color: "green" }}>👬 Loading characters...</h4>}
>
<Await resolve={characters.characterCollection}>
{(characters) => (
<div style={{ display: "flex", flexDirection: "row" }}>
<div>
<ul>
{characters.map((character) => (
<li key={character.id}>
<Link to={`./${character.id}`}>{character.name}</Link>
</li>
))}
</ul>
</div>
<Outlet />
</div>
)}
</Await>
</React.Suspense>
</div>
);
};
detail.tsx
import React from "react";
import { Await, useLoaderData } from "react-router-dom";
import { Character } from "../api/model";
export const DetailPage: React.FunctionComponent = () => {
const data = useLoaderData() as { character: Promise<Character> };
return (
<div>
<h1>Detail</h1>
<React.Suspense
fallback={<h4 style={{ color: "blue" }}> 👩🏻 Loading character...</h4>}
></React.Suspense>
<Await resolve={data.character}>
{(character) => (
<div>
<img src={character.image} alt={character.name} />
<h2>{character.name}</h2>
<p>{character.status}</p>
</div>
)}
</Await>
</div>
);
};
Codesandbox example: https://codesandbox.io/s/vigilant-agnesi-z7tvk1
It seems like the way that I'm using to navigate just forces a full reload:
<Link to={`./${character.id}`}>{character.name}</Link>
Instead of updating only loading the nested page, is this behavior by default? or am I doing something wrong?

Related

How to make a "go back" button change state in home screen

So ive got a list of restaurant names (say fetched from an api). When I click on a restaurant name, I want it to link to a profile page for that specific restaurant, and this would set the text as "selected". And when I click "Go back" on that profile page to return to the home page, I want the that restaurant name to say "not selected".
So, if I click on the restaurant name, then in the profile page go back to the home page, the restaurant will show "unselected" since it was selected in the home page, then unselected in the profile page. However, if I click on the restaurant name, then instead of going back to the home page by clicking the "go back", I type in the url of the home page, it will show "selected".
I'm struggling with making it so when I click "Go back", the home page shows the restaurant name as having "unselected".
https://codesandbox.io/s/serene-williams-2snv1c?file=/src/App.js
(I would also appreciate if I could get the name of this sort of concept so I can look it up myself)
If I'm understanding the question correctly, you want to set some "selected" state, and only clear it if the link from the detail page is clicked.
You can create a React Context to hold and provide out the clickedRestaurants state and updater functions.
The idea here is to use the selectRestaurant handler when navigating "forward" to the details page, and use the deselectRestaurant handler only when the link from the details page back to the home page is clicked. If a user navigates to the home page using any other method, the restaurant won't be de-selected.
The localStorage API is used to persist state changes and initialize the state. The resolves persisting the selected restaurants state when the page is reloaded or a user directly mutates the URL in the address bar, i.e. like manually navigating back to "/".
RestaurantProvider
import { createContext, useContext, useEffect, useState } from "react";
export const RestaurantContext = createContext();
export const useRestaurantContext = () => useContext(RestaurantContext);
const RestaurantProvider = ({ children }) => {
const [clickedRestaurants, setClickedRestaurants] = useState(() => {
return JSON.parse(localStorage.getItem("clickedRestaurants")) ?? {};
});
useEffect(() => {
localStorage.setItem(
"clickedRestaurants",
JSON.stringify(clickedRestaurants)
);
}, [clickedRestaurants]);
const setRestaurantState = (id, selected) => {
setClickedRestaurants((ids) => ({
...ids,
[id]: selected
}));
};
const selectRestaurant = (id) => setRestaurantState(id, true);
const deselectRestaurant = (id) => setRestaurantState(id, false);
return (
<RestaurantContext.Provider
value={{ clickedRestaurants, selectRestaurant, deselectRestaurant }}
>
{children}
</RestaurantContext.Provider>
);
};
export default RestaurantProvider;
index.js - Import and wrap the application components with the RestaurantProvider component created above.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import RestaurantProvider from "./RestaurantProvider";
import App from "./App";
import Details from "./details";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<RestaurantProvider>
<App />
<Details />
</RestaurantProvider>
</StrictMode>
);
App - Import and use the useRestaurantContext hook to access the state and updater functions.
import "./styles.css";
import { Link, Route } from "wouter";
import data from "./data";
import { useRestaurantContext } from "./RestaurantProvider";
export default function App() {
const { clickedRestaurants, selectRestaurant } = useRestaurantContext();
return (
<Route path="/">
<div className="App">
{data.map((restaurant) => {
return (
<Button
key={restaurant}
restaurant={restaurant}
hasBeenClicked={clickedRestaurants[restaurant]}
setClicked={() => selectRestaurant(restaurant)}
/>
);
})}
</div>
</Route>
);
}
function Button({ restaurant, hasBeenClicked, setClicked }) {
return (
<>
<Link href={`/restaurant/${restaurant}`} onClick={setClicked}>
<button>{restaurant}</button>
</Link>
<p>
{restaurant} has {hasBeenClicked ? "" : "not "}been selected
</p>
</>
);
}
Details
import "./styles.css";
import { Link, Route } from "wouter";
import { useRestaurantContext } from "./RestaurantProvider";
export default function Details() {
const { deselectRestaurant } = useRestaurantContext();
return (
<div className="App">
<Route path="/restaurant/:name">
{(params) => {
const restaurant = decodeURI(params.name);
return (
<Link href="/" onClick={() => deselectRestaurant(restaurant)}>
{restaurant} Go back and unselect
</Link>
);
}}
</Route>
</div>
);
}
You'll need to define some sort of state if you want to be able to tell what has been clicked by the user and what hasn't. Here is one way to do it:
App.js
import "./styles.css";
import data from "./data";
import { Link, Route } from "wouter";
import { useState } from "react";
export default function App() {
const [clickedRestaurants, setClickedRestaurants] = useState([])
return (
<Route path="/">
<div className="App">
{data.map((restaurant) => {
return (
<Button
restaurant={restaurant}
hasBeenClicked={clickedRestaurants.includes(restaurant)}
setClicked={() => {
if (!clickedRestaurants.includes(restaurant)) {
setClickedRestaurants([...clickedRestaurants, restaurant])
}
}}
/>
);
})}
</div>
</Route>
);
}
function Button({ restaurant, hasBeenClicked, setClicked }) {
return (
<>
<Link href={`/restaurant/${restaurant}`} onClick={setClicked}>
<button>{restaurant}</button>
</Link>
<p>{restaurant} has {hasBeenClicked ? "" : "not "}been clicked</p>
</>
);
}

Do not understand why duplicate data is being copied into my url in react component

In my App.js (or main component) I am rendering my Navbar component
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Navbar from './components/layout/navbar/Navbar';
import './App.css';
const App = () => {
return (
<Router>
<Navbar />
</Router>
);
};
export default App;
In my Navbar I am rendering my NavLinks component and passing in as props the menu
import React from 'react';
import NavLinks from './NavLinks';
const menu = [
{ id: 1, label: 'Home', url: 'https://www.google.com/?client=safari' },
{ id: 2, label: 'Contact us', url: 'https://stackoverflow.com' },
];
const Navbar = () => {
return (
<nav>
<NavLinks items={menu} />
</nav>
);
};
export default Navbar;
In my NavLinks I bring in as props the items which is the menu we saw before and map through it and pass in as props url and label.
import React from 'react';
import NavLink from './NavLink';
const NavLinks = ({ items }) => {
const links = items.map((item) => (
<NavLink key={item.id} url={item.url} label={item.label} />
));
return <ul>{links}</ul>;
};
export default NavLinks;
In my NavLink component I am creating a Link to the url
import React from 'react';
import { Link } from 'react-router-dom';
const NavLink = ({ url, label }) => {
return (
<li className='nav-item'>
<Link to={url}>{label}</Link>
</li>
);
};
export default NavLink;
For some reason my Link has a path of multiple google urls. The url to the google homepage is duplicated many times. I do not know why this is happening.
Link component is to Provides declarative, accessible navigation around your application
If you use the Link component for the external URL, this will keep appending your URL to the previous ones.
For navigating to an external URL, I would suggest you to use native HTML tag instead:
const NavLink = ({ url, label }) => {
return (
<li className="nav-item">
<a href={url}>{label}</a>
</li>
);
};
Working example:

Passing props from parent to sibling in React

I am recreating a simple React app that I have already created in Angular. The React app has two components: one (menus.js) for a side menu and a second (content.js) that will display the content from each item in the menu when each link is clicked (just like an iframe of sorts). In the App.js I am making a REST API call to populate the state for the menus.js component. Note that both components are in the App.js as follows:
App.js
import React,{Component} from 'react';
import Menus from './components/menus';
import Content from './components/content';
class App extends Component {
state = {
menus: []
}
componentDidMount(){
fetch('api address')
.then(res => res.json())
.then((data)=> {
this.setState({menus: data})
})
.catch(console.log)
}
render(){
return (
<div>
<div><Menus menus={this.state.menus} /></div>
<div><Content /></div>
</div>
);
}
}
export default App;
here is the menu.js component; it takes a prop (menus) from App.js and builds the menu links with items from it:
import React from 'react';
import { BrowserRouter as Router, Link,} from "react-router-dom";
const Menus = ({ menus }) => {
return (
<Router>
<div>
<center><h1>Lessons</h1></center>
{menus.map(menu => (
<li key={menu.lesson}>
<Link to={`/lesson/${menu.lesson}`}>{menu.lessonName}</Link>
</li>
))}
</div>
</Router>
);
};
export default Menus;
Here is what I need - how do I pass items from the same prop (from App.js) to the content component? FYI - I need this to happen each time a link in the menu in menu.js is clicked (which is why a key is used in the list The simple idea is content will update in the content component each time a menu link in the menu component is clicked.
**content.js**
import React from 'react'
const Content = () => {
return (
<div>{menu.content}</div>
)
};
export default Content
Based on your description of the problem and what I can see of what you've written, it seems to me like you are trying to build an application where the menu persists, but the content changes based on menu clicks. For a simple application, this is how I would structure it differently.
<ParentmostComponent>
<MenuComponent someProp={this.state.some_data} />
<Switch>
<Route path={"/path"} render={(props) => <Dashboard {...props} someProp={this.state.some_other_data_from_parents} />
</Switch>
</ParentMostComponent>
This would allow the menu to always stay there no matter what the content is doing, and you also won't have to pass the menu prop to two components.
In your menu.js, attach the menu object to the Link
...
{menus.map(menu => (
<li key={menu.lesson}>
<Link to={{
pathname: `/lesson/${menu.lesson}`,
state: menu
}}> {menu.lessonName} </Link>
</li>
))}
...
In your content.js receive the menu like this:
import React from 'react'
const Content = () => {
console.log(props.location.state.menu.content);
return (
<div>{props.location.state && props.location.state.menu.content }</div>
)
};
export default Content
Read more here
Your example uses React Router, so this answer uses it as well.
First of all, move the Router up the hierarchy from Menus to App to make the router props available to all components. Then wrap your Content inside a Route to render it conditionally (i.e. if the path matches "/lesson/:lesson"):
class App extends Component {
state = {
menus: [
{
lesson: '61355373',
lessonName: 'Passing props from parent to sibling in React',
content: 'I am recreating a simple React app…'
},
{
lesson: '27991366',
lessonName: 'What is the difference between state and props in React?',
content: 'I was watching a Pluralsight course on React…'
}
]
}
render() {
const { menus } = this.state
return (
<Router>
<div>
<div><Menus menus={menus}/></div>
<Route path="/lesson/:lesson" render={({ match }) => (
<div><Content menu={menus.find(menu => menu.lesson === match.params.lesson)}/></div>
)}>
</Route>
</div>
</Router>
)
}
}
With the help of the render prop, you can access the router props (in this case match.params.lesson) before rendering your child component. We use them to pass the selected menu to Content. Done!
Note: The basic technique (without React Router, Redux etc.) to pass props between siblings is to lift the state up.

React - Show only the clicked user

In the following app, I'm accessing the random user API and show a list of 12 users.
App.js
import React, { useState } from 'react'
import UserList from './components/UserList'
const App = props => {
const [id, setID] = useState(null)
console.log(`Passed variable to App.js is: ` + id)
return (
<>
<UserList setID={setID} />
</>
)
}
export default App
UserList.js
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const UserList = ({ setID }) => {
const [resources, setResources] = useState([])
const fetchResource = async () => {
const response = await axios.get(
'https://api.randomuser.me/?results=12'
)
setResources(response.data.results)
}
useEffect(() => {
fetchResource()
}, [])
return (
<ul>
{resources.map(item => (
<li key={item.name.first}>
<div>
<h2>{item.name.first} {item.name.last}</h2>
<button
onClick={() => setID(item.login.uuid)}
>
Details
</button>
</div>
</li>
))}
</ul>
)
}
export default UserList
The above code is working. But now I want that if I click on the button for any of those listed users, only that user get showed.
How can I do that?
The response JSON looks like this:
Easiest way would be to apply a filter on your ressources variable to only display the user with selected uuid.
To do that, first you need to share selected id with UserList component:
App.js
<UserList id={id} setID={setID} />
Then update UserList accordingly:
UserList.js
const UserList = ({ id, setID }) => {
return (
<ul>
{ resources
.filter(user => Boolean(id) ? user.login.uuid == id : true )
.map(item => (
<li key={item.name.first}>
<div>
<h2>{item.name.first} {item.name.last}</h2>
{ Boolean(id) ?
<button onClick={() => setID(null)}>
Hide
</button>
:
<button onClick={() => setID(item.login.uuid)}>
Details
</button>
}
</div>
</li>
)
}
</ul>
)
}
That way, you will only display the select user in you <ul>. To unselect your user, just call setID(null)
Show user profile instead of list
If that solution work to filter your list, I guess you might want to adapt your page to show all details from your user. Next step would be to implement multi pages using react-router-dom with a url container your user uuid.
You can look at the url-params example which might be exactly what you are looking for.
Here's a slightly detailed option that extends beyond a single component but more easy to scale on account of modularity.
Create a new react component in a new file say, UserDetails.js
Now you need a way to navigate to this new page when the button is clicked.
So in your App.js you need a router like
import { BrowserRouter, Switch} from 'react-router-dom'
Then in your App.js file wrap all your components in the router:
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<Switch>
<Route exact path="/user-list" component={UserList} />
<Route exact path="/detail" component={UserDetails}/>
</Switch>
</div>
</BrowserRouter>
);
}
}
export default App;
Now you are ready to navigate to the user details page, when the button is clicked. So add a function like goToDetails like:
<button onClick={() => goToDetails(item)}>
Next define the function that navigates to the next page
goToDetails(item) {
this.props.history.push('/detail', {selectedUser:item:});
}
The history prop is available above because we earlier wrapped the entire app in BrowserRouter.
In the details page, you get the selectedUser details as a prop:
const selectedUser = this.props.location.state.selectedUser;
Now you can render it however you want.

Initialize script in componentDidMount – runs every route change

I am working on a navbar for my react app (using gatsbyjs to be precise). In the navbar I have a marquee that I initialize in the navbar component in componentDidMount.
It works as intended, but upon every route change componentDidMount will run again which results in the marquee speeding up for every page change, making it go faster and faster.
Is this expected behaviour? And if so, how do I make sure that the script is only run once?
navbar.js
import React from 'react';
import { Link } from 'gatsby';
import styles from '../styles/navbar.module.css';
import NewsMarquee from './newsMarquee';
import Marquee3k from 'marquee3000';
const topLevelNav = [
{
href: '/news',
label: <NewsMarquee/>,
extraClass: styles.navLinkNews,
mediaQueryClass: styles.navLinkHiddenSmall,
},
];
export default class Navbar extends React.Component {
componentDidMount() {
Marquee3k.init();
}
render() {
return (
<div>
<header className={styles.navbar} role="banner">
<nav className={styles.nav}>
{topLevelNav.map(({ href, label, extraClass = '', mediaQueryClass = '' }) => (
<Link
key={label}
to={href}
className={`${styles.navLink} ${extraClass} ${mediaQueryClass} ${menuItemsHidden}`}
activeClassName={styles.navLinkActive}
>
{label}
</Link>
))}
</nav>
</header>
</div>
)
}
}
newsMarquee.js
import React from 'react';
import { StaticQuery, graphql } from "gatsby";
import styles from '../styles/newsMarquee.module.css';
export default () => (
<StaticQuery
query={graphql`
query {
allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC } limit: 10) {
totalCount
edges {
node {
id
frontmatter {
title
date(formatString: "YYYY.MM.DD")
}
fields {
slug
}
}
}
}
}
`}
render={data => (
<div className={`marquee3k ${styles.marquee}`}>
<div>
{data.allMarkdownRemark.edges.map(({ node }) => (
<span className={styles.marqueeItem} key={node.id}>
{node.frontmatter.date} {node.frontmatter.title}
</span>
))}
</div>
</div>
)}
/>
)
Since I'm using GatsbyJS I went with this plugin from V1, which makes my layout component persist across pages.
gatsby-plugin-layout
This plugin enables adding components which live above the page components and persist across page changes.
This can be helpful for:
Persisting layout between page changes for e.g. animating navigation
Storing state when navigating pages
Custom error handling using componentDidCatch
Inject additional data into pages using React Context.
This plugin reimplements the behavior of layout components in gatsby#1, which was removed in version 2.

Resources