I am feeling quite frustrated at this simple example because either I am not getting something or it is not working the way I expect it and the way it says it should in the documentation.
So I am trying to extend the CloneButton component to actually retrieve the record on each row od the Datagrid and pass it to the create component:
import React from 'react';
import PropTypes from 'prop-types';
import { CloneButton, useGetOne } from 'react-admin';
import CircularProgress from '#material-ui/core/CircularProgress';
const CloneQueryButton = ({
label = "Duplicate",
basePath,
record,
...props
}) => {
const { data, loading, error } = useGetOne(basePath, record.id);
if (loading) { return <CircularProgress />; }
if (error) { return <p>ERROR {error.message}</p>; }
if (data) {
console.log("inside", data);
data.name = `Copy of ${data.name}`;
}
console.log("outside", data);
return <CloneButton
basePath={basePath}
record={data}
label={label}
{...props} />
};
What I get on the console is just outside null
Also when the button is pressed it redirects to the create page with empty record property.
I can see the network request and they all return valid jsons. I don't understand why the component obviusly doesn't rerender when the response is recieved. If someone can see what am I missing, I will be extremely grateful!
The console succeeds, this means that there is neither an error nor a loading.
After checking the implementation of useGetOne, if it doesn't basically find a resource in your admin state with the given name, it will return null.
So I presume that you don't have the resource being declared in the admin's state (aka <Resource name="your name" /> in an <Admin> tree.
onst useGetOne = (
resource: string,
id: Identifier,
options?: any
): UseGetOneHookValue =>
useQueryWithStore(
{ type: 'getOne', resource, payload: { id } },
options,
(state: ReduxState) =>
state.admin.resources[resource]
? state.admin.resources[resource].data[id]
: null
);
I faced the same issue. Here's what I found.
When an action has been dispatched, Redux checks if the state has changed. If the state has not changed then it doesn't have to update the components. When Redux cannot see that the state has changed, it will not update the React components with new data from the state. Reference
The returning data is the same JSON which is why the state is not updating.
Two ways to solve this.
If you have access to your API, then create a new resource with a different basePath that returns the same result. Temporary fix. Not the best solution.
Ex: <Resource name="cart" />, <Resource name="cartDetails" />
Clear State in React - You can follow this article for clearing state in hooks
Related
I have built an application that works well, but i now need to add some logic into each page that checks to see if the user has a subscription and if they do then send them to payment page to make a payment.
I have a hook (using SWR) that gets me the user session and within the hook it returns a boolean for isSubscribed. I intend to use this.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
An above example is what i currently do. But this solution requires me to copy pasta everytime to the page which obviously i can do, but it's no way efficient. I know that HOC exists and from what i know i an use a HOC to do this. But i have no idea how to write one that would fit this purpose.
As an added benefit, it would be useful to add the session as a prop to the 'new component' so that i dont have to call the hook twice.
Thanks for all and any help.
p.s. i mention it in the title, but i'm using NextJS. Not sure if this has any baring (i dont think it does, but worth mentioning)
You can create a wrapper HOC such as following;
const withSession = (Component: NextComponentType<NextPageContext, any, {}>) => {
const Session = (props: any) => {
const session = useSession();
if (session.isLoading) {
return <>Loading..</>
}
else {
return <Component {...props} />
}
};
// Copy getInitial props so it will run as well
if (Component.getInitialProps) {
Session.getInitialProps = Component.getInitialProps;
}
return Session;
};
And to use it in your page or component, you can simply do like;
const UserDetailPage: React.FC = (props) => {
// ...
// component's body
return (<> HI </>);
};
export default withSession(UserDetailPage);
I think this problem doesn't necessary require a HOC, but can be solved with a regular component composition. Depending on your actual use case, it may or may not be a simpler solution.
We could implement a Session component that would leverage the useSession hook and conditionally render components passed via the children prop:
const Session = props => {
const { isLoading } = useSession();
if (isLoading) {
return "Loading...";
}
return props.children;
};
Then nest the Page component into the Session:
const GuardedPage: React.FC<PageProps> = props => {
return (
<Session>
<Page {...props} />
</Session>
);
};
I see the question has already been answered, just wanted to suggest an alternative. One of the benefits of this approach is that we can wrap an arbitrary tree into the Session, and not just the Page.
Are you trying to return a page loading screen component and direct the user to the appropriate page based on thier subscription status? or isLoading handles one event and isSubscribed handles another?
Let's define (HOC) higher order component for the sake of your problem. By using HOC, logic can be modularized and redistributed throughout components. This HOC your creating should have the capability to call different methods on a single data source or one method to be applied across multiple components. For instance say you have an API component with 5 end points (login, subscribe, logout, unsubsubscribe) the HOC should have the ability to utilize any of the endpoints from any other component you use it in. HOC is used to create an abstraction that will allow you to define logic in a single place.
Your code calls one singular method to check if the session is in use of display the content of a page based on user subscription and page loading. Without seeing the components you are trying to use I can not determine the state that needs to be passed? but I will give it shot.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
First thing I see wrong in above code as a use case for an HOC component you have no export statement to share with other components. Also, why use 2 return statements for isLoading unless both conditions need to be checked (isLoading & isSubscribed) also, are these conditional statements depended on each other or seprate functions that can be called separately from another source? if you posted more of your code or the components you are pasting this into it would help?
To use this as an HOC in NEXT is essentially the same as react.
Dependant logic
const session = useSession(props);
// ad constructor and state logic
...
if(session.isLoading) {
return this.setState({isLoading: true});
} else {
return this.setState({isSubscribed: false});
}
Separate logic
const session = useSession(props);
// ad constructor and state logic
...
isLoading () => {
return this.setState({isLoading: true});
}
isSubscribed() => {
return this.setState({isSubscribed: true});
}
or something like this that uses routes...
import React, { Component } from 'react';
import { Redirect, Route } from 'react-router-dom';
export const HOC = {
isState: false,
isSubscribed(props) {
this.isState = false;
setTimeout(props, 100);
},
isLoading(props) {
this.isState = true;
setTimeout(props, 100);
}
};
export const AuthRoute = ({ component: Component, ...rest}) => {
return (
<Route {...rest} render={(props) => (
HOC.isAuthenticated === true ? <Component {...props} /> : <Redirect to='/' />
)}/>
)};
}
If you could share more of you code it would be more helpful? or one of the components you are having to copy and paste from your original HOC code. I would be easier than stabbing in the dark to assist in your problem but I hope this helps!
Cheers!
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
Let's say we have a context provider set up, along with some initial data property values.
Somewhere along the line, let's say a consumer then modifies those properties.
On page reload, those changes are lost. What is the best way to persist the data so we can retain those data modifications? Any method other than simply local storage?
Yeah, if you want the data to persist across reloads, your options are going to be storing that info server-side (via an api call) or in browser storage (local storage, session storage, cookies). The option you'll want to use depends on what level of persistence you're looking to achieve. Regardless of storage choice, it would likely look something along the lines of
const MyContext = React.createContext(defaultValue);
class Parent extends React.Component {
setValue = (value) => {
this.setState({ value });
}
state = {
setValue: this.setValue,
value: localStorage.getItem("parentValueKey")
}
componentDidUpdate(prevProps, prevState) {
if (this.state.value !== prevState.value) {
// Whatever storage mechanism you end up deciding to use.
localStorage.setItem("parentValueKey", this.state.value)
}
}
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
)
}
}
Context doesn't persist in the way you want. Here's a sample of what I've done, using stateless functional with React hooks.
import React, {useState, useEffect} from 'react'
export function sample(){
// useState React hook
const [data, setData] = useState({})
const [moreData, setMoreData] = useState([])
// useState React hook
useEffect(() => {
setData({test: "sample", user: "some person"})
setMoreData(["test", "string"])
}, [])
return data, moreData
}
export const AppContext = React.createContext()
export const AppProvider = props => (
<AppContext.Provider value={{ ...sample() }}>
{props.children}
</AppContext.Provider>
)
Understand from the start that this isa workaround, not a permanent solution. Persisting data is the job of a database, not the client. However, if you need persisted data for development, this is one way. Notice first that I'm using React hooks. This is a fully supported feature as of 16.8. The useEffect() replaces the lifecycle methods found in class declarations like that of TLadd above. He's using componentDidUpdate to persist. The most up-to-date way of doing this is useEffect. When the app is refreshed this method will be called and set some hard-coded data in context.
To use the provider:
import React from 'react'
import Component from './path/to/component'
import { AppProvider } from './path/to/context'
const App = () => {
return (
<AppProvider>
<Component />
</AppProvider>
)
}
When you refresh, data and moreData will still have whatever default values you assign to them.
I am assuming that you are already familiar with setting context and setting up the context provider.
One of the things you can do is to store the value in the browser's Cookie or any storage available to you, and then, in your Context Provider, retrieve the value, if you can find it, you set it, if not, set the default. If the provider file is a class based component, you would like to retrieve this value in the constructor(), otherwise if it is functional, you can use useLayoutEffect() to set it.
I am trying to implement a shared state into my application using the React context api.
I am creating an errorContext state at the root of my tree. The error context looks like so:
// ErrorContext.js
import React from 'react';
const ErrorContext = React.createContext({
isError: false,
setError: (error) => {}
});
export default ErrorContext;
Desired Result
I would like to update (consume) this context from anywhere in the app (specifically from within a promise)
Ideally the consume step should be extracted into a exported helper function
Example Usage of helper function
http.get('/blah')
.catch((error) => {
HelperLibrary.setError(true);
})
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
render() {
return (
<ErrorContext.Provider value={this.state}>
{this.props.children}
</ErrorContext.Provider>
)
}
}
Then I can consume this provider by using the Consumer wrapper from inside a render call:
<ErrorContext.Consumer>
{(context) => {
context.setError(true);
}}
</ErrorContext.Consumer>
The Problem with this approach
This approach would require every developer on my team to write lots of boilerplate code every-time they wish to handle a web service error.
e.g. They would have to place ErrorContext.Consumer inside the components render() method and render it conditionally depending on the web service response.
What I have tried
Using ReactDOM.render from within a helper function.
const setError = (error) =>{
ReactDOM.render(
<ErrorContext.Consumer>
// boilerplate that i mentioned above
</ErrorContext.Consumer>,
document.getElementById('contextNodeInDOM')
) }
export default setError;
Why doesn't this work?
For some reason ReactDOM.render() always places this code outside the React component tree.
<App>
...
<ProviderClass>
...
<div id="contextNodeInDOM'></div> <-- even though my node is here
...
</ProviderClass>
</App>
<ErrorContext.Consumer></ErrorContext.Consumer> <-- ReactDOM.render puts the content here
Therefore there is no context parent found for the consumer, so it defaults to the default context (which has no state)
From the docs
If there is no Provider for this context above, the value argument
will be equal to the defaultValue that was passed to createContext().
If anyone can assist me on my next step, I am coming from Angular so apologies if my terminology is incorrect or if I am doing something extremely stupid.
You can export a HOC to wrap the error component before export, eliminating the boilerplate and ensuring that the context is provided only where needed, and without messing with the DOM:
// error_context.js(x)
export const withErrorContext = (Component) => {
return (props) => (
<ErrorContext.Consumer>
{context => <Component {...props} errorContext={context} />}
</ErrorContext.Consumer>
)
};
// some_component.js(x)
const SomeComponent = ({ errorContext, ...props }) => {
http.get('/blah')
.catch((error) => {
errorContext.setError(true);
})
return(
<div></div>
)
};
export default withErrorContext(SomeComponent);
Now that React 16.8 has landed you can also do this more cleanly with hooks:
const SomeComponent = props => {
const { setError } = useContext(ErrorContext)
http.get("/blah").catch(() => setError(true))
return <div />
}
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
I don't think so - there should be setState used. There is a general rule in react "don't mutate state - use setState()" - abusing causes large part of react issues.
I have a feeling you don't understand context role/usage. This is more like a shortcut to global store eliminating the need of explicitly passing down props to childs through deep components structure (sometimes more than 10 levels).
App > CtxProvider > Router > Other > .. > CtxConsumer > ComponentConsumingCtxStorePropsNMethods
Accessing rendered DOM nodes with id is used in some special cases, generally should be avoided because following renders will destroy any changes made externally.
Use portals if you need to render sth somewhere outside of main react app html node.
I'm having hard times understanding how I can keep the current state when sharing an URL.
I have an app made with React and Redux.
When I click on a button I'm dispatching an action to update the store and I'm changing the route at the same time.
The Redux store gets updated and the component gets rendered with data from the Redux store.
If I want to share the URL to a person that has the same app, that person does not have the store updated so it wont render anything.
I tried to summarize the code:
on route: /
<Home>
<BookList data={featured} />
<BookList data={top} />
</Home>
on route: /record/:recid
<BookDetails>
<BookInfo />
<BookList data={suggested}/>
</BookDetails>
BookList is a reusable component:
<BookList>
<BookItem onCLick={goToDetailsPage}/>
<BookItem />
<BookItem />
<BookItem />
</BookList>
How can I solve this issue?
Thank you for all the help you can provide!
In order to load the correct state to your model view (book), you'll need to have access to the necessary variables. Two simple ways to achieve this is through a url param ?recid=12 or using react-router's match params, which you're doing here. You have access to the :recid through the route match.params.recid.
React docs recommend fetching data in componentDidMount(), but there's nothing wrong with fetching new data in componentDidUpdate() when your path updates. Remember to keep track of your fetching state, so you don't make multiple api calls. A watered down version could look something like this:
class BookModel extends Component {
constructor {...}
componentDidMount() {
const { dispatch, match } = this.props;
const { recid } = match.params;
dispatch(someFetchAction("book", recid));
}
componentDidUpdate(prevProps) {
const { dispatch, fetching, match } = this.props;
const oldBook = prevProps.match.params.recid;
const newBook = match.params.recid;
const didUpdate = oldBook !== newBook;
//fetch new record if not already fetching and record id has changed
!fetching && didUpdate && dispatch(someFetchAction("book", newBook));
}
render() {...}
}
export default connect((store, props) => ({
bookModels: store.bookModels,
fetching: store.fetching
}))(BookModel);
If you're experiencing issues with updates not happening, you may want to add withRouter, but I don't believe it would be necessary in this case.