push a route to change url without reload page - reactjs

I have a react app.
my routing looks like this =>
<AppRoute path="/profile" exact={true} menuTitle="Profile" component={pages.Profile} />
<AppRoute path="/profile/post_feed" exact={true} menuTitle="Post Feed" component={pages.PostFeed} />
<AppRoute path="/profile/story_feed" exact={true} menuTitle="Story Feed" component={pages.StoryFeed} />
now in profile, there is some filter, so I would like to save them in the URL, to do so, I am using the following.
const setQueryParams = (filterId: number | null) => {
history.push(`${urlParams.pathname}?filterId=${filterId}`);
};
const handleFilterSelectionChange = (id: number) => {
setQueryParams(id);
//....
};
but this trigger a COMPLETE refresh of the page... and all the query that are associated. I put the following effect in my Profile component ( the one called from the route)
useEffect(() => {
console.log('FY');
}, []);
And this trigger at load, then for each filter, the page is fully refreshed. But I would like those thing to trigger once, and when the route don't change (only the query parameters) those effect don7t get retriggered. Is it possible ?

Try to use replace instead push
const setQueryParams = (filterId: number | null) => {
history.replace({ search: `?filterId=${filterId}` })// CHANGE HERE
};
const handleFilterSelectionChange = (id: number) => {
setQueryParams(id);
//....
};

Related

react-router : how can I change the URL to a "pretty" one once I get my post data loaded?

