How to listen to query param change event in react-router v5 - react-router

I'd like to be able to listen to query param change events, preferably via a hook, but anything would be nice. I can't find anything that suggests it's even possible with react-router, so other suggestions without it are welcome too.

There's nothing in react-router-dom#5 that directly does this, so you'd need to implement this yourself. You can use the useLocation hook to access the location.search value to create a URLSearchParams object, then a useEffect hook to issue the side-effect based on any specific queryString parameter updating.
Example:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
...
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const param = searchParams.get("param");
useEffect(() => {
// issue side-effect
}, [param]);
For RRDv5 there is this recipe to abstract the access of the query params:
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
const useQuery = () => {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
};
...
import { useEffect } from 'react';
import { useQuery } from '../path/to/hooks';
...
const searchParams = useQuery();
const param = searchParams.get("param");
useEffect(() => {
// issue side-effect
}, [param]);
You can use useQuery and the useEffect hook to create another custom hook.
import { useEffect } from 'react';
import { useQuery } from '.';
const useQueryParam = (paramKey, cb) => {
const searchParams = useQuery();
const param = searchParams.get(paramKey);
useEffect(() => {
if (param) {
cb(param);
}
}, [param]);
};
...
import { useQueryParam } from '../path/to/hooks';
...
useQueryParam(
"myParameter",
(paramValue) => {
// do something with "myParameter" param value
},
);

did you tried below
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function MyComponent() {
const location = useLocation();
useEffect(() => {
console.log('Location changed');
}, [location]);
...
}

Related

React : i have following code, i am using setInterval to change text value time to time ,when i switch pages and come back text is changing reallyfast

I assume when component is rendered multiple times something happens to setinterval,but how can i fix this.
bottom code is for Store that i am using and i don't understand.someone said that i must have useffect outside component but then it gives me error.
Anyways im new to react so i need help ,everyones appriciated.Thanks.
import SmallLogo from '../img/logo.svg';
import StarskyText from '../img/starskyproject.svg';
import './Statement.css'
import { BrowserRouter as Router,Routes,Route,Link } from "react-router-dom";
import { getElementError } from '#testing-library/react';
import react, { useRef , useState, useEffect } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { store } from "./appReducer";
function TempText(props) {
return <span className="yellow changetext"> {props.body} </span>;
}
function doUpdate(callback) {
setInterval(callback, 1300);
}
export default function Statement(){
const dispatch = useDispatch();
const textOptions = ["NFT", "CRYPTO", "METAVERSE", "WEB3"];
const tempText = useSelector((state) => state.tempText);
function change() {
let state = store.getState();
const index = state.index;
console.log(index);
console.log(textOptions[index]);
dispatch({
type: "updatetext",
payload: textOptions[index]
});
let newIndex = index + 1 >= textOptions.length ? 0 : index + 1;
dispatch({
type: "updateindex",
payload: newIndex
});
}
useEffect(() => {
doUpdate(change);
}, []);
var [dropdownOpen , Setdrop] = useState(false);
return(
<div>
<Link to="/">
<img className='star-fixed' alt='starlogo' src={SmallLogo}></img>
</Link>
<img className='starsky-fixed' alt='starsky-project' src={StarskyText}></img>
<div className='text-content'>
<span className='statement-text'>WEB3 IS NOT ONLY THE FUTURE.
IT’S THE ONLY FUTURE!</span>
<span className='starsk-link'>starsk.pro</span>
</div>
<div className='text-content-bottom'>
<span className='statement-text-bottom'>CREATE YOUR NEXT
<TempText body={tempText} />
<span className='flex'> PROJECT WITH
<Dropdown className="hover-drop-out" onMouseOver={() => Setdrop(dropdownOpen=true) } onMouseLeave={() => Setdrop(dropdownOpen=false)} isOpen={dropdownOpen} toggle={() => Setdrop(dropdownOpen = !dropdownOpen) }>
<DropdownToggle className='hover-drop'> STRSK.PRO </DropdownToggle>
<DropdownMenu> </DropdownMenu>
</Dropdown> </span>
</span>
</div>
</div>
)
}
import { createStore } from "redux";
const initialState = {
tempText: "NFT",
index: 1
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "updatetext":
return {
...state,
tempText: action.payload
};
case "updateindex":
return {
...state,
index: action.payload
};
default:
return state;
}
};
export const store = createStore(reducer);
You can clear your timer by calling clearTimeout function with a reference to your timer when your component unmounting.
useEffect(() => {
const timer = setInterval(change, 1300);
// in order to clear your timeout
return () => clearTimeout(timer);
}, [])

