Why doesn't react-router-dom useParams hook work within Context? - javascript

I've got a context shared amongst a group of profile pages. The context is responsible for loading and setting the user's profile from a database, like so:
const Profile = props => {
const { userProfile } = useContext(ProfileContext);
return userProfile && (
<div className="profile-container">
...stuff
</div>
);
};
export default Profile;
...routes:
<BrowserRouter>
<Header />
<main className="main-container">
<Switch>
...other routes
<ProfileContextProvider>
<Route path="/profile/:id" exact component={Profile} />
<Route path="/settings" exact component={Settings} />
</ProfileContextProvider>
</Switch>
</main>
<Footer />
</BrowserRouter>
The context itself is very simple:
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
export const ProfileContext = React.createContext({
userProfile: {},
setUserProfile: () => {}
});
ProfileContext.displayName = "ProfileContext";
const ProfileContextProvider = props => {
const { id } = useParams(); //always undefined!
const [userProfile, setUserProfile] = useState(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
...api call to load data
};
return (
<ProfileContext.Provider value={{
userProfile,
setUserProfile
}}>
{props.children}
</ProfileContext.Provider>
);
};
export default ProfileContextProvider;
However, that use of useParams() in there doesn't work. The "id" is always undefined. If I move this useParams() usage to the Profile component itself, it works fine. This does me no good, because I need to make a decision about the route, when loading data in the context, before I load all of Profile. I need it to work in the context!
Is this possible? Am I doing something wrong?

Context is itself different management and query param should props from the page to context set variable then it works

Your ProfileContextProvider is outside the route that defines the "id" param.... this cant work.

Related

React Router V6 Re-renders on Route Change

When I navigate back and forth between routes, React Router re-renders memoized routes causing useEffect(() => []) to re-run and data to re-fetch. I'd like to prevent that and instead keep existing routes around but hidden in the dom. I'm struggling with "how" though.
The following is sample code for the problem:
import React, { useEffect } from "react";
import { BrowserRouter as Router, Route, Routes, useNavigate } from "react-router-dom";
export default function App() {
return (
<Router>
<Routes>
<Route path={"/"} element={<MemoizedRouteA />} />
<Route path={"/b"} element={<MemoizedRouteB />} />
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router A");
}, []);
return (
<button onClick={() => { navigate('/b') }}>Go to B</button>
);
};
const MemoizedRouteA = React.memo(RouteA)
function RouteB() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router B");
}, []);
return (
<button onClick={() => { navigate('/') }}>Go to A</button>
);
}
const MemoizedRouteB = React.memo(RouteB)
Sandbox: https://codesandbox.io/s/wonderful-hertz-w9qoip?file=/src/App.js
With the above code, you'll see that the "alert" code is called whenever you tap a button or use the browser back button.
With there being so many changes of React Router over the years I'm struggling to find a solution for this.
When I navigate back and forth between routes, React Router re-renders
memoized routes causing useEffect(() => []) to re-run and data to
re-fetch. I'd like to prevent that and instead keep existing routes
around but hidden in the dom. I'm struggling with "how" though.
Long story short, you can't. React components rerender for one of three reasons:
Local component state is updated.
Passed prop values are updated.
The parent/ancestor component updates.
The reason using the memo HOC doesn't work here though is because the Routes component only matches and renders a single Route component's element prop at-a-time. Navigating from "/" to "/b" necessarily unmounts MemoizedRouteA and mounts MemoizedRouteB, and vice versa when navigating in reverse. This is exactly how RRD is intended to work. This is how the React component lifecycle is intended to work. Memoizing a component output can't do anything for when a component is being mounted/unmounted.
If what you are really trying to minimize/reduce/avoid is duplicate asynchronous calls and data fetching/refetching upon component mounting then what I'd suggest here is to apply the Lifting State Up pattern and move the state and useEffect call into a parent/ancestor.
Here's a trivial example using an Outlet component and its provided context, but the state could be provided by any other means such as a regular React context or Redux.
import React, { useEffect, useState } from "react";
import {
BrowserRouter as Router,
Route,
Routes,
Outlet,
useNavigate,
useOutletContext
} from "react-router-dom";
export default function App() {
const [users, setUsers] = useState(0);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then(setUsers);
}, []);
return (
<Router>
<Routes>
<Route element={<Outlet context={{ users }} />}>
<Route path={"/"} element={<RouteA />} />
<Route path={"/b"} element={<RouteB />} />
</Route>
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate("/b")}>Go to B</button>
</div>
);
}
function RouteB() {
const navigate = useNavigate();
const { users } = useOutletContext();
return (
<div>
<button onClick={() => navigate("/")}>Go to A</button>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} : {user.email}
</li>
))}
</ul>
</div>
);
}