I have a route like this one
<Route
path="/post/:post_id"
exact
component={PostPage}
/>
Which loads my <PostPage/> component.
In that component, I load my post data (including its name) using the post_id parameter.
Once the data is successfully loaded based on that post_id; I would like to use the name of that post to make the browser URL prettier.
Example:
/post/123* : goes to my <PostPage/> component
which loads the post datas for post ID 123; including its name: post-foo-bar
I would like the browser URL to change to /post/123/post-foo-bar
I guess that using a wrong name should redirect to the good one :
/post/123/post-not-so-foo-bar would redirect to /post/123/post-foo-bar
How could I achieve this ?
Thanks !
Assuming you are using react-router v6 (it'd be easier/different in v5)
I went through the base tutorial for react-router, so it's a pretty basic app example:
https://stackblitz.com/edit/github-ylwf81?file=src%2Froutes%2Finvoice.jsx
Resulting webpage: https://github-ylwf81--3000.local.webcontainer.io/invoices/1995/Santa%20Monica
The main things to note in this, are the route definitions
For the invoices which would be analogous to your posts:
<Route path="invoices" element={<Invoices />}>
<Route
index
element={
<main style={{ padding: '1rem' }}>
<p>Select an invoice</p>
</main>
}
/>
<Route path=":invoiceId">
<Route index element={<Invoice />} />
<Route path=":invoiceName" index element={<Invoice />} />
</Route>
</Route>
Note the use of an index route for just the invoiceId, and a separate route for invoiceId + invoiceName.
Because they are both output to the same element, they aren't treated as different by React, so it retains state properly. You can see that in the example by the fact that it doesn't go back to the loading state.
As for the navigation to add in the invoiceName to the URL, it's just a basic navigation in a useEffect when the name of your object is defined (after an async call). And because this always triggers when the invoice.name changes, and how we aren't actually using the :invoiceName variable, it doesn't matter at all what the user inputs for that as long as the :invoiceId is correct
useEffect(() => {
if (!invoice?.name) {
return;
}
navigate(`/invoices/${invoice.number}/${invoice.name}`);
}, [invoice?.name, invoice?.number]);
In react-router-dom v5 you can declare an array of matching paths to render a routed component on. I suggest adding a matching path that includes a route param for the pretty name.
Example:
<Route
path={["/post/:post_id/:postName", "/post/:post_id"]}
component={PostPage}
/>
Then use an useEffect hook on the `PostPage component to check the following cases:
If post_id param and post id mismatch, then fetch post by id, then redirect to path with post name.
If postName param, current fetched post, and pretty name
mismatch, then redirect to current path with correct post name.
If no postName param, current fetched post, then redirect to path with post name.
Code
const { params, path } = props.match;
const { post_id, postName } = params;
const [post, setPost] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchPost = async () => {
setLoading(true);
try {
const post = await fetchPostById(post_id);
setPost(post);
history.replace(
generatePath(`${path}/:postName`, {
post_id: post.id,
postName: post.name
})
);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
if (post_id && post_id !== post?.id) {
fetchPost();
} else if (postName && postName !== post?.name) {
history.replace(
generatePath(path, {
post_id: post.id,
postName: post.name
})
);
} else if (!postName && post?.name) {
history.replace(
generatePath(`${path}/:postName`, {
post_id: post.id,
postName: post.name
})
);
}
}, [history, post, post_id, postName, path]);

Make Route for multiple variables using React Router

I'm trying to make multiple pages about some game character using React Router
For multiple pages, I want to load data from firebase and insert the data into pages.
the all character page formats are same and just need to change detailed data
For example,
we have
character 'a' : Age : 12, Gender : Male
character 'b' : Age: 13, Gender : Female
and each character page would need to show the character's data with loaded data
I need to show the data of only one character at one page.
Here is my code about routing
function App() {
useEffect(() => {
async function getFromDocs() {
const data = await db
.collection('Character')
.doc(curChar)
.get()
.then((snap) => {
return snap.data() as CharProps;
});
setData(data);
}
getFromDocs();
}, [curChar]);
const onCharChange = (text: string) => {
setCurChar(text);
};
return (
<>
<title>Tekken_info 0.1.0</title>
<GlobalStyle />
<Wrapper>
<PageContent>
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/Data" exact={true}>
<Page data={data} />
</Route>
</Switch>
</PageContent>
</Wrapper>
</>
);
}
export default App;
But how I did temporarily was not quite satisfying...
I load character data with useState and load it in to '/Data' page
I don't think this is good way and want to make one route for characters.
For example
when we access to '/a' load data about a only 'a'....
If anything you don't understand about my description and question
let me know
Instead of passing the data as props to Page component, You could move the useEffect into the page itself. so that only curChar is a route path variable and can be accessed inside the page something like:
/Data/curChar
Change the path to
path="/Data/:curChar"
Inside the page
const Page = () => {
let {
curChar
} = useParams();
//move the useEffect here.
useEffect(() => {
async function getFromDocs() {
const data = await db
.collection('Character')
.doc(curChar)
.get()
.then((snap) => {
return snap.data() as CharProps;
});
setData(data);
}
getFromDocs();
}, [curChar]);
return <div > Now showing details of {
curChar
} < /div>;
}

Prevent top scroll after update state

Hi I have problem with scroll to top page after every render.
In my Root component, when I get Array of objects items from Redux Store.
I filter mainArray on three subsArrays like : Hot, Favorite, Regular, then render each of them on specific route.
The filter func is running each time when mainArray is updated: like rating is rise and down or set favorite will be marked.
The question is, why react render each times when action is dispatching to redux store(I think redux causes this,I guess) and how I can prevent this.
Please give me a hint, I struggle with it for a while...
function Root() {
const state = useSelector((state) => state);
const { mainList: list } = state;
const [regularList, setRegularList] = useState([]);
const [hotList, setHotList] = useState([]);
const [favoriteList, setFavoriteList] = useState([]);
const dispatch = useDispatch();
const handleVote = (e) => {
const currentId = Number(e.nativeEvent.path[3].id);
const name = e.currentTarget.dataset.name;
dispatch(listActions.vote(currentId, name));
};
const handleSetFave = (e) => {
const currentId = Number(e.nativeEvent.path[3].id);
dispatch(listActions.setFave(currentId));
const setArrays = (arr) => {
const regularArr = arr.filter((meme) => meme.upvote - meme.downvote <= 5);
const hotArr = arr.filter((meme) => meme.upvote - meme.downvote > 5);
const favoriteArr = arr.filter((meme) => meme.favorite);
setRegularList([...regularArr]);
setHotList([...hotArr]);
setFavoriteList([...favoriteArr]);
};
useEffect(() => {
window.localStorage.getItem("mainList") &&
dispatch(
listActions.setMainList(
JSON.parse(window.localStorage.getItem("mainList"))
)
);
}, []);
useEffect(() => {
setArrays(list);
list.length > 0 &&
window.localStorage.setItem("mainList", JSON.stringify(list));
}, [list]);
return (
<div className={styles.App}>
<Router>
<Navigation />
<Route path="/" component={FormView} exact />
<Route
path="/regular"
component={() => (
<MemesView
list={regularList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
)}
/>
<Route
path="/hot"
component={() => (
<MemesView
list={hotList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
)}
/>
<Route
path="/favorite"
component={() => (
<MemesView
list={favoriteList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
)}
/>
</Router>
</div>
);
};
why react render each times when action is dispatching to redux store
Because you are subscribing to the whole redux state (by using useSelector((state) => state);), remember that each time an action is dispatched, redux computes a new state.
So you should not write const state = useSelector(state => state); otherwise your component will be rerendered each time an action is dispatched. Instead you must select the part of the state you are interested in.
I can deduce from your code you want to be notified every time there is a change on the mainList, so you can write :
const list = useSelector(state => state.mainList);
You can get more info by reading the documentation
by default useSelector() will do a reference equality comparison of the selected value when running the selector function after an action is dispatched, and will only cause the component to re-render if the selected value changed
Basically, the scroll to top page you are experiencing might also comes from a bad use of the Route component.
Try to use this
<Route path="/regular">
<MemesView
list={regularList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
</Route>
instead of
<Route path="/regular"
component={() => (
<MemesView
list={regularList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
)}
/>
Don't forget to also update /hot and /favorite routes.
You can read from the react router documentation
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).

In react, how do I redirect back the to current page I'm on and include a url parameter

I'm still working on my react skills and am having a struggle with redirecting back to THE CURRENT "page" (component/control) that I'm on. Below is some sample code of what I have in my app.js file and then a snippet of what I have in my "PageTwo.js" file. Currently when I click on the checkbox icon, the page appears to redirect but none of my table data shows or a different way to say it maybe is that my table control doesn't reload. Any help would be appreciated.
app.js
<Switch>
<Route path="/" exact component={SignUp} />
<Route path="/PageTwo" exact component={PageTwo} />
<Route path="/PageThree/:id" exact component={PageThree} />
</Switch>
PageTwo.js
.
.
.
const myRedirect = (stdId) => {
history.push('/PageTwo/' + stdId);
}
.
.
.
useEffect(() => {
let ref = Firebase.database().ref('/studentTime');
var refQuery = ref.orderByChild("TimeOut").equalTo("");
refQuery.on("value", studentTimeLog => {
theTimeLogList = O2A(studentTimeLog);
getTheTimeLog(theTimeLogList);
});
let userRef = Firebase.database().ref('/users');
var userQuery = userRef.orderByChild("Email").equalTo(authContext.userEmail);
//console.log(userQuery);
userQuery.once("value", function (snapshot) {
snapshot.forEach(function (child) {
if (child.val().Role === "ADMIN") {
authContext.isAdmin = true;
console.log(authContext.isAdmin);
}
});
});
}, []);
.
.
.
<TableCell align="center">
<CheckIcon onClick={() =>myRedirect(row.StudentId)}/>
</TableCell>
</TableRow>
Because your Route for page 2 is exact it will only show if it is exactly the correct url so when you add parameters, it's no longer exactly that. Simply remove the exact keyword and it should work.
Please check your router.
<Route path="/PageTwo" exact component={PageTwo} />
<Route path="/PageThree/:id" exact component={PageThree} />
const myRedirect = (stdId) => {
history.push('/PageTwo/' + stdId);
}
If you mean PageTree, follow this method.
<Route path="/PageThree/:id" exact component={PageThree} />
. . .
const [stdId, setStdId] = useState('');
const myRedirect = () => {
history.push('/PageThree/' + stdId);
}
or
use window.location.reload(); instead of history.push()
I figured out what I needed to do. Well let me say it this way, I found A WAY.
If I add the current URL to the 2nd optional array argument, then it will hit my useEffect() again as I desire. Let me say right off the bat that I'm not 100% sure this is THE BEST WAY to do this but it does fit my immediate need/desire to make sure that my useEffect() is hit if the url changes. It works for what I'm looking for because it re-renders my grid/table, which is what I want. If there is a better way to achieve this then I'm completely open to learning that. My main goal was for this page to "re-render" my control while getting the new URL (with my parameter added to it).
my app.js Switch/Route code
<Switch>
<Route path="/" exact component={SignUp} />
<Route path="/PageTwo/:stdid?" component={PageTwo} />
<Route path="/PageThree/:id" exact component={PageThree} />
</Switch>
my new useEffect()
useEffect(() => {
let ref = Firebase.database().ref('/studentTime');
var refQuery = ref.orderByChild("TimeOut").equalTo("");
refQuery.on("value", studentTimeLog => {
theTimeLogList = O2A(studentTimeLog);
getTheTimeLog(theTimeLogList);
});
//https://stackoverflow.com/questions/48240734/how-to-query-in-firebase-in-react
let userRef = Firebase.database().ref('/users');
var userQuery = userRef.orderByChild("Email").equalTo(authContext.userEmail);
//console.log(userQuery);
userQuery.once("value", function (snapshot) {
snapshot.forEach(function (child) {
if (child.val().Role === "ADMIN") {
authContext.isAdmin = true;
console.log(authContext.isAdmin);
}
});
});
}, [window.location.href]);

React router creates two exact same components on route change, how to reuse same component across two routes?

I've made a quick test that looks like this :
<Router>
<Switch>
<Route path="/foo" render={ () => <TestComponent key="test" data="1" /> } />
<Route path="/bar" render={ () => <TestComponent key="test" data="2" /> } />
</switch
</Router>
Where the TestComponent looks like this :
const TestComponent = withRouter(({ history, data }) => {
const [ value, setValue ] = useState(null);
const loading = !value;
useEffect(() => {
setTimeout(() => {
setValue(data);
history.push('/bar');
}, 1000 );
}, [ data ]);
console.log("**VAL", data, value);
return <div>
{ data } / { loading ? "LOADING" : value }
</div>;
});
So, when I hit the route /foo, the component loads, wait a second, then redirect to the other route using the same component (albeit a different instance).
The debug shows
**VAL 1 null
**VAL 1 1
**VAL 2 null // <-- expected : VAL 2 1
**VAL 2 2
**VAL 2 2
I would've expected React to see that both are the same component and not create a different instance. How can I have the same component respond to two different routes?
You can have one Route component have multiple paths, so multiple paths will result in the same component. More specifically the path prop of Route can take a string or string array. See docs here: https://reactrouter.com/web/api/Route/path-string-string
Then you can use one of the react-router hooks to determine which route you are on and render whatever differences you need based on that. Alternatively, you could use URL Parameters or Query Parameters.

Resources