How to remove query param with react hooks? - reactjs

I know we can replace query params in component based classes doing something along the lines of:
componentDidMount() {
const { location, replace } = this.props;
const queryParams = new URLSearchParams(location.search);
if (queryParams.has('error')) {
this.setError(
'There was a problem.'
);
queryParams.delete('error');
replace({
search: queryParams.toString(),
});
}
}
Is there a way to do it with react hooks in a functional component?

For React Router V6 and above, see the answer below.
Original Answer:
Yes, you can use useHistory & useLocation hooks from react-router:
import React, { useState, useEffect } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
export default function Foo() {
const [error, setError] = useState('')
const location = useLocation()
const history = useHistory()
useEffect(() => {
const queryParams = new URLSearchParams(location.search)
if (queryParams.has('error')) {
setError('There was a problem.')
queryParams.delete('error')
history.replace({
search: queryParams.toString(),
})
}
}, [])
return (
<>Component</>
)
}
As useHistory() returns history object which has replace function which can be used to replace the current entry on the history stack.
And useLocation() returns location object which has search property containing the URL query string e.g. ?error=occurred&foo=bar" which can be converted into object using URLSearchParams API (which is not supported in IE).

Use useSearchParams hook.
import {useSearchParams} from 'react-router-dom';
export const App =() => {
const [searchParams, setSearchParams] = useSearchParams();
const removeErrorParam = () => {
if (searchParams.has('error')) {
searchParams.delete('error');
setSearchParams(searchParams);
}
}
return <button onClick={removeErrorParam}>Remove error param</button>
}

Related

mapStateToProps react router dom v6 useParams()

BlogDetailsPage.js
import { connect } from "react-redux";
import { useParams } from "react-router-dom";
const BlogDetailsPage = (props) => {
const { id } = useParams();
return <div>Blog Details: {}</div>;
};
const mapStateToProps = (state, props) => {
const { id } = useParams();
return {
blog: state.blogs.find((blog) => {
return blog.id === id;
}),
};
};
export default connect(mapStateToProps)(BlogDetailsPage);
How to use mapStateToProps in "useParams()" react-router-dom ?
and whatever links that navigate to /slug path are ended up in BlogDetailsPage.js, Since BlogDetailsPage.js is being nested nowhere else so i couldn't get specific props pass down but route params. From my perspective this is completely wrong but i couldn't figure out a better way to do it.
Compiled with problems:X
ERROR
src\components\BlogDetailsPage.js
Line 11:18: React Hook "useParams" is called in function "mapStateToProps" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks
Search for the keywords to learn more about each error.```
Issue
React hooks can only be called from React function components or custom React hooks. Here it is being called in a regular Javascript function that is neither a React component or custom hook.
Solutions
Preferred
The preferred method would be to use the React hooks directly in the component. Instead of using the connect Higher Order Component use the useSelector hook to select/access the state.blogs array.
Example:
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
const BlogDetailsPage = () => {
const { id } = useParams();
const blog = useSelector(state => state.blogs.find(
blog => String(blog.id) === id
));
return <div>Blog Details: {}</div>;
};
export default BlogDetailsPage;
Alternative/Legacy
If you have the need to access path params in any mapStateToProps function, if you are using a lot of oder code for example, then you'll need to create another HOC to access the path params and have them injected as props so they are available in the mapStateToProps function.
Example:
import { useParams, /* other hooks */ } from "react-router-dom";
const withRouter = Component => props => {
const params = useParams();
// other hooks, useLocation, useNavigate, etc..
return <Component {...props} {...{ params, /* other injected props */ }} />;
};
export default withRouter;
...
import { compose } from 'redux';
import { connect } from 'react-redux';
import withRouter from '../path/to/withRouter';
const BlogDetailsPage = ({ blog }) => {
return <div>Blog Details: {}</div>;
};
const mapStateToProps = (state, { params }) => {
const { id } = params || {};
return {
blog: state.blogs.find((blog) => {
return String(blog.id) === id;
}),
};
};
export default compose(
withRouter, // <-- injects a params prop
connect(mapStateToProps) // <-- props.params accessible
)(BlogDetailsPage);
I think, react hook functions are allowed to use inside of react component.
Outside of react components, it's not allowed to use react api hook functions.
Thanks, I'd liked to help you my answer.

match.params.id not working in react router dom

I have installed the latest version of react-router-dom in my project. And it is saying match is undefined. However, it works fine when I use react-router-dom#5.2.0.
Can anybody tell me how should i change my code to use the match.params.id or what is the substitute method in the latest react-router-dom??
Attached is my code which is working fine in react router dom version#5.2.0.
import React from 'react';
import { useState, useEffect } from 'react';
function ItemDetail({match}) {
const [item, setItem]= useState({});
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
useEffect(()=>{
fetchItem();
//console.log(match);
// eslint-disable-next-line react-hooks/exhaustive-deps
},[setItem]);
const fetchItem= async ()=>{
setIsLoading(true);
setHasError(false);
try {
const fetchItem= await fetch(`https://fortnite-api.theapinetwork.com/item/get?id=${match.params.id}`
);
const item1 = await fetchItem.json();
setItem(item1.data.item);
console.log(item);
console.log(item1);
} catch (error) {
setHasError(true);
}
setIsLoading(false);
}
return (
<div >
<h1>{item.name}</h1>
</div>
);
}
export default ItemDetail;
First import { useParams } from 'react-router-dom';
add const {id} = useParams();
And instead of using (match.params.id) use only (id)
Hope this will work!
You can try to use hooks: useParams, that returns an object with all variables inside your route
const params = useParams();
if the problem persists then it may be a problem with your component hierarchy or route definition