Can't access redux actions from props when working with react router

I'm pretty new to react and redux and I have been having a problem accessing my redux actions from the props when used in conjunction with react router. I have tried lots of configurations for formatting but no matter what I try the props only have the react router functions i.e. history, match and location. I am using connected-react-router but it does not seems to be doing anything. I have been able to access the redux actions from my nav menu components so I don't think anything is wrong there.
Here is a sample of the latest configuration I tried:
const mapStateToProps = (state) => {
return { isSignedIn: state.auth.isSignedIn }
}
const ConnectedApp = connect(
mapStateToProps,
{ signOut, signIn }
)(App);
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter basename={baseUrl} history={history}>
<ConnectedApp />
</ConnectedRouter>
</Provider>,
rootElement);
and here is inside App:
class App extends Component {
static displayName = App.name;
render () {
return (
<Layout>
<Route exact path='/' component={(props) =><Home {...props}/>} />
<Route exact path='/LogInExisting' component={(props) => <LogInExisting {...props} />} />
<Route exact path='/LogInCreate' component={(props) => <LogInCreate {...props} />} />
</Layout>
);
}
}
And here is whats trying to access the signIn action:
const LogInExisting = (props) => {
const history = useHistory();
const handleSignIn = async (e) => {
e.preventDefault()
var data = {
UserName: document.getElementById('login').value,
Password: document.getElementById('password').value
}
var response = await fetch('LogInExistingUser', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
var respData = await response.json();
if (!respData.success) {
//TODO: Error handling
} else {
props.signIn();
history.push('/')
}
}
I feel like I am missing something obvious but I really could use some help.
You are passing the route props (history, location, and match) when using the Route component's component prop function.
<Route
exact
path='/LogInExisting'
component={(props) => <LogInExisting {...props} />}
/>
Missing is the passing through of the props that were passed to App.
<Route
exact
path='/LogInExisting'
component={(routeProps) => <LogInExisting {...routeProps} {...this.props} />}
/>
You don't want to use an anonymous function on the component prop as this will remount the routed component each time App renders. Instead, use the render prop function, it's meant for this use case. See Route render methods for more in-depth explanation.
<Route
exact
path='/LogInExisting'
render={(routeProps) => <LogInExisting {...routeProps} {...this.props} />}
/>
This being said though, you are using Redux, which is built using the React Context API, which solves the issue of "props drilling". You shouldn't be passing your redux state and actions down as props like this. Wrap your routed components in the connect HOC locally.
<Route exact path='/LogInExisting' component={LogInExisting} />
LogInExisting
const mapStateToProps = (state) => ({
isSignedIn: state.auth.isSignedIn,
});
const mapDispatchToProps = { signOut, signIn };
export default connect(mapStateToProps, mapDispatchToProps)(LogInExisting);
Since LogInExisting is a function component, you can use the useDispatch and useSelector hooks instead of the connect HOC.
import { useDispatch, useSelector } from 'react-redux';
import { signIn } from '../path/to/actions';
const LogInExisting = (props) => {
const dispatch = useDispatch();
const isSignedIn = useSelector(state => state.auth.isSignedIn);
const history = useHistory();
const handleSignIn = async (e) => {
e.preventDefault();
...
if (!respData.success) {
//TODO: Error handling
} else {
dispatch(signIn());
history.push('/');
}
}

React-router-dom Render props isn't returning any component

I'm using react-router-dom version 6.0.2 here and the "Render" props isn't working, every time I got to the url mentioned in the Path of my Route tag it keeps throwing me this error - "Matched leaf route at location "/addRecipe" does not have an element. This means it will render an with a null value by default resulting in an "empty" page.". Can someone please help me with this issue
import './App.css';
import Home from './components/Home';
import AddRecipe from './components/AddRecipe';
import items from './data';
import React, { useState } from 'react';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom';
const App = () => {
const [itemsList, setItemsList] = useState(items)
const addRecipe = (recipeToAdd) => {
setItemsList(itemsList.concat([recipeToAdd]));
}
const removeItem = (itemToRemove) => {
setItemsList(itemsList.filter(a => a!== itemToRemove))
}
return (
<Router>
<Routes>
<Route path="/addRecipe" render={ ({history}) => {
return (<AddRecipe onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
history.push('/');
} } />);
} } />
</Routes>
</Router>
);
}
export default App;
In react-router-dom version 6, you should use element prop for this.
I suggest your read their document on upgrading from version 5 where they explain the changes.
For your problem, you should write something like this:
<Route
path="/addRecipe"
element={
<AddRecipe
onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
history.push('/');
}
/>
}
/>
The Route component API changed significantly from version 5 to version 6, instead of component and render props there is a singular element prop that is passed a JSX literal instead of a reference to a React component (via component) or a function (via render).
There is also no longer route props (history, location, and match) and they are accessible only via the React hooks. On top of this RRDv6 also no longer surfaces the history object directly, instead abstracting it behind a navigate function, accessible via the useNavigate hook. If the AddRecipe component is a function component it should just access navigate directly from the hook. If it unable to do so then the solution is to create a wrapper component that can, and then render the AddRecipe component with the corrected onAddRecipe callback.
Example:
const AddRecipeWrapper = ({ addRecipe }) => {
const navigate = useNavigate();
return (
<AddRecipe
onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
navigate('/');
}}
/>
);
};
...
const App = () => {
const [itemsList, setItemsList] = useState(items);
const addRecipe = (recipeToAdd) => {
setItemsList(itemsList.concat([recipeToAdd]));
};
const removeItem = (itemToRemove) => {
setItemsList(itemsList.filter(a => a !== itemToRemove))
};
return (
<Router>
<Routes>
<Route
path="/addRecipe"
element={<AddRecipeWrapper addRecipe={addRecipe} />}
/>
</Routes>
</Router>
);
};

