I have the following component:
import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import DashboardContext from '../../contexts/DashboardContext';
import authorizeWorker from '../../workers/authorize-worker';
/**
* A protected route that calls the authorize API and redirects to login
* on fail
* #param {Function} component The component to redirect to on success
*/
const ProtectedRoute = ({ component }) => {
const [isAuthenticated, setIsAuthenticated] = useState(null);
const dashboardContext = useContext(DashboardContext);
dashboardContext.setIsDashboard(true);
const Component = component;
useEffect(() => {
authorizeWorker({})
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
}, []);
if (isAuthenticated === true) {
return <Component />;
}
if (isAuthenticated === false) {
return <Redirect to="/login" />;
}
return null;
}
ProtectedRoute.propTypes = {
component: PropTypes.func
};
export default ProtectedRoute;
I use this for my router e.g.
<ProtectedRoute path="/projects" component={Projects} />
I am recently seeing a warning in the console: Warning: Cannot update a component (App) while rendering a different component (ProtectedRoute). To locate the bad setState() call insideProtectedRoute, follow the stack trace as described. Why am I seeing this error and how can I fix it?
The error is because on initial render phase, you render the component with setIsDashboard(true);, usually, you want to do it on mount (useEffect with empty dep array).
There is an initial render phase, then mount phase, see component's lifecycle diagram.
Be sure that setIsDashboard is persistent, meaning it is created by React API (like useState).
Or its memoized with useMemo/useCallback, or you will get inifite loop because on every render a new instance of setIsDashboard will be created and the dep array ([setIsDashboard]) will cause another execution.
const ProtectedRoute = ({ component }) => {
const { setIsDashboard } = useContext(DashboardContext);
// Make sure `setIsDashboard` persistant
useEffect(() => {
setIsDashboard(true);
}, [setIsDashboard]);
...
};
The error is because you can't set state when you are rendering:
dashboardContext.setIsDashboard(true); is probably the problem.
You don't post your stack trace or line numbers so it's hard to tell exactly what the issue is:
https://stackoverflow.com/help/minimal-reproducible-example
Related
In my app, I am using react-router v5 and react/typescript I have a component that uses the react-query and fetches some data. At the moment it only fetches the data when the component is rendered the first time, When navigating the request does not get cancelled and navigating back it does not make a new request. This component takes in an id parameter which fetches the data based on the id, so it needs to either refresh the component or maybe I need to add the method into the useEffect hook?
Routing component
import React from 'react';
import { BrowserRouter, Route, Switch} from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { RouteComponentProps } from "react-router-dom";
import Component1 from '../Component1';
import Component2 from '../Component2';
const queryClient = new QueryClient()
const Routing: React.FunctionComponent = () => {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Switch>
<Route exact path="/" component={Component1} />
<Route path="/details/:id" render={(props: RouteComponentProps<any>) => <Component2 {...props}/>} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
</QueryClientProvider>
)
}
export default Routing;
Component2 (id)
import React from 'react';
import { useQuery } from 'react-query';
import { RouteComponentProps, useLocation } from "react-router-dom";
interface stateType {
model: { pathname: string },
start: { pathname: string | Date }
}
const Component2: React.FunctionComponent<RouteComponentProps<any>> = (props) => {
const { state } = useLocation<stateType>();
let alertInnerId = props.match.params.id;
const fetchChart = async () => {
const res = await fetch(`/detail/${id}`);
return res.json();
};
const { data, status } = useQuery('planeInfo', fetchPlane, {
staleTime: 5000,
});
return (
<>
{status === 'error' && (
<div className="mt-5">Error fetching data!</div>
)}
{status === 'loading' && (
<div className="mt-5">Loading data ...
</div>
)}
{status === 'success' && (
{data.map(inner => {
return (
<p>{inner.id}</p>
)
})}
)}
</div>
</>
)
}
export default Component2;
In the Component 1 I am programmatically navigating:
onClick={() => history.push(`/detail/${id}}`, { model: plane.model, start: formattedStartDateTime })}>
Either way by programmatically or normal, its still the same.
[...] and navigating back it does not make a new request.
First of all, according to your code, as per the staleTime option that is set as an option on useQuery itself, the cache should invalidate every five seconds. So each time the useQuery hook is mounted (such as on route change), if five seconds have passed, a new request should be made. Your code does appear to be incomplete though as you're referencing id which appears to be undefined.
In any case, since you are requesting details of a resource with an ID, you should consider using a query key like: [planeInfo, id] instead of planeInfo alone. From the documentation:
Since query keys uniquely describe the data they are fetching, they
should include any variables you use in your query function that
change. For example:
function Todos({ todoId }) {
const result = useQuery(['todos', todoId], () =>
fetchTodoById(todoId))
}
To handle canceling the request on navigation:
You can't wrap the useQuery hook from React Query in a useEffect hook, but rather you can use use the return function of useEffect to clean up your useQuery request, effectively canceling the request when the component unmounts. With useQuery there are two ways (possibly more) to cancel a request:
use the remove method exposed on the returned object of useQuery
use the QueryClient method: cancelQueries
(see: useQuery reference here)
see: QueryClient reference here and specifically cancelQueries
Using remove with useEffect
(I've only kept the relevant bits of your code)
const Component2: React.FunctionComponent <RouteComponentProps<any>> = (props) => {
const fetchChart = async() => {
const res = await fetch(`/detail/${id}`);
return res.json();
};
const {
data,
status,
/** access the remove method **/
remove
} = useQuery('planeInfo', fetchPlane, {
staleTime: 5000,
});
useEffect(() => {
/** when this component unmounts, call it **/
return () => remove()
/** make sure to set an empty deps array **/
}, [])
/** the rest of your component **/
}
Calling remove like this will cancel any ongoing request, but as its name suggests, it also removes the query from the cache. Depending on whether you need to keep the data in cache or not, this may or may not be a viable strategy. If you need to keep the data, you can instead use the canceQueries method.
Using cancelQueries with useEffect
Much like before except here you need to export your queryClient instance from the routing component file (as you have it defined there) and then you're importing that instance of QueryClient into Component2 and calling cancelQueries on the cache key from useEffect:
import { queryClient } from "./routing-component"
const Component2: React.FunctionComponent <RouteComponentProps<any>> = (props) => {
const fetchChart = async() => {
const res = await fetch(`/detail/${id}`);
return res.json();
};
const {
data,
status,
} = useQuery(['planeInfo', id], fetchPlane, {
staleTime: 5000,
});
useEffect(() => {
/** when this component unmounts, call it **/
return () => queryClient.cancelQueries(['planeInfo', id], {exact: true, fetching: true})
}, [])
/** the rest of your component **/
}
Here you see that I've implemented the query key as I suggested before, with the id as well. You can see why having a more precise reference to the cached object can be beneficial. I'm also using two query filters: exact and fetching. Setting exact to true will make sure React Query doesn't use pattern matching and cancel a broader set of queries. You can decide whether or not this is necessary for your implementation needs. Setting fetching to true will make sure React Query includes and cancels and queries that are currently fetching data.
Just note that by depending on useEffect, it is in some cases possible for it's parent component to unmount due to factors other than the user navigating away from the page (such as a modal). In such cases, you should move your useQuery up in the component tree into a component that will only unmount when a user navigates, and then pass the result of useQuery into the child component as props, to avoid premature cancellations.
Alternatively you could use Axios instead of fetch. With Axios you can cancel a request using a global cancel token, and combine executing that cancellation with React Router's useLocation (example here). You could of course also combine useLocation listening to route changes with QueryClient.cancelQueries. There are in fact, many possible approaches to your question.
I want to pass the setState method of the component (SnackBar) to all the child components of the _app.js. If I pass the setState method of SnackBar to all the child components of _app.js then it will be a very tedious task. Because, there are approx 4 levels of hierarchy from _app.js to the single component node. It includes,
_app.js -> pages -> layouts -> sections -> components
The snippet of _app.js is here.
function MyApp({ Component, pageProps }) {
const [ toastOpen, setToastOpen ] = React.useState({
msg: '',
open: false
});
React.useEffect(() => {
pageProps = { ...pageProps, setToastOpen };
}, []);
return (
<React.Fragment>
<ToastMessage
message={ toastOpen.msg }
setOpenState={ setToastOpen }
openState={ toastOpen.open }
/>
<Component {...pageProps} />
</React.Fragment>
)
}
Is there any way that I can directly import the setToastOpen method in the child component and use it whenever I need it?
React have a special Feature called Context Api , using that you can skip the props chain passed into your components..
I recomend you to checkout below resources to learn about context Api -
https://reactjs.org/docs/context.html
https://www.freecodecamp.org/news/react-context-in-5-minutes
Example of ContextAPI
Create a seperate file for Context Toast-context.js , You can use any name you want.
import React, { useState } from "react"
const ToastContext = React.createContext({
message: "",
toastOpen: false,
toggleToast: () => { },
changeMessage: () => { }
})
export const ToastContextProvider = ({children}) => {
/*you need to use
this component to wrap App.js so that the App.js and all of its children
and their children components and so on will get the access to the
context*/
const [toastOpen, setToastOpen] = useState(false);
const [message, setMessage] = useState("");
const toggleToast = () => {
setToastOpen(true)
}
const changeMessage = (message) => {
setMessage(message);
}
return (
<ToastContext.Provider value={
toastOpen,
message,
toggleToast,
changeMessage
}>
{children}
</ToastContext.Provider>
)
}
now in the App.js file you need to wrap your components with ToastContextProvider component
import React, { useContext } from "react";
import { ToastContextProvider } from "./Toast-context";
import { ToastContext } from "./Toast-context";
function MyApp({ Component, pageProps }) {
const { message, toastOpen, toggleToast, changeMessage } =
useContext(ToastContext);
return (
<ToastContextProvider>
{toastOpen && <div className="toast">{message}</div>}
</ToastContextProvider>
);
}
just import the context using useContext Hook in any component you want. you don't need to wrap with <ToastContextProvider> in every component.
just use useContext hook and then you can see the state and as well as call the functions methods to change the state.
Also make sure to refer the above links to learn more about Context Api. Thank You
I am receiving the error of Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state..
It appears that it is because of this code
const LoginAuth = () => {
navigate(routes.INDEX);
return null;
};
removing navigate(routes.INDEX); stops the error.
What is wrong with the code? Should I be using another method to redirect an authUser? Any help is appreciated.
It is a part of
import React from 'react';
import { navigate } from 'gatsby';
import AuthUserContext from './AuthUserContext';
import withAuthentication from './withAuthentication';
import Layout from './Layout';
import LoginForm from './LoginForm';
import * as routes from '../../constants/routes';
const LoginAuth = () => {
navigate(routes.INDEX);
return null;
};
const LoginPage = () => (
<Layout>
<Transition>
<AuthUserContext.Consumer>
{authUser => (authUser ? <LoginAuth /> : <LoginNonAuth />)}
</AuthUserContext.Consumer>
</Transition>
</Layout>
);
const LoginNonAuth = () => <LoginForm />;
export default withAuthentication(LoginPage);
Stateless functional components are expected to be pure functions, i.e. contain no side effects, while navigate() provides side effects.
Side effects are supposed to be applied after the component is mounted, that's the purpose of componentDidMount hook.
It should be:
class LoginAuth extends Component {
componentDidMount() {
navigate(routes.INDEX);
}
render() {
return null;
}
}
With the introduction of stateful functional components, side effects belong to useEffect hook:
const LoginAuth = () => {
useEffect(() => {
navigate(routes.INDEX);
}, []);
return null;
};
I have a parent component that renders a list of child components, but it's based on the result I got from an API call, set the state of the parent component, and render the child components using this.state. The problem I have right now is that since setState is an async function, by the time the parent component is mounted using Enzyme, the setState hasn't completed yet and therefore the child components don't exist in the DOM. I just want to know what is the correct way to test the existence of child components in this case?
Any help would be appreciated!
ParentComponent.js
componentDidMount() {
someAsyncAPICall().then(status => {
this.setState({status: status});
});
}
render() {
return (
{this.state.status.map((object, rowIndex) =>
<ChildComponent key={object.id}
...
/>
)}
);
}
Mocha unit test for parent
import React from 'react';
import {expect} from 'chai';
import {mount} from 'enzyme';
import fetchMock from 'fetch-mock';
...
describe('ParentComponent', () => {
const baseURL = "http://localhost:8000";
const status = [{
...
}];
before(() => {
fetchMock.get(`${baseURL}/api`, status);
});
after(() => {
fetchMock.reset();
fetchMock.restore();
});
it('contains the correct number of components', (done) => {
const wrapper = mount(<ParentComponent />);
//This is not working - the length of ChildComponent is always 0
expect(wrapper.find(ChildComponent).length).to.equal(status.length);
});
});
Suppose that one has the following component file HelloForm.jsx:
import React from 'react';
import {graphql} from 'react-apollo';
import {connect} from 'react-redux';
import {actions, Control, Form} from 'react-redux-form';
import {compose, withProps} from 'recompose';
import query from './HelloForm.gql';
const passThrough = fn => BaseComponent => props => {
fn(props, BaseComponent);
return BaseComponent(props);
};
export const HelloForm = ({onSubmitEventHandler}) =>
<Form
className="my-form"
model="forms.hello"
onSubmit={onSubmitEventHandler}
>
<label htmlFor="name">Name</label>
<Control.text
{...props}
model=".name"
id="name"
/>
<button type="submit">Say Hello</button>
</Form>;
export const enhance = compose(
connect(),
withProps({
onSubmitEventHandler: ({name}) => {
window.alert(`Hello, ${name}`);
},
}),
graphql(query),
passThrough(({data, dispatch, loading}) => {
if (!loading) {
dispatch(actions.load('forms.hello', data));
}
}),
);
export default enhance(HelloForm);
This appears to work as expected but one gets the following warning:
Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
However, React's component documentation suggests that one should dispatch the action during the componentDidMount lifecycle event (which can be done with functional components via recompose.lifecycle). But no props are provided to the componentDidMount event handler.
What is the proper way to "asynchronously" dispatch actions to Redux?
The solution was indeed to use the recompose.lifecycle higher-order component. However, one must not use an arrow function. The updated enchance higher-order component should be implemented as follows:
export const enhance = compose(
connect(),
withProps({
onSubmitEventHandler: ({name}) => {
window.alert(`Hello, ${name}`);
},
}),
graphql(query),
lifecycle({
componentDidMount: function () {
const {dispatch, loading, recipe} = this.props;
if (loading) {
dispatch(actions.load('forms.hello', data));
}
},
}),
);