Alternative to React useLocation hook for page update based on url? - javascript

This is a tricky problem.
A bit of info: I can have multiple collections of multiple images and a specific header logo for each collection. My collection url looks like this /collection/{collectionId}/item/{itemId}
const MyApp = () => {
// unrelated code
return(
<BrowserRouter>
<SiteLayout>
</BrowserRouter>
)
}
and
const SiteLayout = () => {
location = useLocation()
collectionId = location.pathname.split('/')[2] //gives me the collection Alias
const [collData, setCollData] = useState()
useEffect(() => {
const getCollectionData = async() => {
//fetch collection data
//setCollData(fetched data)
}
getCollectionData()
}, [collectionId])
return(
<CollectionContext.Provider value={collData}>
<div className='mainContainer'>
<div className='headerContainer'>
<Header/>
</div>
<div className='mainContent'>
<Route exact path='/collection/:collectionId' component={CollectionViewPage}>
<Route exact path='/collection/:collectionId/item/:itemId' component={ItemViewPage}
</div>
<div className='footerContainer'>
<Footer/>
</div>
</div>
</CollectionContext.Provider>
)
}
export default SiteLayout
My thoughts:
I set it up this way, because I cant use useLocation() outside of <BrowserRouter> and I want to update my collectionContext (which contains information about a the collection I'm on, including the collection specific header logo) when the collectionId changes, which is part of the location
My issue:
When I move from item to item, within a collection, the header and everything else still re-renders, for example
from -- /collection/collection1/item/1
to -- /collection/collection1/item/2
My thoughts: This header shouldn't update because the collectionId never changed. However, when I look at the Profiler in React dev tools, it says the Header changed because the parent changed, following it up to <Route> which changed because it's state (location) changed.
What I'm looking for: How do I refactor this to update when the collectionId changes, but not every time the location changes? I need this because (many things, but for simplicity) I want the header to update when I change collections, so that it can use the correct logo, but I would like to prevent the <Header> from re-rendering if I'm navigating around within the collection.
Something else I've tried:I've tried ripping out useLocation and instead using window.location.pathname but nothing will update.

Solved
Using the suggestion from #HMR in response to the original question:
If Header and CollectionViewPage are functional components maybe you can wrap [them] in React.memo If they still re render [...] look at what you need and maybe create a container that only picks what you actually need...
Implementation:
By adding a middle layer, I am able to split location, and then memoize SiteLayout based on whatever part of it I would like.
const MyApp = () => {
// unrelated code
return(
<BrowserRouter>
<LocationPartition>
</BrowserRouter>
)
}
export default MyApp
where in LocationPartition (could probably use a better name):
const LocationPartition = () => {
const location = useLocation()
const collectionId = location.pathname.split('/')[2]
return(
<SiteLayout collection={collectionId}/>
)
}
export default LocationPartition
and so in SiteLayout, I can now memoize based off the collection prop passed from LocationPartition
const SiteLayout = memo(({collection})) => {
const [collData, setCollData] = useState()
useEffect(() => {
const getCollectionData = async() => {
//fetch collection data
//setCollData(fetched data)
}
getCollectionData()
}, [collection])
return(
<CollectionContext.Provider value={collData}>
<div className='mainContainer'>
<div className='headerContainer'>
<Header/>
</div>
<div className='mainContent'>
<Route exact path='/collection/:collectionId' component={CollectionViewPage}>
<Route exact path='/collection/:collectionId/item/:itemId' component={ItemViewPage}
</div>
<div className='footerContainer'>
<Footer/>
</div>
</div>
</CollectionContext.Provider>
)
}
export default SiteLayout
And lo and behold ... React Profiler now only shows re-renders where I want them!

Related

React: How to update parent component's useEffect hook when using routes

I use the useEffect hook to dispatch the getQuestions function in order to get the data from the server
function App () {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getQuestions());
}, [dispatch]);
return (
<Routes>
<Route exact path="/" element={<Layout/>}>
<Route path="repetition" element={<Repetition/>}/>
<Route path="family" element={<Family/>}/>
</Route>
</Routes>
);
}
The problem is that when I, for example, open the family link (which I declared in the App function), initially I get the data, but when I refresh the page, the data disappears.
I certainly understand that when the page is refreshed the parent App component is not rendered from this and I get an error, similar issues I have looked at in the forums where it was suggested to use withRouter which updates the parent component, but my version of react-router-dom does not supports withRouter, except that I don't want to downgrade my version of react-router-dom to use withRouter.
I would like to know if there is any way to fix this problem.
I tried the option that #Fallen suggested, i.e. I applied the useEffect hook in each child element and analyzed this approach in GoogleLighthouse, and I'm happy with the results.
Here is my final code in child component
function Family () {
const dispatch = useDispatch();
const questions = useSelector(state => state.QuestionsSlices.familyQuestions);
useEffect(() => {
dispatch(getFamilyQuestions());
}, [dispatch]);
return (
<>
{questions.data.map((item, idx) => (
<div key={idx}>
{ idx + 1 === questions.score && CheckQuestionsType(item, questions) }
</div>
))}
</>
);
}