React-router-dom params undefined in nested route after using link

I need a switch component to have access to route params. The switch is rendered in one of the routes but its also rendered outside of it. Is there a way to get the same params in the component rendered outside of the route? Thanks for the help in advance!
It's usually a good pattern to not directly pass params through the route and keep those simple with the view component. You can use useContext, and then have each component(route) plug into that state using the useContext hook in the component.
for example...
app.js
import { useState } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { Routes } from "./auth/routes.js";
import { GlobalContext } from './globals/GlobalContext.js';
const App = () => {
// variables
const [someState, setSomeState] = useState('hello world');
// render
return (
<div>
<GlobalContext.Provider value={{someState, setSomeState}}>
<Router children={Routes} basename={process.env.REACT_APP_PUBLIC_URL} />
</GlobalContext.Provider>
</div>
);
}
GlobalContext.js
import { createContext } from 'react';
export const GlobalContext = createContext("");
routes.js
import { Route, Switch } from "react-router-dom";
// views
import ViewOne from '../views/ViewOne.js';
import ViewTwo from '../views/ViewTwo.js';
// globals
import { frontendLinks } from '../globals/index.js';
export const Routes = (
<Switch>
<Route exact path={frontendLinks.viewOne} component={ViewOne}></Route>
<Route exact path={frontendLinks.viewTwo} component={ViewTwo}></Route>
</Switch>
);
now the views...
import { useContext } from 'react';
// globals
import { GlobalContext } from '../globals/GlobalContext.js';
const ViewOne = () => {
const { someState } = useContext(GlobalContext);
return (
<div>
<h1>{someState}<h1>
</div>
)
}
export default ViewOne;
and
import { useContext } from 'react';
// globals
import { GlobalContext } from '../globals/GlobalContext.js';
const ViewTwo = () => {
const { someState } = useContext(GlobalContext);
return (
<div>
<h1>{someState}<h1>
</div>
)
}
export default ViewTwo;
If you don't want to manage shared state in your app.js file, I suggest you check out this video for managing useContext state in different files > https://www.youtube.com/watch?v=52W__dKdNnU

Ionic Route replace Url without page animation

Im about to setup a product Page with ionic-react and #ionic-react-router.
The products have different variants, soo I created a optional url parameter to enable smooth sharing.
My route is defined like this:
product: {
component: ProductPage,
background: '/images/dashboard-navigation-productOverview.png',
key: 'PRODUCT',
path: '/product/:id/:variantId?'
},
My ProductPage Component looks like this:
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { IonPage, IonContent } from '#ionic/react';
....
const ProductPage = ({match}) => {
const [variants, setVariants] = useState([]);
const [source, setSource] = useState(null);
const [variant, setVariant] = useState(null);
useEffect(() => {
(async () => {
const response = await productService.getProduct(parseInt(match.params.id, 10))
setSource(response);
})();
}, [match.params.id]);
useEffect(() => {
const variants = productHelpers.allVariantsFromSource(source);
if (match.params.variantId) {
const variant = productHelpers.getProductVariationById(variants, parseInt(match.params.variantId, 10));
setVariant(variant || variants[0]);
} else {
// 0 is always main detail
setVariant(variants[0]);
}
setVariants(variants);
}, [source, match.params.variantId]);
return <IonPage>
<Toolbar title={(source) ? source.name : ''}/>
<IonContent>
<Product source={source} variant={variant} variants={variants}/>
</IonContent>
</IonPage>
}
export default ProductPage;
And somewhere in product I want to change the variation with replacing the url.
So the user can use the back button, to get back where he comes from (mostly product list).
import React, {useContext} from "react";
import {IonButton, NavContext} from '#ionic/react';
const ProductVariant = ({source, newVariant}) => {
const {navigate} = useContext(NavContext);
return (
<IonButton
onClick={() => navigate(`/product/${source.id}/${newVariant.id}`, 'none', 'replace')}>{newVariant.name}</IonButton>
);
};
My Problem
The page URL is updating like I want to, The back function is working too.
But the page transition is still happening.
What i'm doing wrong??