How do I pass a URL Param to a selector

I recive a url param from useParams. I want to pass it to a selector using mapStateToProps.
collection.component.jsx
import { useParams } from "react-router-dom";
import { connect } from "react-redux";
import { selectShopCollection } from "../../redux/shop/shop.selectors";
import './collection.styles.scss'
const Collection = ({ collection }) => {
const { collectionId } = useParams();
console.log(collection)
return (
<div>
<h1>{collection}</h1>
</div>
)
}
const mapStateToProps = (state, ownProps) => ({
collection: selectShopCollection(ownProps.match.params.collectionId)(state)
})
export default connect(mapStateToProps)(Collection);
shop.selectors.js
import { createSelector } from "reselect"
const selectShop = state => state.shop
export const selectShopCollections = createSelector([selectShop], shop =>
shop.collections
)
export const selectShopCollection = collectionUrlParam =>
createSelector([selectShopCollections], collections =>
collections.find(collection => collection.id === collectionUrlParam)
)
I guess the problem is that, I cannot pass params using match as react-router-dom v6 does not pass it in props. Is there any other way to pass collectionId to the selector selectShopCollection?
Since Collection is a function component I suggest importing the useSelector hook from react-redux so you can pass the collectionId match param directly. It simplifies the component API. reselect selectors work well with the useSelector hook.
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectShopCollection } from "../../redux/shop/shop.selectors";
import './collection.styles.scss'
const Collection = () => {
const { collectionId } = useParams();
const collection = useSelector(selectShopCollection(collectionId));
console.log(collection);
return (
<div>
<h1>{collection}</h1>
</div>
)
};
export default Collection;
Collection component can be given props by withRouter. But it was deprecated with react-router v6. Hence we need to create our own HOC which wrap our component.
I created a HOC like this:
import { useParams } from "react-router-dom"
const withRouter = WrappedComponent => props => {
const params = useParams()
return (
<WrappedComponent {...props} params={params} />
)
}
export default withRouter;
See this answer for How to get parameter value from react-router-dom v6 in class to see why this HOC was made.
And, we can import the withRouter to the component and use with connect inside compose. Read more on compose. It just returns final function obtained by composing the given functions from right to left.
const mapStateToProps = (state, ownProps) => ({
collection: selectShopCollection(ownProps.params.collectionId)(state)
})
export default compose(withRouter, connect(mapStateToProps))(Collection)

React router v6 history.listen

