How to watch if the querystring changes within React Router v6? - javascript

I've a form with a drop down on it, when I select the drop down I push the value onto the querystring. When the url with that querystring is first hit it stores the querystring param into state and then uses it to populate the drop down. This all works as intended. My issue is triggering the form to see changes to the querystring while still on the same page.
If I'm already on the page but then click a react router Link to the same page but with a different query string parameter it doesn't catch/see that it has changed. I'm wondering how to watch the querystring for changes or if there is a better way to handle this.
I've found that you can listen to the history object that React-Router is meant to use under the hood but I've had little success with this and again I'm not sure if this is the right way to go https://github.com/ReactTraining/history/blob/master/docs/getting-started.md
I've tried adding the following to the page but the listener never seems to fire when I change the querystring (aka search) of the url. Wonder what I'm missing?
useEffect(() => {
// Listen for changes to the current location.
console.info("wiring up history listener");
let unlisten = history.listen(({ location, action }) => {
console.info("HISTORY CHANGED", location.search);
console.log(action, location.pathname, location.state);
// Plan was to update my state here if needed
});
return unlisten;
}, []);
Using react-router-dom v6.

If you want to listen for changes on the path's query string parameters you need to "listen" for changes to them from the location object. Use the useLocation hook to get the location object.
location
{
key: 'ac3df4', // not with HashHistory!
pathname: '/somewhere',
search: '?some=search-string',
hash: '#howdy',
state: {
[userDefined]: true
}
}
Listen for changes using effect hook.
const { search } = useLocation();
useEffect(() => {
// search query string changed
}, [search]);

Your code doesn't work because react-router uses a different instance of history so your custom listeners aren't fired when the change is made through react-router's Link (it gets handled by a different version of history). However, clicking on the browser's forward or back buttons should trigger your listener since this notifies all history instances.
The best tool for your use case is useSearchParams hook
const [searchParams] = useSearchParams();
useEffect(() => {
// code for handling search query change
}, [searchParams]);
Also, you might not need to use useEffect but treat searchParams as a source of truth for search data without creating another entity in the state.

react-router comes with all the hooks you need. Sounds to me like you want: useParams

Related

Keep param consistent on location change ReactJs

I am using react router v4 to change location in ReactJs.
this.props.history.push("/profile");
<Link to="/profile" />
The above code works fine.
Now I want to keep a param consistent in URL http://localhost:3000?source=test by using the same code as above.
One approach is that I find all the occurrences in the code and add condition that if params source=test exist then append it to the the URL as well but this approach doesn't look fine to me as I have add condition on every redirect, Link and history.push
Second approach that I find is that use of listener on location update given by react router
In my Main Route file
class App extends Component {
componentDidMount() {
this.unlisten = this.props.history.listen((location, action) => {
if (/source=ep/.test(this.props.location.search)) {
location.search = _startsWith(location.search, "?") ? location.search + "&source=test" : "?source=test"
}
});
}
}
With this approach I can easily append the params in search query of react router but the param doesn't show up in URL.
the URL looks like this http://localhost:3000/profile and When I get search params from react-router console.log(this.props.location.search) it shows the param source=test and it's exactly what I want but In this case if user refreshes on this page the search params lost from react-router as well because it's not in the URL.
Can you guys help me to keep source=test consistent even in URL.

Use browser history state with next js

I am trying to implement custom scrolling with nextjs. I want to be able to save the scroll position of the page when a user leaves so when they return with popstate I can reset the scroll position.
The issue is that when I try to use replaceState with the history api next/router overrides what I put there with their own data. Next/router doesn't provide any type of history for their router and they don't put an id or anything that I can reference to use my own state. So I have no way to reference what page of the history stack I'm actually on. Next/router api docs are located here Nextjs Router API.
Here is a quick example of what I'm trying to do:
const setHistory = () => {
const { state } = window.history;
const newState = {
...state,
scroll: 'my custom scroll value'
};
history.replaceState(newState, '', state.url);
}
const handleRouteChangeStart = (url) => {
setHistory();
}
const handlePopstate = () => {
console.log(window.history)
}
window.addEventListener('popstate', handlePopstate);
Router.events.on('routeChangeStart', handleRouteChangeStart);
I have also tried to set the value to the options key that nextjs sets but I have inconsistent results with that as well meaning sometimes the value isn't set and it feels a little hacky.
Does NextJs really not allow you to have any kind of interaction with browser history or the ability to manipulate the window.history object?

