React router provides javascript object instead of original type on url address bar reload - reactjs

I am explining my problem with just the relevant code, as the full example is in this codesandbox link.
I am passing some props through a link to a component.
These props, have a firebase timestamp.
The props are passed correctly when the component is called through the link.
Link:
<Link to={{
pathname:path,
state: {
project
},
}} key={project.id}>
<ProjectSummary project={project} deleteCallback={projectDelete}/>
</Link>
Route:
<Route
path='/project/:id'
render={({ location }: {location: Location<{project: IFirebaseProject}>}) => {
const { state } = location;
const returnedComponent = state ? <ProjectDetails project={state.project} /> :
<ProjectDetails project={undefined}/>;
return returnedComponent;
}}
/>
and received by the ProjectList component, like this:
<div>{moment(stateProject.createdAt.toDate()).calendar()}</div>
My problem is that when the component is called through the link, props are passed and everything works fine, but, when I re-enter in the url adress bar, as the access to the component is not through the link, I would expect that the Route's render returned an undefined project (check route:
const returnedComponent = state ? <ProjectDetails project={state.project} /> : <ProjectDetails project={undefined}/>;) but, it returns the last passed project, with the timestamp as a plain Javascript object instead of a Timestamp type. So I get the error:
TypeError: stateProject.createdAt.toDate is not a function
Because the toDate() function is not available in the plain Javascript object returned, it is the Timestamp firebase type. Seems that for this specific case, the router is keeping it as a plain js object, instead of the original Timestamp instance. I would expect the route to return always the proyect undefined if not called from the link, as the props are not passed in (supposedly), but its not the case on the reload from the url address bar.
Curiously, in the codesandbox project, it does not reproduce, it fetches the data (you will be able to see the console.log('project fetched!!') when the project received is undefined).
However thrown from the dev server it happens. Might have something to do.
Find the git url if you wish to clone and check: https://github.com/LuisMerinoP/my-app.git
Remember that to reproduce you just need to enter to the link, and then put the focus in the explorer url address bar en press enter.
I case this might be the expected behaviour, maybe there is a more elegant way to way to deal with this specific case instead of checking the type returned on the reload. I wonder if it can be known if it is being called from the address bar instead of the link.
I know I can check the type in my component and fix this, creating a new timeStamp in the component from the js object returned, but I do not expect this behaviour from the router and would like to understand what is happenning.

