Passing data through <Link /> but getting undefined using React Router in React - javascript

I'm struggling to pass some data from one component to another one using <Link />. I cannot pass any params to my URL so, please excuse that solution. Below are my sample files from my problem,
App.jsx
<BrowserRouter>
<Router history={history}>
<Route exact path={'/my-dashboard/first-page'} component={FirstComponent}/>
</Router>
</BrowserRouter>
LinkingComponent.jsx
const LinkingComponent = ({ data }) => {
return (
<Link
to={{
pathname: '/my-dashboard/first-page',
state: { userDetails: data }
}}
>
{data.firstName}
{data.lastName}
</Link>
);
};
FirstComponent.jsx
const FirstComponent = props => {
const {
location: {
state: { userDetails }
}
} = props;
console.log('user-detail', userDetails);
....
some data manipulations
....
}
Here, the FirstComponent is not directly linked or related to the LinkingComponent. I'm using the LinkingComponent in my dashboard and I am passing my 'data' there. It is receiving it properly in the LinkingComponent. But somehow whenever I try to access it in my FirstComponent, it gives me an error that it is undefined. I have already tried various solutions and read various articles, but couldn't find an appropriate solution.
Please help me with the solution here. TIA.

First, get rid of Router or BrowserRouter, because Router is usually just an alias for BrowserRouter. Below is the import you might see:
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
And then, your App.jsx will be like:
<Router>
<Route exact path={'/my-dashboard/first-page'} component={FirstComponent}/>
</Router>
Second, this is seriously wrong:
const FirstComponent = props => {
const {
location: {
state: { userDetails }
}
} = props;
console.log('user-detail', userDetails); // <- Seriously wrong!
return <div>FirstComponent</div>;
}
Although FirstComponent is a function, it's not a normal javascript function but React functional component! You need to have a life circle method (componentDidMount) to see the userDetails prop.
Assuming that you know Hooks, your FirstComponent will be like:
import { useEffect } from "react"
const FirstComponent = props => {
const {
location: {
state: { userDetails }
}
} = props;
useEffect(() => {
console.log({ userDetails }); // <- Better console.log with Object
}, []) // <- empty dependency = componentDidMount
return <div>FirstComponent</div>;
}
Also, you need to wrap your FirstComponent by withRouter HOC, which is provided by react-router, to get the location prop in your FirstComponent's props.

Here I've created a small example. Play with it.

Related

In React Router V6, can I still get RouteComponentProps (or access to history and location) in class components? [duplicate]

