React router v6 history.listen - reactjs

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.

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.

React hooks useState getting diferrent value from redux state

I have react component look like this following code:
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useParams } from "react-router-dom";
import { createClient, getClients } from "../redux/actions/clients";
function UpdateClient(props) {
let params = useParams();
const { error, successSubmit, clients } = useSelector(
(state) => state.clients
);
const [client, setClient] = useState(clients[0]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getClients({ id: params.id }));
}, []);
const submitClient = () => {
dispatch(createClient(client));
};
return (
<div>{client.name} {clients[0].name}</div>
);
}
export default UpdateClient;
And the result is different client.name return test1,
while clients[0].name return correct data based on route parameter id (in this example parameter id value is 7) which is test7
I need the local state for temporary saving form data. I don't know .. why it's become different?
Can you please help me guys? Thanks in advance
You are referencing a stale state which is a copy of the clients state.
If you want to see an updated state you should use useEffect for that.
useEffect(() => {
setClient(clients[0]);
}, [clients]);
Notice that duplicating state is not recommended.
There should be a single “source of truth” for any data that changes in a React application.

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>
}

How to remove query param with react hooks?

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>
}

Can I replace context with hooks?

Is there a way with new react hooks API to replace a context data fetch?
If you need to load user profile and use it almost everywhere, first you create context and export it:
export const ProfileContext = React.createContext()
Then you import in top component, load data and use provider, like this:
import { ProfileContext } from 'src/shared/ProfileContext'
<ProfileContext.Provider
value={{ profile: profile, reloadProfile: reloadProfile }}
>
<Site />
</ProfileContext.Provider>
Then in some other components you import profile data like this:
import { ProfileContext } from 'src/shared/ProfileContext'
const context = useContext(profile);
But there is a way to export some function with hooks that will have state and share profile with any component that want to get data?
React provides a useContext hook to make use of Context, which has a signature like
const context = useContext(Context);
useContext accepts a context object (the value returned from
React.createContext) and returns the current context value, as given
by the nearest context provider for the given context.
When the provider updates, this Hook will trigger a rerender with the
latest context value.
You can make use of it in your component like
import { ProfileContext } from 'src/shared/ProfileContext'
const Site = () => {
const context = useContext(ProfileContext);
// make use of context values here
}
However if you want to make use of the same context in every component and don't want to import the ProfileContext everywhere you could simply write a custom hook like
import { ProfileContext } from 'src/shared/ProfileContext'
const useProfileContext = () => {
const context = useContext(ProfileContext);
return context;
}
and use it in the components like
const Site = () => {
const context = useProfileContext();
}
However as far a creating a hook which shares data among different component is concerned, Hooks have an instance of the data for them self and don'tshare it unless you make use of Context;
updated:
My previous answer was - You can use custom-hooks with useState for that purpose, but it was wrong because of this fact:
Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
The right answer how to do it with useContext() provided #ShubhamKhatri
Now i use it like this.
Contexts.js - all context export from one place
export { ClickEventContextProvider,ClickEventContext} from '../contexts/ClickEventContext'
export { PopupContextProvider, PopupContext } from '../contexts/PopupContext'
export { ThemeContextProvider, ThemeContext } from '../contexts/ThemeContext'
export { ProfileContextProvider, ProfileContext } from '../contexts/ProfileContext'
export { WindowSizeContextProvider, WindowSizeContext } from '../contexts/WindowSizeContext'
ClickEventContext.js - one of context examples:
import React, { useState, useEffect } from 'react'
export const ClickEventContext = React.createContext(null)
export const ClickEventContextProvider = props => {
const [clickEvent, clickEventSet] = useState(false)
const handleClick = e => clickEventSet(e)
useEffect(() => {
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [])
return (
<ClickEventContext.Provider value={{ clickEvent }}>
{props.children}
</ClickEventContext.Provider>
)
}
import and use:
import React, { useContext, useEffect } from 'react'
import { ClickEventContext } from 'shared/Contexts'
export function Modal({ show, children }) {
const { clickEvent } = useContext(ClickEventContext)
useEffect(() => {
console.log(clickEvent.target)
}, [clickEvent])
return <DivModal show={show}>{children}</DivModal>
}

Resources