React JS - Redirect to login except home page

I'm learning to use React JS.
I have the following page.
Home
Login
Note
Create Note
My case is as follows.
Home can be accessed without logging in
Note and create notes cannot be accessed without logging in
How to make the case above work?
Here's the code snippet I made:
index.js
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
<BrowserRouter> // from "react-router-dom"
<App />
</BrowserRouter>,
document.getElementById("root")
);
serviceWorker.unregister();
App.js as entry home page
import React, { Component } from "react";
import AuthService from "./services/auth.service";
import Routes from "./config/routes";
// Lot of import bootstrap dan font-awesome and css
class App extends Component {
constructor(props) {
super(props);
this.logOut = this.logOut.bind(this);
this.state = {
currentUser: undefined,
backendSupportInformation: undefined,
};
}
componentDidMount() {
const user = AuthService.getCurrentUser();
if (user) {
this.setState({
currentUser: user,
backendSupportInformation: user.backend,
});
}
}
logOut() {
AuthService.logout();
}
render() {
const { currentUser, backendSupportInformation } = this.state;
return (
<div>
<header>
<nav className="navbar navbar-expand-sm navbar-dark bg-dark">
// some of link here
</nav>
</header>
<main role="main" className="container-fluid mt-3">
<Routes /> // use react-route-dom
</main>
</div>
);
}
}
export default App;
Routes.js
import React from "react";
import { Route, Switch } from "react-router-dom";
const Routes = () => {
return (
<Switch>
<Route exact path={["/", "/home"]} component={Home} />
<Route exact path="/login" component={Login} />
<Route exact path="/note" component={Note} />
<Route exact path="/note/create" component={NoteCreate} />
<Route exact path="/profile" component={Profile} />
</Switch>
);
};
export default Routes;
Now i am doing in NoteComponent like this.
NoteComponent
export default class Note extends Component {
state = {
redirect: null,
userReady: false,
};
componentDidMount() {
const currentUser = AuthService.getCurrentUser();
if (!currentUser) this.setState({ redirect: "/home" });
this.setState({ currentUser: currentUser, userReady: true });
this.retrieveAll();
}
render() {
if (this.state.redirect) {
// pass message that you need login first to access this note page
return <Redirect to={this.state.redirect} />;
}
}
I dont want to repeat my self into NoteCreate Component?
Any advice it so appreciated.
Just as a note to start, not sure which resources you're using to learn React, but as of now I would highly recommend you look into a modern course which teaches React with Hooks, aside from to get error boundaries (which with react-error-boundary) there is no reason to be writing class components.
Regarding the issue at hand, you didn't specifically mention any errors so this seems to be a question of "how should I go about this" as opposed to actually fixing something? Let me know if theres specific errors and I'll try to adjust my answer to help further.
I would recommend refactoring the logic you have in your Note component into a component of itself, so that you can wrap your routes with it. Store the information for whether they're authenticated into a context, and then wrap your routes with that context provider so you can consume that context in your child components, without duplicating that logic on each page.
You need to create a RouterWithAuth Component and use that instead of using Router directly, something like this:
export default class RouteWithAuth extends Component {
state = {
redirect: null,
userReady: false,
};
componentDidMount() {
const currentUser = AuthService.getCurrentUser();
if (!currentUser) this.setState({ redirect: "/home" });
this.setState({ currentUser: currentUser, userReady: true });
this.retrieveAll();
}
render() {
const { redirect, userReady } = this.state;
if (redirect) {
// pass message that you need login first to access this note page
return <Redirect to={this.state.redirect} />;
} else if (userReady) {
return (
<Route
exact={props.exact}
path={props.path}
component={props.component}
/>
);
} else {
return <div>Loading....</div>;
}
}
}
which a cleaner way of creating RouteWithAuth might be to use React Function Component like this:
export default function RouteWithAuth() {
const [redirect, setRedirect] = useState(null);
const [userReady, setUserReady] = useState(false);
useEffect(() => {
const currentUser = AuthService.getCurrentUser();
if (!currentUser) {
setRedirect("/home");
return;
}
//Do Something with the currentUser such as storing it in redux store or in context for later use cases
setUserReady(true);
}, []);
if (redirect) {
return <Redirect to={redirect} />;
} else if (userReady) {
return (
<Route
exact={props.exact}
path={props.path}
component={props.component}
/>
);
} else {
return <div>Loading....</div>;
}
}

TypeError: Cannot read property 'match' of undefined when using useParams from react-router

I can't understand why it gives me the above error. I used the props way with props.match.params.languagename and it works just fine.
I did not include all imports in the code below.
import { useParams } from 'react-router';
const App = () => {
const topicsState = useSelector(state => state.topics);
const dispatch = useDispatch();
const { languagename } = useParams();
useEffect(() => {
dispatch(fetchGitHubTrendingTopics());
}, [dispatch]);
const handleRepositoryPages = () => {
const repositoryPages = topicsState.find(
topicState => topicState.name === languagename
);
if (repositoryPages)
return <RepositoryPage repositoryPages={repositoryPages} />;
};
return (
<>
<Router>
<Header topics={topicsState} />
<Switch>
<Route path="/" exact>
<Dashboard topics={topicsState} />
</Route>
<Route
path="/language/:languagename"
exact
render={handleRepositoryPages()}
/>
<Redirect to="/" />
</Switch>
</Router>
</>
);
};
You can only use useParams in a component that is a child of your Router component, but App is the parent in your case.
The Router component injects the context containing the match into the tree below it which will be read by the useParams hook by internally using the useContext hook.
You can use this code , in jest test file for useParams .This worked for me
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn().mockReturnValue({ environment: 'dev', service: 'fakeService' }),
}))
Hope this works !
For anyone else seeing this error from trying to use useParams to get Query/Search Parameters and not URL Parameters, you can use useLocation from react-router-dom
import { useLocation } from 'react-router-dom';
...
...
const query = new URLSearchParams(useLocation().search);
...
Difference between Query Parameters and URL Parameters
Query Parameter demo
Brwoser Support for URLSearchParams()

Categories