In React, is it possible to store a ref in a context?

I need global app-wide access to a VideoElement to play it on user events on browsers like Safari and was wondering if storing the VideoElement in a context would be the best way to do that. I programmatically play my video through a redux action and in Safari that is not possible unless it has been played once through a user triggered event (like a click)
Is it possible to store an element (ref) within a context? The VideoElement will be then rendered within the component which I want to have my video, and then other components will also have access to the context and be able to call functions such as usePlayVideo that based on the context's state, will either call videoElement.play() if this is the first time the video is being played, or dispatch the redux action to play the video programmatically otherwise
It is possible to store a ref into context! You need to create a context at first. Then you need to pass value to the context provider and create a ref object using useRef hook. After that, you pass the ref into the value.
Now, You have a ref object sharing between components under the context provider and if you want to retrieve or pass a new ref, you could use useContext hook to deal with it.
Here is the demo (codesandbox).
Here is the sample code.
import { createContext, useContext, useEffect, useRef, useState } from "react";
import "./styles.css";
const MyContext = createContext();
export const ContextStore = (props) => {
const ref = useRef();
return <MyContext.Provider value={ref}>{props.children}</MyContext.Provider>;
};
export default function App() {
return (
<>
<ContextStore>
<MyComponent />
<MyComponent2 />
</ContextStore>
</>
);
}
const MyComponent = () => {
const myContext = useContext(MyContext);
return (
<div className="App" ref={myContext}>
<h1>Hello MyComponent1</h1>
</div>
);
};
const MyComponent2 = () => {
const myContext = useContext(MyContext);
const [divRef, setDivRef] = useState();
useEffect(() => {
setDivRef(myContext);
}, [myContext]);
return (
<div className="App">
<h1>{divRef?.current && divRef.current.innerText}</h1>
</div>
);
};
You can use this approach:
VideoContext.js
import { createContext, createRef, useContext } from "react";
const VideoContext = createContext();
const videoRef = createRef();
export const VideoContextProvider = (props) => {
return (
<VideoContext.Provider value={videoRef}>
{props.children}
</VideoContext.Provider>
);
};
export const useVideoContext = () => useContext(VideoContext);
and App.js for example:
import { useState, useEffect } from "react";
import { useVideoContext, VideoContextProvider } from "./VideoContext";
const SomeComponent = () => {
const videoRef = useVideoContext();
return (
<div ref={videoRef}>
<h1>Hey</h1>
</div>
);
};
const SomeOtherComponent = () => {
const [ref, setRef] = useState();
const videoRef = useVideoContext();
useEffect(() => {
setRef(videoRef);
}, [videoRef]);
return (
<div>
<h1>{ref?.current?.innerText}</h1>
</div>
);
};
export default function App() {
return (
<>
<VideoContextProvider>
<SomeComponent />
</VideoContextProvider>
{/* ... */}
{/* Some other component in another part of the tree */}
<VideoContextProvider>
<SomeOtherComponent />
</VideoContextProvider>
</>
);
}
code sandbox
Why not? I'll say. Let's see if we can setup an example.
const fns = {}
const addDispatch = (name, fn) => { fns[name] = fn }
const dispatch = (name) => { fns[name] && fns[name]() }
const RefContext = createContext({ addDispatch, dispatch })
export default RefContext
const Child1 = () => {
const [video, dispatchVideo] = useState(...)
const { addDispatch } = useContext(RefContext)
useEffect(() => {
addDispatch('video', dispatchVideo)
}, [])
}
const Child2 = () => {
const { dispatch } = useContext(RefContext)
const onClick = () => { dispatch('video') }
...
}
The above two childs do not have to share the same ancestor.
I didn't use ref the way you wanted, but i think you can pass your ref to one of the function. This is a very basic idea. I haven't tested it yet. But seems it could work. A bit
I used this approach:
first I creacted the context and ContextProvider;
import React, { useRef } from "react";
export const ScrollContext = React.createContext();
const ScrollContextProvider = (props) => {
return (
<ScrollContext.Provider
value={{
productsRef: useRef(),
}}
>
{props.children}
</ScrollContext.Provider>
);
};
export default ScrollContextProvider;
then Added my provider in my index.js:
root.render(
<React.StrictMode>
<ScrollContextProvider>
<App />
</ScrollContextProvider>
</React.StrictMode>
);
after that I used my context where I needed it:
import React, { useContext } from "react";
import { ScrollContext } from "../../store/scroll-context";
const Products = () => {
const scrollCtx = useContext(ScrollContext);
return (
<section ref={scrollCtx.productsRef}>
// your code...
</section>
);
};
In my case I wanted to to scroll to the above component clicking a button from a different component:
import React, { useContext } from "react";
import { ScrollContext } from "../../store/scroll-context";
function Header() {
const scrollCtx = useContext(ScrollContext);
const scrollTo = () => {
setTimeout(() => {
scrollCtx.productsRef.current.scrollIntoView({ behavior: "smooth" });
}, 0);
};
return (
<header>
//your code ...
<button alt="A table with chair" onClick={scrollTo}>Order Now<button />
</header>
);
}
No. It's not possible to use Ref on context api. React ref is considered to be used on rendering element.
What you're looking for is to forward the ref, so that you can consume them wherever you want.