React Router v6.0.0-alpha.5 history prop removal - navigating outside of react context

According to the latest v6.0.0-alpha.5 release of react router, the history prop has been removed:
https://github.com/ReactTraining/react-router/releases/tag/v6.0.0-alpha.5
Removed the <Router history> prop and moved responsibility for setting
up/tearing down the listener (history.listen) into the wrapper
components (<BrowserRouter>, <HashRouter>, etc.). <Router> is now a
controlled component that just sets up context for the rest of the
app.
Navigating within the react context is simple with the useNavigate hook.
But, how does the removal of the history prop affect programmatically navigating outside of the react context?
For example, how would we keep our history in sync in order to navigate from inside redux, or an axios/http interceptor, etc., when we no longer can pass the history object?
Current V5 implementation:
https://reacttraining.com/react-router/web/api/Router
Or, from v6 onwards is the goal to rely on navigating from within react components only?
Thanks for the question, we know this is going to come up a lot. This is a common question we've gotten for years. Please be patient with us as we begin documenting all of these kinds of things, there's a lot to do!
Short answer: Typically people use thunks for async work that leads to wanting to navigate somewhere else (after a login, after a record is created, etc.). When your thunk is successful, change the state to something like "success" or "redirect" and then useEffect + navigate:
export function AuthForm() {
const auth = useAppSelector(selectAuth);
const dispatch = useAppDispatch();
const navigate = useNavigate();
useEffect(() => {
if (auth.status === "success") {
navigate("/dashboard", { replace: true });
}
}, [auth.status, navigate]);
return (
<div>
<button
disabled={auth.status === "loading"}
onClick={() => dispatch(login())}
>
{auth.status === "idle"
? "Sign in"
: auth.status === "loading"
? "Signing in..."
: null}
</button>
</div>
);
}
A bit more explanation:
For example, how would we keep our history in sync in order to navigate from inside redux
We've always considered this bad practice and reluctantly provided the history objects as first-class API to stop having philosophical conversations about app state and the URL 😅.
But today things are a bit different. The conversation isn't just philosophical anymore but has some concrete bugs when mixed with React's recent async rendering, streaming, and suspense features. To protect react router apps from synchronization bugs with the URL (that developers can't do anything about), v6 no longer exposes the history object.
Hopefully this explanation will help:
Changing the URL is a side-effect, not state. Thunks are used to perform side-effects that eventually figure out some state for the state container but aren't used for the side-effect in and of itself (at least that's my understanding).
For example, you may want to change the focus on the page after your redux state changes. You probably wouldn't try to synchronize and control the document's focus at all times through redux actions and state. Scroll position is the same. Ultimately the user is in control of these things: they can hit the tab key, click on something, or scroll around. Your app doesn't try to own or synchronize that state, it just changes it from time to time in response to actions and state that you do control.
The URL is the same. Users can type whatever they want into the address bar, click back, forward, or even click and hold the back button to go 3 entries back! It's the same kind of state as focus and scroll positions: owned by the user. The container can't ever truly own the URL state because it can't control the actions surrounding it. Mix in React's new and upcoming features and you're gonna lose that game.
In a nutshell: Change redux state > useEffect in the UI > navigate. Hope that helps!
I solved this for my login redirect by creating a navigate hook in my LoginForm UI component, then passing it to my login action creator, and calling it when the login endpoint returns successfully. In other words...
import React from 'react'
import {useNavigate} from 'react-router-dom'
function LoginForm (props) {
// create your navigate hook in your UI component
const navigate = useNavigate()
// other stuff...
handleLogin (loginFields) {
// then pass it into your action file, and call it
// when the query returns a successful result
dispatch(login(loginFields, navigate))
}
// other stuff, and return statement...
}

Next JS: Right way to fetch client-side only data in a Functional Component

