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.
Related
I'm using React and my application has a side panel that displays information from various parts of the app. I tried updating the panel information through state and props but the problem is that the UI rerenders unrelated parts of the app because I need to put the state high up in the tree. I want this panel position to be fixed, and the data to only update from the relevant child components. I thought a good solution is to create the panel high up in the tree hierarchy and then pass its reference to children so that they can update it without rerendering the whole app. I have 2 questions:
Is creating the side panel high up and passing a single reference to children the right approach?
If yes, how can I do this?
I finally figured out the answer which was inspired by this example: https://codesandbox.io/s/3y18l37wm6.
I'm still hoping to find a better solution, having a Singleton doesn't sit well with me, and I don't know if it's a good approach in general.
Here are the steps...
First, create an index.js file for a module that represents the SidePane
const warn = () => console.log("haven't resgiter the SidePane yet");
let sidePane = {
open: warn,
};
/**
* this method is to save the reference of the singleton component
*/
export const registerSidePane = ref => {
if (ref) {
console.log("sidepane registered");
sidePane.open = ref.open;
} else {
console.log("sidepane unregistered");
sidePane.open = warn;
}
};
/**
* export the side pane component
*/
export { default as SidePaneInstance } from "./SidePane";
/**
* export the open function
*/
export default sidePane;
Second, create the component (call it SidePane.js) to which you want to pass your data, and structure it however you like.
export default class SidePane extends React.Component {
state = { component: <></> };
open = component => {
this.setState({ component: component });
}
render() {
return this.state?.component
}
}
Third, place this pane wherever you want in the app and register the ref like this:
<SidePaneInstance ref={registerSidePane} />
Finally, pass the data dynamically from any component as follow:
SidePane.open(<div>it finally works!</div>)
I want to show/hide element present in a component, based on role level permission,
here is the code which I tried so far
My Component:
<ShowForPermission permission="My Studies" configuration={configuration}>
<div>Mystudies</div>
<ShowForPermission>
HOC:
import React from 'react'
export const ShowForPermission = ({ permission, configuration}) => {
const isAllowed = checkPermission(permission, configuration);
return isAllowed
}
export const checkPermission = (permission, configuration) => {
return configuration&&configuration.filter((item)=>item.name===permission)[0].permission.read
}
Here in My Component I'm passing key as My Studies and role config of that particular component as configuration to ShowForPermission
And in HOC I'm checking the given key i.e permission "My studies" equals to configuration.filter((item)=>item.name==="My Studies") so what I'm checking s suppose if this value is true I want render div present in my Component or else no. How to achieve this. Please help me on this
if permission.read==true, return true else false and render div based on condition.
Thanks
The thing you've described is not a HOC. That's fine, component composition(when you nest component to show in another component that provides control) suits even better than HOC here.
So your ShowForPermission makes some check and renders whatever nested component provided(nested components are passed as children prop)
export const ShowForPermission = ({ permission, children }) => {
const isAllowed = ..... // do some permission check
if (isAllowed) {
return children; // rendering nested elements
} else {
/*
it also might be false, empty string or empty array
(and for React 18 it can be `return;` or `return undefined` as well);
also you even can omit explicit `return undefined;` for `else` branch
but this way intention is more clear
*/
return null;
}
}
But how do you get configuration? Definitely not passing it as a prop. Why? Because you will need to pass it every time you use ShowForPermission. And to pass it in 3rd-, 5th- or 10th-level nested component you would have to pass configuration to every its parent up the line. That has name of "prop drilling" and Context API is exactly what been created to solve this.
Context API is a way to inject data into component without passing it explicitly into every parent as a prop. Take a look into examples in docs, finally your app should load configuration and create <YourContextProvider value={configurationData}> somewhere at the very root app element or close to the root. I'm intentionally skip question how you can load that configuration before passing to context(a large separate topic and also it's up to your requirements).
Finally, your ShowForPermission would access data from context(preferably with useContext hook) to check the permission:
export const ShowForPermission = ({ permission, children }) => {
const configuration = useContext(YourPermissionContext);
const isAllowed = configuration
.filter(
(item) => item.name===permission
)[0].permission.read;
if (isAllowed) {
return children; // rendering nested elements
} else {
return null;
}
}
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
I have some code that grabs the users ipAddres. I do this right now in my componentDidMount in my app.js
async componentDidMount() {
await eventTrackingStore.getIpAddress();
}
So I did it in my app.js as it is my root component and I only want to set this once. This works fine if the user starts from the home page and navigates through the site.
However some pages can be loaded directly(ie you type in the url in your browser and it goes straight to that page).
Since the react lifecycle starts with most immediate component, which calls a method that expects the ipAddress code to be set but it does not get set till it hits the app.js
Now I could put the above code in each method but that gets tedious. Is there some sort of method in reactjs, or mbox or mbox state tree that would fire first?
If you use mobx-state-tree and you have a global store, then that global store can make the API call in the afterCreate method
const AppModel = types.model({
ips: types.array(types.string)
}).actions(self => ({
afterCreate() {
flow(function*() {
const ips = yield eventTrackingStore.getIpAddress();
self.setIps(ips)
})()
},
setIps(ips: string[]) {
self.ips = ips
}
}))
OR
The same thing you can do in a wrapped react component that wrappes every page of your app.
class App extends React.Component {
componentDidMount() {
eventTrackingStore.getIpAddress().then(res => {
// set the ips into a store or any logic you want in order to pass them down to children
})
}
render() {
return this.props.children
}
}
I can think of two solutions:
You can use react context
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Use context to share the store between all components and if the data is not loaded, initialize loading right there in that nested component.
If the data is already there then just take it,
getIpAddress method should return a promise, so in case when data is already there it will be immediately resolved.
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