Conditional render Navigation - reactjs

Hi I need conditional render my navigation. I use Gatsby and GraphQl. I have Navigation Component and depends on route in which it is I need render different Navigation. The problem is I can not make conditional useStateStatic hook. I've made two diffrent Navigation in my source it is DatoCMS,but I can not query for it.
const Navigation = () => {
const pathName = window.location.pathname;
const data = useStaticQuery(
graphql`
{
datoCmsNavigation(sectionId: { eq: "navigation" }) {
sectionId
logo {
url
alt
}
menuItem {
id
name
href
}
}
}
`
);
const {
datoCmsNavigation: { sectionId, logo, menuItem },
} = data;
return (
<NavBar id={sectionId}>
<NavBarLogoWrapper>
<a href="/">
<img src={logo.url} alt={logo.test} />
</a>
</NavBarLogoWrapper>
<NavBarList>
{menuItem.map((item, index) => (
<NavBarItem key={index}>
<a href={item.href}> {item.name.toLowerCase()}</a>
</NavBarItem>
))}
</NavBarList>
</NavBar>
);
};
Here is my Navigation component. Does anyone has Idea how can I deal with it ?

Your approach will fail in gatsby build since window (and other global objects) are not available during the SSR. You don't need a useState hook in your scenario, you just need to know which page is actually the user seeing.
Top-level components (pages) in Gatsby own the location property, which allows you to know a bunch of data, including the current page. You can pass that data to your Navigation component in order to make your comparison. For example:
const Blog = ({location, someOtherDestructuredProps}) => {
return <section>
<Navigation currentPage={location.pathname}/>
<OtherComponent />
</section>
}
Then, in your Navigation component:
const Navigation = ({currentPage ="/"}) => {
const pathName = window.location.pathname;
const data = useStaticQuery(
graphql`
{
datoCmsNavigation(sectionId: { eq: "navigation" }) {
sectionId
logo {
url
alt
}
menuItem {
id
name
href
}
}
}
`
);
const {
datoCmsNavigation: { sectionId, logo, menuItem },
} = data;
return (
<NavBar id={sectionId}>
<NavBarLogoWrapper>
<a href="/">
<img src={logo.url} alt={logo.test} />
</a>
</NavBarLogoWrapper>
<NavBarList>
{menuItem.map((item, index) => {
if(currentPage == '/blog') return <DifferentNavBar />
return <NavBarItem key={index}>
<a href={item.href}> {item.name.toLowerCase()}</a>
</NavBarItem>
})}
</NavBarList>
</NavBar>
);
};
Among minor changes, the important part is to set a default value for the currentPage (in case it's missing) and the change of the map loop in order to return different navigation bars. Of course, tweak it to adapt it to your needs and requeriments.

Related

Next/React: How to store fetched data from backend (Sanity) in localstorage?

I have a gallery page that shows 6 categories (as images that are used as links) that, when one is selected, will move onto the next page and show a list of referenced sets. The next page should show the category name that was selected.
I'm currently trying to store the category name (which is located in my backend) into local storage once it is clicked, which I can then extract into the next page. This is what I currently have:
import React, { useState, useEffect } from 'react';
import { client, urlFor } from '../lib/client';
import { Header, Footer } from '../components';
import Link from 'next/link';
const gallery = () => {
// fetches sanity data
const [categoryData, setCategories] = useState(null);
useEffect(() => {
client.fetch(
`*[_type=="category"]{
categoryImage_alt,
category_image{
asset->{
_id,
url
}
},
category_name,
contained_sets
}`)
.then((data) => setCategories(data))
.catch(err => console.error(err));
}, [] );
const selectedCategory = [];
const saveCategory = (e) => {
selectedCategory.push({})
localStorage.setItem('selectedCategoryName', JSON.stringify(selectedCategory));
console.log(localStorage)
}
return (
<div>
<Header />
<main className="main-gallery">
<div className="title">
<div className="title-line-left"></div>
<h2>categories</h2>
<div className="title-line-right"></div>
</div>
<div className="categories">
<ul className="categories-container">
{categoryData && categoryData.map((category, index) => (
<li key={index}>
<Link href="/sets"><img src={urlFor(category.category_image).auto('format').url()} alt={category.categoryImage_alt} onClick={saveCategory(category.category_name)} /></Link>
</li>
))}
</ul>
</div>
</main>
<Footer />
</div>
)
}
export default gallery
(I've also tried putting the onClick event in the Link tag instead of the img tag, same result). At the moment however, it doesn't store anything when the category is clicked. Instead, when I click onto the navbar menu that opens up this page with categories, it immediately prints out this 6 times (and nothing happens when I click onto one of the categories):
replace your saveCategory function with
const saveCategoryName = (categoryName) => {
localStorage.setItem('selectedCategoryName', categoryName);
}
and replace the onclick function with
onClick={() => saveCategoryName(category.category_name)}
now selected category name will stay in localStorage

Creating a history back link and make use of my existing link component

I have a certain generic link component (an atom, I am using Atomic design for structuring my React app), which either returns just a <span> with a Title and arrow icon or when it does have a url it returns a react-router-dom <Link>.
I am using react-router-dom v5.
import React from 'react';
import { Link } from 'react-router-dom';
import { Arrow} from 'app/icons';
import { Props } from './MyLink.types';
const MyLink = ({ variant = 'iconRight', text, url }: Props) => {
const renderLinkBody = () => {
return (
<React.Fragment>
<span css={styles.label}>{text}</span>
<Arrow />
</React.Fragment>
);
};
return url !== undefined ? (
<Link to={url} title={text} >
{renderLinkBody()}
</Link>
) : (
<span>{renderLinkBody()}</span>
);
};
export default MyLink;
Now I want to use this component for creating a Back button (similar behaviour as the browser back button).
Is it a clean approach passing an onClick prop (function) from parent to child, something like:
const history = useHistory();
<MyLink text="My title" variant="iconLeft" onClick={history.goBack} />
and then adjust MyLink component a bit:
return url !== undefined || onClick ? (
<Link to={url} title={text} onClick={onClick} >
{renderLinkBody()}
</Link>
) : (
<span>{renderLinkBody()}</span>
);
I think this approach isn't bad. When attaching an onClick handler to the Link component you will likely need to prevent the default link behavior from occurring. You will also need to provide a valid to prop to the Link component when using a click handler.
Example:
const MyLink = ({ variant = "iconRight", onClick, text, url }) => {
const clickHandler = e => {
if (onClick) {
e.preventDefault();
onClick();
}
};
const renderLinkBody = () => {
return (
<>
<span css={styles.label}>{text}</span>
<Arrow />
</>
);
};
return url !== undefined || onClick ? (
<Link to={url || "/"} title={text} onClick={clickHandler} >
{renderLinkBody()}
</Link>
) : (
<span>{renderLinkBody()}</span>
);
};
Because of the issue with the onClick and the Link action, you may want to make the "back" navigation a button instead, and refactor MyLink to conditionally render the button, the link, or the span, depending on needs.

When using a functional component, why do parent state changes cause child to re-render even if context value doesn't change

I'm switching a project from class components over to functional components and hit an issue when using a context.
I have a parent layout that contains a nav menu that opens and closes (via state change in parent). This layout component also contains a user property which I pass via context.
Everything worked great before switching to the functional components and hooks.
The problem I am seeing now is that when nav menu is opened from the layout component (parent), it is causing the child component to re-render. I would expect this behavior if the context changed, but it hasnt. This is also only happening when adding useContext into the child.
I'm exporting the children with memo and also tried to wrap a container with the children with memo but it did not help (actually it seemed to cause even more renders).
Here is an outline of the code:
AppContext.tsx
export interface IAppContext {
user?: IUser;
}
export const AppContext = React.createContext<IAppContext>({});
routes.tsx
...
export const routes = <Layout>
<Switch>
<Route exact path='/' component={Home} />
<Route exact path='/metrics' component={Metrics} />
<Redirect to="/" />
</Switch>
</Layout>;
Layout.tsx
...
const NavItems: any[] = [
{ route: "/metrics", name: "Metrics" }
];
export function Layout({ children }) {
const aborter = new AbortController();
const history = useHistory();
const [user, setUser] = React.useState<IUser>(null);
const [navOpen, setNavOpen] = React.useState<boolean>(false);
const [locationPath, setLocationPath] = React.useState<string>(location.pathname);
const contextValue = {
user
};
const closeNav = () => {
if (navOpen)
setNavOpen(false);
};
const cycleNav = () => {
setNavOpen(prev => !prev);
};
React.useEffect(() => {
Fetch.get("/api/GetUser", "json", aborter.signal)
.then((user) => !aborter.signal.aborted && !!user && setUser(user))
.catch(err => err.name !== 'AbortError' && console.error('Error: ', err));
return () => {
aborter.abort();
}
}, []);
React.useEffect(() => {
return history.listen((location) => {
if (location.pathname != locationPath)
setLocationPath(location.pathname);
})
}, [history]);
const navLinks = NavItems.map((nav, i) => <li key={i}><Link to={nav.route} onClick={closeNav}>{nav.name}</Link></li>);
return (
<div className="main-wrapper layout-grid">
<header>
<div className="header-bar">
<div className="header-content">
<div className="mobile-links-wrapper">
<ul>
<li>
<div className="mobile-nav-bars" onClick={cycleNav}>
<Icon iconName="GlobalNavButton" />
</div>
</li>
</ul>
</div>
</div>
</div>
<Collapse className="mobile-nav" isOpen={navOpen}>
<ul>
{navLinks}
</ul>
</Collapse>
</header>
<AppContext.Provider value={contextValue} >
<main role="main">
{children}
</main>
</AppContext.Provider>
<a target="_blank" id="hidden-download" style={{ display: "none" }}></a>
</div>
);
}
Metrics.tsx
...
function Metrics() {
//Adding this causes re-renders, regardless if I use it
const { user } = React.useContext(AppContext);
...
}
export default React.memo(Metrics);
Is there something I am missing? How can I get the metrics component to stop rendering when the navOpen state in the layout component changes?
Ive tried memo with the switch in the router and around the block. I've also tried moving the contextprovider with no luck.
Every time your Layout component renders, it creates a new object for the contextValue:
const contextValue = {
user
};
Since the Layout component re-renders when you change the navigation state, this causes the context value to change to the newly created object and triggers any components depending on that context to re-render.
To resolve this, you could memoize the contextValue based on the user changing via a useMemo hook and that should eliminate the rendering in Metrics when the nav state changes:
const contextValue = React.useMemo(() => ({
user
}), [user]);
Alternatively, if you don't really need the object, you could simply pass the user as the context value directly:
<AppContext.Provider value={user}>
And then access it like:
const user = React.useContext(AppContext);
That should accomplish the same thing from an unnecessary re-rendering point of view without the need for useMemo.

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.

react-bootstrap breadcrumb with react-router-dom

I'm trying to use react-bootstrap breadcrumb as below.
<Breadcrumb>
<Breadcrumb.Item href="#">Home</Breadcrumb.Item>
<Breadcrumb.Item><Link to={"/products"}>Products</Link></Breadcrumb.Item>
<Breadcrumb.Item active>{productName}</Breadcrumb.Item>
</Breadcrumb>
As you can expect, products Link will render anchor tag inside another anchor tag, which is invalid markup. But Home creates a simple anchor tag instead of react's Link making the page to reload, making it unusable.
What's the solution for this? Unfortunately, there's no mention of this in react-bootstrap doc. (link)
I would probably use react-router-bootstrap, but if you don't want to include it as a dependency, you can apply the link by hand using the now available linkAs and linkProps Breadcrumb params. For instance:
<Breadcrumb.Item linkAs={Link} linkProps={{ to: "/path" }}>
My item
</Breadcrumb.Item>
This approach is interesting especially if you are using just the "as" attribute with other components like Button or NavLink.
This nicely works and it does not refresh the whole page, only what's needed to change
import { Breadcrumb } from "react-bootstrap";
import { Link } from "react-router-dom";
export const SiteMap = ({ hrefIn }) => {
const items = [
{ href: "/dictionaries", name: "Dictionaries" },
{ href: "/antonyms", name: "Antonyms" },
];
return (
<Breadcrumb>
{items.map((item) =>
item.href === hrefIn ? (
<Breadcrumb.Item active>{item.name}</Breadcrumb.Item>
) : (
<Breadcrumb.Item linkProps={{ to: item.href }} linkAs={Link}>
{item.name}
</Breadcrumb.Item>
)
)}
</Breadcrumb>
);
};
I ended up dropping react-boostrap and doing it 'by hand':
const Breadcrumbs = ({ breadcrumbs }) => (
<ol className="breadcrumb">
{breadcrumbs.map((breadcrumb, index) => (
<li key={breadcrumb.key}>
<NavLink to={breadcrumb.props.match.url}>
{breadcrumb}
</NavLink>
</li>
))}
</ol>
);
It works for me if I wrap <Breadcrumb.Item> into the <LinkContainer>.
Now outer Breadcrumb "My applications" which points to /applications URL redirects my app with react-router to the applications page.
I tested this with react-bootstrap v0.32.4 https://5c507d49471426000887a6a7--react-bootstrap.netlify.com/components/breadcrumb/
I got <LinkContainer> from react-router-bootstrap package: https://github.com/react-bootstrap/react-router-bootstrap
I saw "the wrapping" here before, though I don't generate breadcrumb in a loop: https://github.com/react-bootstrap/react-router-bootstrap/issues/141#issue-122688732
import { LinkContainer } from 'react-router-bootstrap';
// and then in the render function
<Breadcrumb>
<LinkContainer to="/applications" exact>
<Breadcrumb.Item>My applications</Breadcrumb.Item>
</LinkContainer>
<Breadcrumb.Item active>My First Applicaton</Breadcrumb.Item>
</Breadcrumb>
Here I have passed onClick to Breadcrumb.Item to handle navigation with the help of
import { withRouter } from "react-router-dom";
Here is the sample:
const RenderBreadcrumb = ({ breadcrumbInfo, history }) => {
const handleRedirect = (url) => {
history.push(url);
}
return (
<Breadcrumb>
{ map(breadcrumbInfo, (item) => {
if(item.isActive) {
return (<Breadcrumb.Item active>{item.text}</Breadcrumb.Item>);
}
return (<Breadcrumb.Item onClick={() => { handleRedirect(item.link); }}>{item.text}</Breadcrumb.Item>);
})}
</Breadcrumb>
)
}
export default withRouter(RenderBreadcrumb);
Bootstrap.Item internally uses SafeAnchor which allows you to not use a link if you don't want to.
Using the as prop you can modify what tag is used (a by default). For example you can pass:
<Bootstrap.Item as="div" />
And it will use a div tag for presenting the item.

Resources