I have a functional component. Basically, the page consists of a form - where I need to populate some existing data into the form and let the user update it.
I'm using a hook that I wrote to handle the forms. What it does is this
const [about, aboutInput] = useInput({
type: 'textarea',
name: 'About you',
placeholder: 'A few words about you',
initialValue: data && data.currentUser.about,
})
about is the value and aboutInput is the actual input element itself. I can pass an initialValue to it too.
At the beginning of the component I'm fetching the data like so:
const { data, loading, error } = useQuery(GET_CURRENT_USER_QUERY)
This only is executed on the client side and on the server side data is undefined.
Hence this code only works when I navigate to the page through a Link component from another client-side page.
It doesn't work for:
When I go the URL directly
When I navigate to this page from another SSR page(which uses getInitailProps)
I don't want to use lifecycle methods/class component(since I'm using hooks, and want to keep using the functional component.
Is there a nice way to achieve this in Next JS and keep using functional component?
You can fetch client-side only data using the useEffect Hook.
Import it first from react
import { useEffect } from 'react';
Usage in the component looks like follows
useEffect(() => {
return () => {
// clean-up functions
}
}, []);
The first argument Is a function and you can make your API calls inside this.
The second argument to the useEffect will determine when the useEffect should be triggered. If you pass an empty array [ ], then the useEffect will only be fired when the component Mounts. If you want the useEffect to fire if any props change then pass such props as a dependency to the array.
If you want GET_CURRENT_USER_QUERY from the query string you can pass the argument from the getInitailProps and read this as props in the useEffect array dependency.
I see you mentioned getInitailProps. So you probably know what it does and its caveats. If you want the data to be defined on server side, why not use getInitailProps on the page which the form is in. That way you can retrieve the data from props and pass it to your form component. It should work either directly visiting the url or visiting from your other pages.

Is there a way to store part of application state in URL query with React+Redux?

I'm using React, Redux and React Router. I want to store most of application state in redux store, and some bits of it in URL query. I believe there's two ways of doing that:
syncing redux store with URL query after each state change
don't store "some bits" in redux store, instead have some way (similar to how reducers work) to change URL query as if it was another store
I'm not sure if first way is even possible, since serialized state length may exceed URL limit. So I probably should go with the second.
For example, I have several checkboxes on a page and I want their state (checked/unchecked) to be mirrored in URL, so I could send my URL link to somebody else and they would have the same checkboxes checked as I do. So I render my checkboxes like that:
export class MyComponent extends React.Component {
handleCheckboxClick(...) {
// Some magic function, that puts my urlState into URL query.
updateUrlState(...)
}
render() {
// 'location' prop is injected by react router.
const urlState = getUrlState(this.props.location.query);
// Let's say 'checkboxes' are stored like so:
// {
// foo: false,
// bar: true,
// }
const { checkboxes } = urlState;
return (
<div>
{ checkboxes.map(
(checkboxName) => <Checkbox checked={ checkboxes[checkboxName] } onClick={ this.handleCheckboxClick }>{ checkboxName }</Checkbox>
)}
</div>
)
}
}
What I want magic updateUrlState function to do is get current URL query, update it (mark some checkbox as checked) and push results back to URL query.
Since this improvised URL state could be nested and complex, it probably should be serialised and stored as JSON, so resulting URL would look somewhat like that: https://example.com/#/page?checkboxes="{"foo":false,"bar":true}".
Is that possible?
Refer to this
Modify the URL without reloading the page
You can simply dispatch an action when the checkbox is checked/unchecked that will invoke your magic url function to change the url using window.history.pushState()
function changeUrl() {
isChecked = state.get('checkBox');
window.history.pushState(state, '', 'isChecked=' + isChecked)
}
The first argument above is for when user clicks the back button, the browser will emit popstate event, and you can use the state to set your ui. See https://developer.mozilla.org/en-US/docs/Web/API/History_API
Then when the other user paste the url, you would want to just parse the url and set the correct state in the store in componentDidMount.
function decodeUrl() {
let params = window.location.href.replace(baseUrl, '');
// parse your params and set state
}
FYI: URL does not support certain characters (e.g. '{'). So you will want to parse them into query params, or encode them. Try this tool http://www.urlencoder.org/
If I am getting your issue right, than redux middleware can really help you here. Middleware is a bit like a reduces, but sits in between the flow from the action to the reducer. Best example for such a use case is logging and I have developed middleware, which handles all remote requests. Basically you get the dispatched action in the middleware, there you can analyze it. In case of an action which should change the query parameter of the applycation, you can dispatch new actions from there.
So your code could look like this:
const queryChanges = store => next => action => {
if (/*here you test the action*/) {
//here can change the url, or
return dispatch(actionToTriggerURLChange());
} else {
return next(action);
}
}
In case you neither call return next(action), or dispatch a new one, just nothing will happen, so keep that in mind.
Have a look at the docs and figure out all the workings of middleware works and I guess this approach will be the most »non-repetitive« in this case.

Categories

Resources