React : navigate to a html tag inside a component

i want to go from a component to another and slide directly to an html tag inside of it .
using react router v6
<Route path="/mypage/#c" element = {<MyPage/>} />
( #c is a id of the html tag in the /mypage )
but the code i've provided isn't working .
it just goes to the page and doesn't slide into the tag .
The hash should be added to the link or simply read from the URL, i.e. the location object, instead specified on the Route component's path prop. Adding it to the path will break route path matching.
<Route path="/mypage" element={<MyPage />} />
react-router-dom#6 doesn't handle linking directly to hash-tags in the document, and at the moment the best library out there for doing this in previous versions of RRD, react-router-hash-link has yet to update to support RRDv6.
The MyPage component could use a useEffect hook with a dependency on the location.hash, query the DOM, and attempt to scroll that element into view.
Example:
import { useLocation } from "react-router-dom";
const MyPage = () => {
const { hash } = useLocation();
useEffect(() => {
const el = document.querySelector(hash);
if (el) {
el.scrollIntoView({ behavior: "auto" });
}
}, [hash]);
return (
<>
... content ...
<div id="c">This is the content I'm interested in</div>
... more content ...
</>
);
};

Single Page Application in React with routing - same structure, different content

Problem
I'm trying to make an SPA with routing (ideally with React hooks) in React, but all the examples, descriptions i find are about displaying different components based on the URL. What i want is something like Youtube or Google docs, where the page structure/components are (mostly) the same and only the content changes.
Context
(Edit: adding a bit more context.)
This is going to be a document editor/presenter.
Page structure: after login, there is always a toolbar(blue color) on the top, for menus, notifications, etc. The rest of the screen will be mostly like the two examples below:
Example1:
Example2:
The search pane(orange) could be switched on/off by a button on the toolbar or by a user session variable. The document will be presented in the document section(grey) based on either a user session variable, doc ID provided in URL or selecting a document in the search pane.
Planned URLs
(Added in edit.)
Landing page: /login , login page.
Landing page: / , here the toolbar and a preconfigured, user session based default doc would be presented.
Document page: /doc?id=oys2OPkfOwQ , same as landing page but document section would contain the document with ID provided as query param.
Anything else: /something , toolbar and something under it.
Idea
(Added in edit.)
The layout is defined by CSS grid and page structure changes based on a variable. So this is going to be a prop for the App component coming from default value and user session configured variable and could change later.
This is the functionality i imagine for the App component (pseudo code-like thing):
<Router>
<Route path='/login'>
<Login/>
// Components: Toolbar and something under it
</Route>
<Route path='/'>
<DocApp/>
// Components: Toolbar, Document or Toolbar, Search and Document
// Default document loaded for default, not logged in user
// Default document loaded from stored user session
</Route>
<Route path='/doc'>
<DocApp/>
// Components: Toolbar, Document or Toolbar, Search and Document
// Same as for '/' except document with ID set as query param is displayed
// This could be called/triggered from search and document component as well
</Route>
<Route path='/somethingelse'>
<SomethingElse/>
</Route>
</Router>
Question
(Edit: rephrased, original question was how to implement a solution where different documents loaded based on URL query parameter.)
What i'm mostly interested in if there is a simpler way to draw the landing layout '/' and specific doc presenter /doc?id=oys2OPkfOwQ layout? In both cases the same components get displayed, only the provided parameter(doc to present) is different.
Solution
(Added in edit.)
By reading the answers and feedback and re-thinking my problem i realized that i have a multiple URLs same content problem.
Using React Router to render components based on UrlParams.
First of all, edit your routes to render DocumentLoader component under the route /doc
// file: app.js
import React from "react";
import { BrowserRouter, Route } from "react-router-dom";
import DocumentLoader from "./DocumentLoader";
const App = (props) => {
return <BrowserRouter>
<Routes>
<Route path="/doc" element={<DocumentLoader />}>
</Routes>
</BrowserRouter>
}
Create custom hooks for loading documents
You need two custom hooks, one for loading new document by changing the docId query parameter, and another hook to listen to docId changes to reload new document from your backend.
NOTE: Edit loadDocumentData to load from your backend
// file: hooks.js
import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
/**
* TODO:// Refactor this function to call your backend to get
* Document data by docId
*/
const loadDocumentData = (docId) =>
new Promise((resolve, reject) => {
// this setTimeout for demonstration porpuse only
setTimeout(() => {
resolve({ id: docId, name: `Document name for ${docId}` });
}, 3000);
});
export const useDocument = () => {
const [loading, setLoading] = useState(true);
const { docId, loadDocument } = useDocumentParam();
const [document, setDocument] = useState(null);
useEffect(() => {
setLoading(true);
// Load your document data based on docID
loadDocumentData(docId)
.then((doc) => {
setDocument(doc);
setLoading(false);
})
.catch((e) => {
console.error('Failed to load doc', docId);
});
}, [docId, setLoading]);
return { document, loading, loadDocument };
};
export const useDocumentParam = () => {
const [searchParams, setSearchParams] = useSearchParams();
const docId = searchParams.get('d');
const loadDocument = useCallback(
(newDocId) => {
setSearchParams({ d: newDocId });
},
[setSearchParams]
);
return { docId, loadDocument };
};
Create DocumentLoader component
To listen on query param changes, load document from server-side, display loading indicator and render the "DocPresenter" component.
// file: DocumentLoader.js
import * as React from 'react';
import DocPresenter from './DocPresenter';
import { useDocument } from './hooks';
const DocumentLoader = (props) => {
const { loading, document, loadDocument } = useDocument();
if (loading) {
return <div>Display loading indicator while loading the document</div>;
}
return (
<div className="document-container">
<div className="toolbar">NavBar</div>
<div className="searchbox">search component</div>
<div className="editor">
<DocPresenter document={document} setParentstate={loadDocument} />
</div>
</div>
);
};
export default DocumentLoader;
Checkout Live Example on StackBlitz.
Helper Links:
React Router Docs
React Custom Hooks Docs
Here's how I would do it. Notice that the URL will remain the same.
const DynamicComponent = () => {
const components = {
Component1: <Component1 />,
Component2: <Component2 />,
Component3: <Component3 />,
};
const [component, setComponent] = useState(components["Component1"]);
return (
<div>
<div id="nav">
<span onClick={(e) => setComponent(components["Component1"])}>
Set to component 1
</span>
<span onClick={(e) => setComponent(components["Component2"])}>
Set to component 2
</span>
<span onClick={(e) => setComponent(components["Component3"])}>
Set to component 3
</span>
</div>
<div>{component}</div>
</div>
);
};
export default DynamicComponent;

React: How to redirect to new route (different id) from same route?

In react-router, we cannot push the same route in useHisotry() and cause a re-render. E.g., if component App is showing on route https://localhost:3000 and I click the button Click Me!, it won't cause a re-render:
function App() {
const history = useHistory();
return (
<button onClick={() => {history.push('/')}}> Click Me! </button>
)
}
I want to achieve similar functionality, but I am unsure about the approach or what I am missing.
My current route looks like this: https://localhost:3000/user/1
I want to go to user/2 by clicking a button.
My code looks like the below:
<Route exact path="/user/:userId" component={User} />
function User() {
const history = useHistory();
return (
<button onClick={() => {history.push('/user/2')}}> Click Me! </button>
)
}
The above code changes the route but doesn't re-render the component. How can I fix this issue?
Thanks
I don't recommend using history for this case.
If you really need to, inside User component get userId parameter and react on that.
<Route exact path='/user/:userId' component={User} />
const User = () => {
const { userId } = useParams();
return (
<div>userId: { userId }</div>
);
}
export default User;
My advice is to upgrade to react router dom v6 and use useNavigate , tutorial here
once you import useNavigate from react-router-dom
let navigate = useNavigate();
and on your button you call this function on click passing your desired url
<button onClick={()=> navigate('/users/2')}
Your component's info wont change because you arent rendering anything dynamically in it, so you should grab the userid from the url, and then lets say display it. Check Docs
As the answer below, you can do it exactly as he said.
const { userId } = useParams();
return (
<div>userId: { userId }</div>
);

React unique state for every component of same type

I am creating simple React App with Posts and Comments (similar to how facebook posts works). One post can have multiple comments and there are multiple posts. I am using useState() fuction in CommentsList component:
export default function CommentsList(props) {
const [comments, setComments] = React.useState([]);
function handleNewComment(comment) {
setComments([<Comment text={comment} />, ...comments ]);
}
return (
<ul className="comments-list">
<CommentInput onNewComment={handleNewComment} />
{comments}
</ul>
);
}
Post component:
export default function Post(props) {
return (
<div className="post">
//...
<CommentsList />
</div>
);
}
The problem is whenever I create new Post, all written comments became part of newest post. I need solution where every post have unique comments list and when the list changes, react re-render comments. What is the right solution?
Your state should be the comment texts alone, not components. Let React take care of creating the components in the returned JSX:
export default function CommentsList(props) {
const [comments, setComments] = React.useState([]);
function handleNewComment(comment) {
setComments([comment, ...comments]);
}
return (
<ul className= "comments-list" >
<CommentInput onNewComment={ handleNewComment } />
{ comments.map(comment => <Comment text={ comment } />) }
</ul>
);
}
Similarly, wherever you're using Post, return a new instance of Post every time, eg with
return <Post />
rather than saving Post instances in the parent component's state. State should not contain React elements, especially when those React elements may have state of their own - it can lead to confusing behavior like you're experiencing.

Categories