The version of react-router-dom is v6 and I'm having trouble with passing values to another component using Navigate.
I want to pass selected rows to another page called Report. But, I'm not sure I'm using the right syntax for navigate method and I don't know how to get that state in the Report component.
Material-ui Table: I'm trying to use redirectToReport(rowData) in onClick parameter.
function TableRows(props){
return (
<MaterialTable
title="Leads"
columns={[
...
]}
data = {props.leads}
options={{
selection: true,
filtering: true,
sorting: true
}}
actions = {[{
position: "toolbarOnSelect",
tooltip: 'Generate a report based on selected leads.',
icon: 'addchart',
onClick: (event, rowData) => {
console.log("Row Data: " , rowData)
props.redirect(rowData)
}
}]}
/>
)}
LeadTable component
export default function LeadTable(props) {
let navigate = useNavigate();
const [leads, setLeads] = useState([]);
const [loading, setLoading] = useState(true);
async function fetchUrl(url) {
const response = await fetch(url);
const json = await response.json();
setLeads(json[0]);
setLoading(false);
}
useEffect(() => {
fetchUrl("http://localhost:5000/api/leads");
}, []);
function redirectToReport(rowData) {
navigate('/app/report', { state: rowData }); // ??? I'm not sure if this is the right way
}
return(
<div>
<TableRows leads={leads} redirect={redirectToReport}></TableRows>
</div>
)}
Report component
export default function ReportPage(state) {
return (
<div>
{ console.log(state) // This doesn't show anything. How to use the state that were passed from Table component here?}
<div className = "Top3">
<h3>Top 3 Leads</h3>
<ReportTop3 leads={[]} />
</div>
</div>
);}
version 6 react-router-dom
I know the question got answered but I feel this might be helpful example for those who want to use functional components and they are in search of passing data between components using react-router-dom v6.
Let's suppose we have two functional components, first component A, second component B. The component A wants to share data to component B.
usage of hooks: (useLocation,useNavigate)
import {Link, useNavigate} from 'react-router-dom';
function ComponentA(props) {
const navigate = useNavigate();
const toComponentB=()=>{
navigate('/componentB',{state:{id:1,name:'sabaoon'}});
}
return (
<>
<div> <a onClick={()=>{toComponentB()}}>Component B<a/></div>
</>
);
}
export default ComponentA;
Now we will get the data in Component B.
import {useLocation} from 'react-router-dom';
function ComponentB() {
const location = useLocation();
return (
<>
<div>{location.state.name}</div>
</>
)
}
export default ComponentB;
Note: you can use HOC if you are using class components as hooks won't work in class components.
Your navigate('/app/report', { state: rowData }); looks correct to me.
react-router-v6
If you need state, use navigate('success', { state }).
navigate
interface NavigateFunction {
(
to: To,
options?: { replace?: boolean; state?: any }
): void;
(delta: number): void;
}
Your ReportPage needs to be rendered under the same Router that the component doing the push is under.
Route props are no longer passed to rendered components, as they are now passed as JSX literals. To access route state it must be done so via the useLocation hook.
function ReportPage(props) {
const { state } = useLocation();
console.log(state);
return (
<div>
<div className="Top3">
<h3>Top 3 Leads</h3>
<ReportTop3 leads={[]} />
</div>
</div>
);
}
If the component isn't able to use React hooks then you still access the route state via a custom withRouter Higher Order Component. Here's an example simple withRouter HOC to pass the location as a prop.
import { useLocation, /* other hooks */ } from 'react-router-dom';
const withRouter = WrappedComponent => props => {
const location = useLocation();
// other hooks
return (
<WrappedComponent
{...props}
{...{ location, /* other hooks */ }}
/>
);
};
Then access via props as was done in pre-RRDv6.
class ReportPage extends Component {
...
render() {
console.log(this.props.location.state);
return (
<div>
<div className="Top3">
<h3>Top 3 Leads</h3>
<ReportTop3 leads={[]} />
</div>
</div>
);
}
}
2 things (just a suggestion):
Rather than a ternary use &&
{location && <div>{location.state.name}</div>}
Why are you checking location and rendering location.state.name? I would use the check on the data you are fetching or make sure the data returns null or your value.
On Sabaoon Bedar's Answer, you can check if there is any data or not before showing it :
Instead of this <div>{location.state.name}</div>
Do this { location != null ? <div>{location.state.name}</div> : ""}
if you want to send data with usenavigate in functional component you can use like that
navigate(`/take-quiz/${id}`, { state: { quiz } });
and you can get it with uselocation hook like this
const location = useLocation();
location.state.quiz there is your data
But you cannot get this data in props it;s tricky part ;)!!
on SABAOON BEDAR answer,
from component A: navigate('/', {state:"whatever"}
in component B: console.log(location.state) //output = whatever

using JWTs saved in localstorage with react context

Im having a really weird issue with My react authentication context (authProvider) and properly rendering my application page. Currently, my root app looks like this:
const App = () => {
const authCtx = useContext(AuthState);
return(
<AuthStateProvider>
<BrowserRouter>
<Switch>
{!authCtx.isLoggedIn && <Route path="/admin" component={SignInUp} />}
{authCtx.isLoggedIn && <Route path="/admin" component={Admin} />}
<Redirect from="/" to="/admin/myDashboard" />
</Switch>
</BrowserRouter>
</AuthStateProvider>
)};
Then in a seperate file that I use to manage the authentication context, I attempt to pull a JWT from local storage and verify it. If that's successful then the context is updated using state variables (including the "isLoggedIn" variable you see above").
const AuthState = React.createContext({
userName: "",
isLoggedIn: false,
authToken: null,
});
const AuthStateProvider = (props) => {
let token = null;
if(localStorage.getItem("token")) return token = localStorage.getItem("token");
const [ user, setUser ] = useState({
userName: "Anonymous",
isLoggedIn: false,
authToken: token,
});
const autoLogin = useCallback( async () => {
try {
const response = await axios({
method:'post',
url: 'http://127.0.0.1:3001/authEn'
headers: {
"Content-Type": "application/json",
"Authentication": user.authToken
}
});
if(response.status === 200){
//code to update context using setUser state handler
} else {
throw new Error("request failed");
}
} catch (e) {
console.log(e.message);
}
});
useEffect( async () => {
await autoLogin();
}, [autoLogin]);
return (
<AuthState.Provider
value={{
userName: user.userName,
isLoggedIn: user.isLoggedIn,
authToken: user.authToken
}}
>
{props.children}
</AuthState.Provider>
);
}
(I've excluded the code for my setUser handler to try and keep this short.)
So the problem is that as of right now, I'm just trying to see that the application can
A: check for stored token on initial page load / reload
B: Navigate you to either logIn or Admin page accordingly.
The app has no problem taking you to logIn page if there is a faulty/no JWT in localstorage. But when I try testing if the application can properly navigate to the admin page when there is a valid token in local storage (i have a seperate helper function to save a valid token), the page loads, but with NONE of the actual admin dashboard. Instead, all there is on the page is the token itself displayed at the top of the window as if it were just an html page with a single div containing the token as a string. I have no Idea why this happens. When I try rendering the admin component (removing the "isLoggedIn" logic and the authStateProvider) everything is fine. But each time I try adding authentication this way things start getting weird. Am I missing something obvious (usually the case)? Is this just a really stupid approach (also usually the case)? Or is this a low-level react issue (I'm not super familiar with all the intricacies of how react works under the hood.)
I think this is a bad practice to make conditions in the Switch.
Instead, you can create a separate component like ProtectedRoute or wrap your pages with a Higher-Order Component (HOC)
First way with ProtectedRoute
Pass isProtected in props if your wrapped route requires authentification
// ProtectedRoute.js
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const ProtectedRoute = ({
isProtected,
children,
}) => {
const { isLoggedIn } = useContext(AuthState);
if (isProtected && !isLoggedIn) {
return <Redirect to='/login' />;
}
return children
};
export default ProtectedRoute;
Then in your switch:
<Switch>
{ /* Other routes */ }
<ProtectedRoute isProtected>
<Route path="/admin" component={Admin} />
</ProtectedRoute>
</Switch>
HOC
// withAuth
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const withAuth = (WrappedComponent: any) =>
function (props: any) {
const { isLoggedIn ) = useContext(AuthState);
if (!isLoggedIn) {
<Redirect to='/login' />
}
return <WrappedComponent {...props} />;
}
export default withAuth;
Now you can insert your route in the switch without conditions. If your component requires authentification, wrap it with withAuth.
Example:
const NeedAuth = () => (
<div>Hello I need auth</div>
);
export default withAuth(NeedAuth)
I figured out the issue, and yes it was something super small. In the line of code where I check to see if there is a token stored on localstorage, I use an if block with a return statement. I saw a while back that doing this allows for "if" statements to be written completely on a single line and without brackets {} encapsulating the code. At the time it was really just a style choice but now I see that when the if statement runs (i.e. there is a token in local storage) the return statement within overrides the return statement of the whole functional component. So rather than having a context file that returns a provider that wraps your desired children (my admin page router), It just prints the authtoken. So I returned to traditional styling for the If statement and removed the return statement and it worked fine!

How can I define the route for the following schema in react router dom

I'm trying to make the page for the following route:
/password?token=479wasc8-8ffe-47a6-fatw-624e9d2c323a&user=e238bc4c-cf79-4cc3-b4a5-8fe7ewrta54a9w8a5
My solution to that initially was like the following:
<Route exact path='/password?token=:token&user=:user' component={Password}/>
But I guess I'm missing something important here. I tried various sources, but didn't find anything close to my problem.
The Password component can make use of the useLocation hook to read the query string:
<Route path='/password' component={Password} />
import { useLocation } from 'react-router-dom'
const useQuery = () => {
return new URLSearchParams(useLocation().search)
}
const Password = () => {
const query = useQuery()
return (
<div>
<p>{query.get('token')}</p>
<p>{query.get('user')}</p>
</div>
)
}
Here's the example on the react router website

Pass Props to Outlet in React Router v6

I can't manage to pass props to my Outlet components in the new react-router v6. I tried the straightforward solution:
render() {
return (
<Outlet name="My name" />
);
}
And that correctly renders the child component, however no props are passed to the child. None of the examples provided by the React team (or anyone else for that matter) display Outlets with props, so I'm worried it's not actually a thing. Is there another way I'm not finding or am I using Output components incorrectly?
Edit: Seems there's no straightforward way to pass props, see answer below.
You can do it with
outlet context
This is now possible (from version 6.1.0) with the context prop
<Outlet context={}/>
github issue
react router outlet docs
An alternative option here is to use Context API to share props from your parent view to your child view.
const Context = React.createContext({})
function ParentView () {
const outlet = useOutlet()
return (
<Context.Provider value={{ foo: 'bar' }}>
<h1>Parent View</h1>
{outlet}
</Context.Provider>
)
}
function ChildView () {
const props = React.useContext(Context)
return (
<div>child view {props.foo}</div>
)
}
Another option (untested) may be to use React.cloneElement to clone outlet and add props to it.
When using functional component declare the name in the parent component like this.
function Parent() {
const const name='Your name'
return <Outlet context={[name]} />;
}
Then in the child component do this
//import this
import { useOutletContext } from "react-router-dom";
function Child() {
const [name] = useOutletContext();
return <p >{name}</p>;
}
one way i did it and it works well is to create a reach context, if you know how to use react context, this will be easy for you.
In a separate file create Context.js to prevent require loop
const AdminStoreContext = React.createContext();
and then export it
export{AdminStoreContext}
then in another file create a consumer and provider of the context, and then import the context you've creates
import { AdminStoreContext } from "../../contexts";
class AdminStoreContextProvider extends React.Component {
constructor(props) {
super(props);
this.state = { ===vlaues you want to share }
}
render() {
return (
<AdminStoreContext.Provider
value={{
...this.state,//===spread the value you want to share
}}>
{
this.props.children
}
</AdminStoreContext.Provider>
);
}
}
const AdminStoreContextConsumer = AdminStoreContext.Consumer;
export { AdminStoreContextConsumer, AdminStoreContextProvider }
you can wrap your app with the context
<AdminStoreContextProvider>
<app/>
<AdminStoreContextProvider />
you can use either the consumer or the context to get the values for the purpose of Outlet, we use the context
once again import it
import { AdminStoreContext } from "../../contexts";
const route[{
path: 'consumer',
element: <MyMainComponent AdminStoreContext ={AdminStoreContext } />,
children: [
{ path: 'account', element: <MySubComponent1 /> },
{ path: 'purchaseHistory', element: <MySubComponent2 /> }
]
},
then in your MySubComponent1 or MySubComponent2
get the value from the props and use
const { AdminStoreContext } = props;
const context = React.useContext(AdminStoreContext )
and from the context you an get your values, hope this is helpfull
context.//get any value you put on the state
Unfortunately after digging for a while it looks like there's no straightforward way to do this and no plans to change it (at least for now), based on this GitHub issue's response https://github.com/ReactTraining/react-router/issues/7495.
You have to define where you want to use the name prop when defining the Outlet component
const outlet = ( props ) => {
return (
<h1>{props.name}</h1>
);
};

How to resolve unique keys required error when using <MemoryRouter /> in a test

I'm writing snapshot tests for a React application, and I'm finding that when I write tests using <MemoryRouter/> in them, it throws a warning that I need to have a unique key prop.
When I looked at the react-router documentation, there was no mention of needing a key, and I haven't found a solution to this particular issue elsewhere.
This is what my test looks like:
import React from 'react';
import ReactDOM from 'react-dom';
import { mount, shallow } from 'enzyme';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router'
import Navbar from '../Navbar';
it('renders a snapshot', () => {
const wrapper = mount(
<MemoryRouter
initialEntries={['/' ]}
initialIndex={1}
>
<Navbar/>
</MemoryRouter>
)
const tree = renderer.create(wrapper).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders without crashing', () => {
shallow(<Navbar />);
});
And here is the error I'm getting:
Warning: Each child in an array or iterator should have a unique "key" prop.
Any reason you're suing mount instead of shallow?
I'm sort of a newbie... seriously asking...
I was running into this same problem with shallow and the solution I came up with...
(if using shallow is justifiable... )
try this:
`
<div key={1}> // this is the dude here
<MemoryRouter>
<App />
</MemoryRouter>
</div>`
I'm assuming MemoryWrapper ignores unfamiliar props.
I would like to know if this is a decent solution or if there's something else we're (I'm) missing.
interesting console play... it looks like mount hits some lifecycles more than once, so the key wouldn't be unique after round 1, but not sure how you would pass that.
App.js
`class App extends Component {
componentWillMount() {
console.log('componentWillMount');
}
componentDidMount() {
console.log('componentDidMount');
}
componentWillReceiveProps() {
console.log('componentWillReceiveProps');
}
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
return true;
}
componentWillUpdate() {
console.log('componentWillUpdate');
}
componentDidUpdate() {
console.log('componentDidUpdate');
}
componentWillUnmount() {
console.log('componentWillUnmount');
}
render() {
return (
<div className='App-main'>
<Route path='/' component={Header}/>
<Route path='/' component={About} />
</div>
)
}
}`
App.test.js w/ mount
it('renders App without crashing', () => {
const appWrapper =
mount(
<div key={1}>
<MemoryRouter>
<App />
</MemoryRouter>
</div>
);
console.logs from app.js with MOUNT
`console.log src/App.js:17
componentWillMount
console.log src/App.js:20
componentDidMount
console.error node_modules/fbjs/lib/warning.js:33
Warning: Each child in an array or iterator should have a unique "key" prop. See https://some-dumb-shit.org for more information.
console.log src/App.js:17
componentWillMount
console.log src/App.js:20
componentDidMount
console.log src/App.js:36
componentWillUnmount`
App.test.js w/ SHALLOW
it('renders App without crashing', () => {
const appWrapper =
shallow(
<div key={1}>
<MemoryRouter>
<App />
</MemoryRouter>
</div>
);
console.logs from app.js with SHALLOW
`console.log src/App.js:17
componentWillMount
console.log src/App.js:20
componentDidMount
console.log src/App.js:36
componentWillUnmount`
no error. I'm not sure if my answer is correct in that maybe you were using mount when you should have used shallow? But, there's got to be some purpose for mount, and some way to rid the error.
When you create the initial entries on a MemoryRouter it should be generating an unique key for each entry. Based on your error message it could even be a problem with how your NavBar is implemented.
But, if its not, you can set a custom key in the initialEntries array like shown here.
So you would end up with your MemoryRouter like this:
<MemoryRouter
initialEntries={[ { pathname: '/', key: 'testKey' } ]}
initialIndex={1}
>
<Navbar/>
</MemoryRouter>
And this will provide the same key in all tests. (I tested with react-router-dom v5.1.2)

Categories