Using Jest to test a Link from react-router v4

I'm using jest to test a component with a <Link> from react-router v4.
I get a warning that <Link /> requires the context from a react-router <Router /> component.
How can I mock or provide a router context in my test? (Basically how do I resolve this warning?)
Link.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import { Link } from 'react-router-dom';
test('Link matches snapshot', () => {
const component = renderer.create(
<Link to="#" />
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
The warning when the test is run:
Warning: Failed context type: The context `router` is marked
as required in `Link`, but its value is `undefined`.
You can wrap your component in the test with the StaticRouter to get the router context into your component:
import React from 'react';
import renderer from 'react-test-renderer';
import { Link } from 'react-router-dom';
import { StaticRouter } from 'react-router'
test('Link matches snapshot', () => {
const component = renderer.create(
<StaticRouter location="someLocation" context={context}>
<Link to="#" />
</StaticRouter>
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
Have a look at the react router docs about testing
I had the same issue and using StaticRouter would still require the context which needed more configuration to have it available in my test, so I ended up using the MemoryRouter which worked very well and without any issues.
import React from 'react';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
// SampleComponent imports Link internally
import SampleComponent from '../SampleComponent';
describe('SampleComponent', () => {
test('should render', () => {
const component = renderer
.create(
<MemoryRouter>
<SampleComponent />
</MemoryRouter>
)
.toJSON();
expect(component).toMatchSnapshot();
});
});
The answer of #Mahdi worked for me! In 2023 if you want to test a component that includes <Link> or <NavLink>, we just need to wrap it with the <MemoryRouter> in the test file:
// App.test.js
import { render, screen } from "#testing-library/react";
import MyComponent from "./components/MyComponent";
import { MemoryRouter } from "react-router-dom"; // <-- Import MemoryRouter
test("My test description", () => {
render(
<MemoryRouter> // <-- Wrap!
<MyComponent />
</MemoryRouter>
);
});
my test like this:
import * as React from 'react'
import DataBaseAccout from '../database-account/database-account.component'
import { mount } from 'enzyme'
import { expect } from 'chai'
import { createStore } from 'redux'
import reducers from '../../../reducer/reducer'
import { MemoryRouter } from 'react-router'
let store = createStore(reducers)
describe('mount database-account', () => {
let wrapper
beforeEach(() => {
wrapper = mount(
< MemoryRouter >
<DataBaseAccout store={store} />
</MemoryRouter >
)
})
afterEach(() => {
wrapper.unmount()
wrapper = null
})
})
but I don't konw why MemoryRouter can solve this。
Above solutions have a common default defact:
Can't access your component's instance! Because the MemoryRouter or StaticRouter component wrapped your component.
So the best to solve this problem is mock a router context, code as follows:
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
describe('YourComponent', () => {
test('test component with react router', () => {
// mock react-router context to avoid violation error
const context = {
childContextTypes: {
router: () => void 0,
},
context: {
router: {
history: createMemoryHistory(),
route: {
location: {
hash: '',
pathname: '',
search: '',
state: '',
},
match: { params: {}, isExact: false, path: '', url: '' },
}
}
}
};
// mount component with router context and get component's instance
const wrapper = mount(<YourComponent/>, context);
// access your component as you wish
console.log(wrapper.props(), wrapper.state())
});
beforeAll(() => {
configure({ adapter: new Adapter() });
});
});