In React Router v5 there was a listen mehtode on the history object.
With the method I wrote a usePathname hook to rerender a component on a path change.
In React Router v6, this method no longer exists. Is there an alternative for something like this? I would hate to use useLocation because it also renders if the state changes, which I don't need in this case.
The hook is used with v5.
import React from "react";
import { useHistory } from "react-router";
export function usePathname(): string {
let [state, setState] = React.useState<string>(window.location.pathname);
const history = useHistory();
React.useLayoutEffect(
() =>
history.listen((locationListener) => setState(locationListener.pathname)),
[history]
);
return state;
}
As mentioned above, useLocation can be used to perform side effects whenever the current location changes.
Here's a simple typescript implementation of my location change "listener" hook. Should help you get the result you're looking for
function useLocationEffect(callback: (location?: Location) => any) {
const location = useLocation();
useEffect(() => {
callback(location);
}, [location, callback]);
}
// usage:
useLocationEffect((location: Location) =>
console.log('changed to ' + location.pathname));
I am using now this code
import { BrowserHistory } from "history";
import React, { useContext } from "react";
import { UNSAFE_NavigationContext } from "react-router-dom";
export default function usePathname(): string {
let [state, setState] = React.useState<string>(window.location.pathname);
const navigation = useContext(UNSAFE_NavigationContext)
.navigator as BrowserHistory;
React.useLayoutEffect(() => {
if (navigation) {
navigation.listen((locationListener) =>
setState(locationListener.location.pathname)
);
}
}, [navigation]);
return state;
}
It seems to work fine
I find using useNavigate and useLocation quite meaningless compared to useHistory in React Rrouter v5.
As a result of these changes, I made a thin custom hook to ease myself from any refactoring.
Just rename the import path to this hook and use the "old" api with v6. To answer or just give hints to your question - using this approach is should be easy to implement the listen function in the custom hook yourself.
export function useHistory() {
const navigate = useNavigate();
const location = useLocation();
const listen = ...; // implement the hook yourself
return {
push: navigate,
go: navigate,
goBack: () => navigate(-1),
goForward: () => navigate(1),
listen,
location,
};
}
Why not simply use const { pathname } = useLocation();? It will indeed renders if the state changes but it shouldn't be a big deal in most scenarii.
If you REALLY want to avoid such behaviour, you could create a context of your own to hold the pathname:
// PathnameProvider.js
import React, { createContext, useContext } from 'react';
import { useLocation } from 'react-router';
const PathnameContext = createContext();
const PathnameProvider = ({ children }) => {
const { pathname } = useLocation();
return (
<PathnameContext.Provider value={pathname}>
{children}
</PathnameContext.Provider>
);
}
const usePathname = () => useContext(PathnameContext);
export { PathnameProvider as default, usePathname };
Then you can use usePathname() in any component down the tree. It will render only if the pathname actually changed.
Given that #kryštof-Řeháček's recommendation (just above) is to implement your own useListen hook, but it might not be obvious how to do that, here's a version I've implemented for myself as a guide (nb: I havent't exhaustively unit tested this yet):
import { useState } from "react";
import { useLocation } from "react-router";
interface HistoryProps {
index: number;
isHistoricRoute: boolean;
key: string;
previousKey: string | null;
}
export const useHistory = (): HistoryProps => {
const { key } = useLocation();
const [history, setHistory] = useState<string[]>([]);
const [currentKey, setCurrentKey] = useState<string | null>(null);
const [previousKey, setPreviousKey] = useState<string | null>(null);
const contemporaneousHistory = history.includes(key)
? history
: [...history, key];
const index = contemporaneousHistory.indexOf(key);
const isHistoricRoute = index + 1 < contemporaneousHistory.length;
const state = { index, isHistoricRoute, key, previousKey };
if (history !== contemporaneousHistory) setHistory(contemporaneousHistory);
if (key !== currentKey) {
setPreviousKey(currentKey);
setCurrentKey(key);
}
return state;
}
I now have just created a new routing library for react where this is possible.
https://github.com/fast-router/fast-router
Server-Side rendering is not supported. The rest should work fine. The library is mainly inspired by wouter -> https://github.com/molefrog/wouter
There are hooks for example usePathname which only cause a new render if the actual pathname changes (ignoring the hash and search)
It is possible to select just a single property of the history.state and don't get a new render if any other values inside the state changes.

React-router : conditionally redirect at render time

So I have this basic component <Redirectable />:
import React from 'react';
import {
useParams,
useHistory,
Redirect,
} from 'react-router-dom';
export default () => {
const history = useHistory();
const {id} = useParams();
if (!checkMyId(id) {
// invalid ID, go back home
history.push('/');
}
return <p>Hey {id}</p>
}
But I get the following error:
Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
I also tried: <Redirect push to="/" />, but same error.
What's the correct way to handle this? I read about onEnter callback at <Router /> level, but as far as I'm concerned, the check should happen at <Redirectable /> level.
There should be a solution, shouldn't it? I don't feel like I'm doing something completely anti-react-pattern, am I?
This seems to do the trick. I was not able to find any documentation as to why this occures. All I was able to find was different examples with callbacks but this solved it for me.
import React from 'react';
import {
useParams,
useHistory,
Redirect,
} from 'react-router-dom';
const MyComponent = () => {
const history = useHistory();
const {id} = useParams();
if (!checkMyId(id) {
// invalid ID, go back home
history.push('/');
}
return <p>Hey {id}</p>
}
export default MyComponent;
It seems that react may recognize export default () => { as a pure component and so side effects are prohibited.
Yes, it seems to me you are written the component in a anti pattern way. Can you please update like below:
const Rediractabke = () => {
const history = useHistory();
const {id} = useParams();
if (!checkMyId(id) {
// invalid ID, go back home
history.push('/');
}
return <p>Hey {id}</p>
}
export default as Redirectable;
#c0m1t was right, the solution was to use useEffect:
import React, {useEffect} from 'react';
import {
useParams,
useHistory,
Redirect,
} from 'react-router-dom';
export default () => {
const history = useHistory();
const {id} = useParams();
useEffect(() => {
if (!checkMyId(id) {
// invalid ID, go back home
history.push('/');
}
})
return <p>Hey {id}</p>
}

Resources