Problem: Non-Serializable State
It returns the last passed project, with the timestamp as a plain Javascript object instead of a Timestamp type
I do not expect this behaviour from the router and would like to understand what is happening.
What's going on is that the state is being serialized and then deserialized, which means it's being converted to a JSON string representation and back. You will preserve any properties but the your methods.
The docs should probably be more explicit about this but you should not store anything that is not serializable. Under the hood React Router DOM uses the browser's History API and those docs make it more clear.
Suggestions
as in typescript is an assertion. It how you tell the compiler "use this type even though it's not really this type". When you have something that really is the type then do not use as. Instead apply a type to the variable: const project: IFirebaseProject = {
Your getProjectId function to get an id from a URL is not necessary because React Router can do this already! Use the useParams hook.
Don't duplicate props in state. You always want a "single source of truth".
Fetching Data
I played with your code a lot because at first I thought that you weren't loading the project at all when the page was accessed directly. I later realized that you were but by then I'd already rewritten everything!
Every URL on your site needs to be able to load on its own regardless of how it was accessed so you need some mechanism to load the appropriate project data from just an id. In order to minimize fetching you can store the projects in the state of the shared parent App, in a React context, or through a global state like Redux. Firestore has some built-in caching mechanisms that I am not too familiar with.
Since right now you are using dummy placeholder data, you want to build a way to access the data that you can later replace your real way. I am creating a hook useProject that takes the id and returns the project. Later on just replace that hook with a better one!
import { IFirebaseProject } from "../types";
import { projects } from "./sample-data";
/**
* hook to fetch a project by id
* might initially return undefined and then resolve to a project
* right now uses dummy data but can modify later
*/
const useProject_dummy = (id: string): IFirebaseProject | undefined => {
return projects.find((project) => project.id === id);
};
import { IFirebaseProject } from "../types";
import { useState, useEffect } from "react";
import db from "./db";
/**
* has the same signature so can be used interchangeably
*/
const useProject_firebase = (id: string): IFirebaseProject | undefined => {
const [project, setProject] = useState<IFirebaseProject | undefined>();
useEffect(() => {
// TODO: needs a cleanup function
const get = async () => {
try {
const doc = await db.collection("projects").doc(id).get();
const data = doc.data();
//is this this right type? Might need to manipulate the object
setProject(data as IFirebaseProject);
} catch (error) {
console.error(error);
}
};
get();
}, [id]);
return project;
};
You can separate the rendering of a single project page from the logic associated with getting a project from the URL.
const RenderProjectDetails = ({ project }: { project: IFirebaseProject }) => {
return (
<div className="container section project-details">
...
const ProjectDetailsScreen = () => {
// get the id from the URL
const { id } = useParams<{ id: string }>();
// get the project from the hook
const project = useProject(id ?? "");
if (project) {
return <RenderProjectDetails project={project} />;
} else {
return (
<div>
<p> Loading project... </p>
</div>
);
}
};
Code Sandbox Link

Related

How to pass class instances to react-router-dom navigate state?

I'm trying to pass a class instance, which has private members, through to a navigate call for react-router-dom, through its state key.
The class instance I'm trying to pass:
class Cart{
#contents;
add(itemName){
const existingEntry = this.#contents[itemName];
const oldCount = existingEntry?.count;
const newCount = oldCount+1;
this.#contents[itemName] = {name: itemName, count: newCount};
return this;
}
get contents(){
return JSON.parse(JSON.stringify(this.#contents))
}
}
Passing the instance to navigate:
function SomeComponent(){
// some react code...
const navigate = useNavigate();
const handleClick = () => {
const item = 'Bananas';
const cart = new Cart().add(item);
navigate('/someURL', { state: {cart} });
}
// some react code...
}
But when I access it from the consumer component using useLocation, it turns into an empty object. This means I can't access the contents of the class, since the recieved object is just a plain object, and has no getter functions, like the class instance I passed in.
function ConsumerComponent(){
//some react code...
const { state } = useLocation();
console.log(state.cart); // '{}'
//some react code...
}
How can I pass it to navigate, while getting back the same instance, instead of a shallow copied object?
EDIT 1
Here is a link to a codesandbox that demonstrates the issue. Strangely enough, the code works in the sandbox; and I get the class instance at the ConsumingComponent. But try downloading the sandbox locally and running it. The ConsumingComponent will only be able to access a plain object(as hypothesized by #Drew Reese) in the comments. I would be inclined to believe that
"only JSON/string serializable objects can be passed via route state"
but I can't wrap my head around this discrepancy in behaviour between the sandbox and the local instance. Any hypotheses, ideas or even conjecture is greatly appreciated.
TMI: I'm trying to implement a checkout fn which accepts a Cart object as a parameter, and navigates to the checkout page #'/checkout', while passing the cart inside the navigate route state. The reason I wish to do so, is because the checkout fn is intended to checkout single objects(similar to a Buy now button on amazon), and hence I don't really want to store the Cart in my global state management system, since its a single-use class instance.

nextjs pass current route to the higher order cmponent as parametre

i am having a hoc withAuth.js
in which i am passing a component and second parameter is current route which would render if the condtion fails
const Detail = (props) => {
return(
<>
<div> this is the my account inside ..... </div>
</>
)
};
export async function getServerSideProps({req ,res}) {
// Call an external API endpoint to get posts.
// You can use any data fetching library
// console.log('request object ', req.headers);
// retun the props.api bcoz client side has no access to the baseurl from the server
return {
props:{}
}
}
export default withAuth(Detail, loginRedirectPath);
my question is that how to pass the current route the hoc
edit
hi i have solve this problem by managing route history
I don't believe you need to actually pass this as a parameter, but you can if you so wish. As per the docs, you can use the useRouter() hook (or another method such as importing the Router object) to get the current pathname. I believe this will work on either the component or HOC when using the hook, although I may be wrong on this. Regardless, using next/router to get the pathname is the approach here!

A simple reducer that share a value and a dispatch to my components

Edited question :
I think i don't really well explained my self in the old question below so I am rewriting it.
So basically I want to do something like a module that have a default value (for example a string), anywhere in my project I need to be able to access to this value and modifie it via importing this module.
When the value of this module is changed, my components should be re-render. This is why in my example in the old question I've tried to use useState, but it doesn't work because it recreate the state every time the module is called.
Old question :
I am trying to create a reducer that keep an object, share this object, and a dispatch that permit me to change this object.
Here is my reducer, he's really simple but for my needs I thought it would be enough (but maybe the problem is not coming from here) :
import { useState } from 'react';
export default () => useState({ foo: 'bar' });
And here is how I try to use it :
// Component that show the content of `foo`
import myReducer from '../reducers/MyReducer';
export default = () => {
const [anObject] = myReducer();
return <h1>foo : {anObject}</h1>;
};
// Component that set `foo`'s value
import myReducer from '../reducers/MyReducer';
export default = () => {
const [anObject, setAnObject] = myReducer();
return <button onClick={() => setAnObject({ ...anObject, foo: 'newBar' })}>Dispatch the reducer</button>;
};
And when I try this in the browser I get this error :
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
In this case the whole reducer is a little bit useless but imagine multiples components like the last one that set the foo's value.
I could pass everything through props but this is just an example, it could not be really viable to do this on a big project.
I am referring to this part of the doc : https://reactjs.org/docs/hooks-custom.html

React native navigation.state.params always undefined

I am fairly new to React native, and I have bought a template called sketch elements to get me started which is using Flow for it's type checking.
https://react-native.shop/elements
I would like to pass a parameter while navigating, however the param is always undefined.
This is my code in my first component - I would like to navigate to the navigateTo component (in this case Photos) passing in the level object
navigate(navigateTo: string, level: object) {
const { navigation } = this.props;
navigation.navigate(navigateTo, { level });
}
My other component looks as follows, console log always returns undefined. Looking through the other examples this looks identical to what has been achieved before but I am obviously missing something.
type PhotosProps = NavigationProps<> & {
level: object
};
export default class Photos extends React.Component<NavigationProps<PhotosProps>> {
render(): React.Node {
const { navigation } = this.props;
console.log( navigation.state.params);
return null;
}
}
Any help would be very much appreciated.
You can access parameters passed into react-navigation library by using const level = navigation.getParam('level'). Docs on passing parameters: https://reactnavigation.org/docs/en/params.html
If using react-navigation v5, then
route.params?.someParam ?? 'defaultValue';

Read the current full URL with React?

How do I get the full URL from within a ReactJS component?
I'm thinking it should be something like this.props.location but it is undefined
window.location.href is what you're looking for.
If you need the full path of your URL, you can use vanilla Javascript:
window.location.href
To get just the path (minus domain name), you can use:
window.location.pathname
console.log(window.location.pathname); //yields: "/js" (where snippets run)
console.log(window.location.href); //yields: "https://stacksnippets.net/js"
Source: Location pathname Property - W3Schools
If you are not already using "react-router" you can install it using:
yarn add react-router
then in a React.Component within a "Route", you can call:
this.props.location.pathname
This returns the path, not including the domain name.
Thanks #abdulla-zulqarnain!
window.location.href is what you need. But also if you are using react router you might find useful checking out useLocation and useHistory hooks.
Both create an object with a pathname attribute you can read and are useful for a bunch of other stuff. Here's a youtube video explaining react router hooks
Both will give you what you need (without the domain name):
import { useHistory ,useLocation } from 'react-router-dom';
const location = useLocation()
location.pathname
const history = useHistory()
history.location.pathname
this.props.location is a react-router feature, you'll have to install if you want to use it.
Note: doesn't return the full url.
Plain JS :
window.location.href // Returns full path, with domain name
window.location.origin // returns window domain url Ex : "https://stackoverflow.com"
window.location.pathname // returns relative path, without domain name
Using react-router
this.props.location.pathname // returns relative path, without domain name
Using react Hook
const location = useLocation(); // React Hook
console.log(location.pathname); // returns relative path, without domain name
You are getting undefined because you probably have the components outside React Router.
Remember that you need to make sure that the component from which you are calling this.props.location is inside a <Route /> component such as this:
<Route path="/dashboard" component={Dashboard} />
Then inside the Dashboard component, you have access to this.props.location...
Just to add a little further documentation to this page - I have been struggling with this problem for a while.
As said above, the easiest way to get the URL is via window.location.href.
we can then extract parts of the URL through vanilla Javascript by using let urlElements = window.location.href.split('/')
We would then console.log(urlElements) to see the Array of elements produced by calling .split() on the URL.
Once you have found which index in the array you want to access, you can then assigned this to a variable
let urlElelement = (urlElements[0])
And now you can use the value of urlElement, which will be the specific part of your URL, wherever you want.
To get the current router instance or current location you have to create a Higher order component with withRouter from react-router-dom. otherwise, when you are trying to access this.props.location it will return undefined
Example
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
class className extends Component {
render(){
return(
....
)
}
}
export default withRouter(className)
Read this I found the solution of React / NextJs. Because if we use directly used the window.location.href in react or nextjs it throw error like
Server Error
ReferenceError: window is not defined
import React, { useState, useEffect } from "react";
const Product = ({ product }) => {
const [pageURL, setPageURL] = useState(0);
useEffect(() => {
setPageURL(window.location.href);
})
return (
<div>
<h3>{pageURL}</h3>
</div>
);
};
Note:
https://medium.com/frontend-digest/why-is-window-not-defined-in-nextjs-44daf7b4604e#:~:text=NextJS%20is%20a%20framework%20that,is%20not%20run%20in%20NodeJS.
As somebody else mentioned, first you need react-router package. But location object that it provides you with contains parsed url.
But if you want full url badly without accessing global variables, I believe the fastest way to do that would be
...
const getA = memoize(() => document.createElement('a'));
const getCleanA = () => Object.assign(getA(), { href: '' });
const MyComponent = ({ location }) => {
const { href } = Object.assign(getCleanA(), location);
...
href is the one containing a full url.
For memoize I usually use lodash, it's implemented that way mostly to avoid creating new element without necessity.
P.S.: Of course is you're not restricted by ancient browsers you might want to try new URL() thing, but basically entire situation is more or less pointless, because you access global variable in one or another way. So why not to use window.location.href instead?

Resources