Can not update a component while rendering a different component - javascript

I am using the useAuth hook to my authContext, so that I can use hook to set global states for my components.
Now, I am trying to navigate to the '/' page if the user was not logged in, and also display a requireLoginAlert to the user in the '/' page after the user was redirected. However, I got the following error when I try to include the setRequireLoginAlert(true) function in requireAuth.js.
Warning: Cannot update a component (`AuthProvider`) while rendering a different component (`RequireAuth`). To locate the bad setState() call inside `RequireAuth`
My requireAuth.js:
const RequireAuth = () => {
const { auth, setRequireLoginAlert } = useAuth();
const location = useLocation();
return (
auth?.user
? <Outlet />
: <Navigate to='/' state={{ from: location }} replace />
&& setRequireLoginAlert(true)
);
};
export default RequireAuth;
My AuthContext.js
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
const [requireLoginAlert, setRequireLoginAlert] = useState(false);
return (
<AuthContext.Provider value={{ auth, setAuth, requireLoginAlert, setRequireLoginAlert }}>
{children}
</AuthContext.Provider>
);
};
export default AuthContext;
I have try using useEffect the display the alert whenever the location has changed, but in this way the alert will keep popping out whenever I go to other pages, which is not ideal. I need a trigger to change the requireLoginAlert state after the user was redirected without error.
Feel free the drop a comment if you have any idea. Thanks a lot.

I think you were on the good way with the useEffect approach.
Indeed you have use the location in the dependencies array, but you also need to include a condition based on the current page so that the requireLoginAlert is not called every time
Have you tried something like the following piece of code ?
useEffect(
() => auth?.user && location === "/" && setRequireLoginAlert(true),
[auth?.user, location]
);

Related

How do I render a react portal modal with different children?

Right now, I have a react portal rendering a 'pinnable' side drawer modal that will display based on a redux state. The contents of the modal will have information based on where that modal was pinned from, in this case my notifications.
The problem I'm running into at the moment is that since the modal will be pinnable in multiple places, I'm not exactly sure of the logic on how to handle the modal contents if the modal is already pinned.
I've tried/considered the following:
Just have one portal render its children dynamically. Unfortunately the location of where the portal will be rendered does not contain the contents and logic of the modal, so I believe this can't be done.
Compare props.children and if they're not identical, render the newer portal and deconstruct the other. I'm hesitant to use this approach since I believe there's a better solution out there.
Render the portals based on Ids and deconstruct/reconstruct where needed if one exists. I'm leaning towards this approach, but again I'd like to see if there's a better one.
Portal location:
export default function PaperContainer() {
return <div id="pinned-container"></div>;
}
Portal:
export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);
return (
pinned &&
createPortal(
<div>
<div>{props.children}</div>
</div>
,
document.getElementById('pinned-container')
)
);
}
Where the portals are called (simplified for brevity):
export default function PortalCallLocationOne() {
const dispatch = useDispatch();
const pinContainer = () => {
dispatch(toggleDrawer());
};
return (
<>
<Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
Pin Notification
</Button>
<PinnedContainer>
//Notification
</PinnedContainer>
</>
);
}
export default function PortalCallLocationTwo() {
const dispatch = useDispatch();
const pinContainer = () => {
dispatch(toggleDrawer());
};
return (
<>
<Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
Pin List
</Button>
<PinnedContainer>
// List
</PinnedContainer>
);
</>
}
I tried going off of #3 and destroying pinned-container's first child if it existed and replace it with the new children. This didn't work since React was expecting that child and kept throwing failed to execute removeChild on node errors.
Unfortunately it looks like react is unable to replace portal children instead of appending them.
However, I was able to solve my issue by unpinning the portal and repinning it with redux actions.
export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);
useEffect(() => {
if (pinned) {
dispatch(clearPinned());
dispatch(pinDrawer(true));
}
}, []);
return (
pinned &&
createPortal(
<div>
<div>{props.children}</div>
</div>
,
document.getElementById('pinned-container')
)
);
}
Reducer:
export const initialState = {
isDrawerPinned: false,
}
export const reducer = (state = initialState, action) => {
switch(action.type){
case actionTypes.PIN_DRAWER:
return {
...state,
isDrawerPinned: action.isPinned ? action.isPinned : !state.isDrawerPinned,
};
case actionTypes.CLEAR_PINNED:
return {
...state,
isDrawerPinned: state.isDrawerPinned ? !state.isDrawerPinned : state.isDrawerPinned
};
}
}

Variable passed trough state in Link can't update

I am updating my theme in my App per useState. This is passed to Topbar-Component per prop. console.log() gets triggered every time it changes. From Topbar theme is passed into a link to AboutMe-Copmponent as state, which works, but when i now change the state of theme it only updates in Topbar. I even tried Useeffect. Only when I refresh the site the change is noticed. I read hours about this but I cant solve it somehow.
AppComponent (not all code just the necessary):
function App() {
const [theme, setTheme] = useState('dark')
return (
<Topbar theme={theme}></Topbar>
<ToggleButton variant='light' onClick={() => setTheme('light')}>Light</ToggleButton>
<ToggleButton variant='dark' onClick={() => setTheme('dark')}>Dark</ToggleButton>
TopbarComponent:
export default function Topbar({theme}) {
console.log('Topbar',theme)
React.useEffect(()=>{
console.log('changed')
},[theme])
Output when I press the buttons:
Topbar light
changed
Topbar dark
changed
AboutMeComponent:
export default function AboutMe() {
const location = useLocation()
console.log(location.state)
React.useEffect(() => {
console.log('About-Me',location.state)
},[location])
Initial output:
dark
About-Me dark
When I now press the other Button I only get the Topbar Output
Only when refreshing I get the AboutMe Outputs again.
PS
The theme is changed anyway from dark to light but i need this state to change fonts etc.
I would suggest sticking with documentation's recommendation which is to use useContext for very this example of setting theme using context.
Check out: https://beta.reactjs.org/apis/react/useContext
Usage : Passing data deeply into the tree
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext);
useContext returns the context value for the context you passed. To determine the context value, React searches the component tree and finds the closest context provider above for that particular context.
To pass context to a Button, wrap it or one of its parent components into the corresponding context provider:
function MyPage() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
);
}
function Form() {
// ... renders buttons inside ...
}
It doesn’t matter how many layers of components there are between the provider and the Button. When a Button anywhere inside of Form calls useContext(ThemeContext), it will receive "dark" as the value.
I have it working now with the useContext hook. Thank you i somehow forgot about it.
App:
export const ThemeContext = React.createContext()
function App() {
const [theme, setTheme] = useState('black')
console.log(theme)
return (
<ThemeContext.Provider value={{backgroundColor:theme}}>
<BrowserRouter>
<div className='App' id={theme}>
<Topbar/>
<div className="position-absolute top-0 start-0">
<ToggleButton variant='light' onClick={() => setTheme('white')}>Light</ToggleButton>
<ToggleButton variant='dark' onClick={() => setTheme('black')}>Dark</ToggleButton>
</div>
Topbar:
export default function Topbar() {
const {user,logout} = UserAuth()
const [error, setError] = useState('')
const navigate = useNavigate()
const style = useContext(ThemeContext)
console.log(style)
AboutMe:
export default function AboutMe() {
const style = useContext(ThemeContext)
console.log(style)
return (
<>
<div className='d-flex' style={style}>
I had to move my Routing from Index.js to App.js because it had to be wrapped in the Context provider, but now my theme gets passed into every single component.

How to set useState while checking propsType

hello I'm trying to set useState but it crashing the whole code. here I'm sending propstype from parent component to child component. so when propstype condition is checked/true then I'm trying to set useState but instead of setting it crashes. please checked below what I tried, Here I've removed unwanted code otherwise it'll too big for readers and they might lost the focus from question. can anyone suggest me any new solution for this I've tried a lot of and then came here for the proper solution.
parent component (BillComponent)
const BillComponent = () => {
return(
{/* Add Bill form */}
<Drawer
title={"Add Bill"}
placement="right"
onClose={onClose}
visible={visible}
className="ant-drawer-half"
>
<AddBill parentCallback={handleCallback} type="add" /> // <---This AddBill is child component and here I'm passing props type as add
</Drawer>
</>
</>
);
};
export default BillComponent;
child component (AddBill)
const AddBill = (props) => {
const [checked, setChecked] = useState(false);
if(props.type === "add"){
setChecked(false); //<---This is where main code is crashing. when I set useState
}else{
setChecked(true);
}
return(
// divs of project
);
};
export default AddBill;
Not sure if that's your case because you didn't show the error. Probably the error will be something related to "Too many re-renders".
It's a bad practice to use those React hooks in the "root" of the component. In your particular case, I'd do the following:
const [checked, setChecked] = useState(false);
useEffect(() => {
if (props.type === "add") {
setChecked(false);
} else {
setChecked(true);
}
}, [props.type]);
Please share the error you're getting.
Also why not just use:
const [checked, setChecked] = useState(props.type === 'add');

Trigger usePosition() custom hook onClick element

I'm facing some troubles when I try to trigger usePosition hook in a onClick event.
What I want to achieve is to delay the geolocation permission prompt triggered by the browser until the user clicks some element.
What I've tried so far is a bunch of variations of the following code, but without success:
const IndexPage = () => {
const [geolocation, setGeolocation] = useState({});
const [isGeolocationActive, triggerGeolocation] = useState(false);
let currentPosition = usePosition();
function handleGeolocation() {
if (isGeolocationActive) {
setGeolocation(currentPosition)
console.log('geolocation', geolocation)
} else triggerGeolocation(true);
}
return (
<Layout>
<Wrapper type={`list`} classNames={`wrapper pet__cards cards__list`}>
<Card>
<Image/>
<div onClick={() => handleGeolocation()}>Click me to share my location</div>
I've tried to set a useState hook (initially set to false) which should control and handle the usePosition hook if is set to true but the browser still asking for geolocation permission as soon as I land on the page.
Edit: I've also tried to put the usePosition hook in another component and call it onClick event. However, in this case, I face some hooks rules error such as:
"Invalid hook call. Hooks can only be called inside of the body of a
function component. This could happen for one of the following
reasons..."
Finally I've solved the issue using usePosition hook in another component, something like this:
import React from "react";
import {usePosition} from "use-position";
const Geolocation = () => {
let currentPosition = usePosition();
return (
<div>
<p>{currentPosition.latitude}</p>
</div>
)
};
export default Geolocation;
While in my main component I've used a useState hook which controls the rendering like this:
const IndexPage = () => {
const [geolocation, setGeolocation] = useState('');
function handleGeolocation() {
setGeolocation(<Geolocation/>);
}
return (
<Layout>
<Card>
<Image/>
<div onClick={() => handleGeolocation()}>Click me to share my location</div>
{geolocation}
...

How to reset location state in react router

I am following react-router docs to create a protected route HOC and for unauthenticated requests I am Redirecting the user as following :
<Redirect to={{
pathname: '/input-number',
state: { from: props.location },
}} />
Now on the redirected component, I show an error message using the following logic :
this.props.location.state && this.props.location.state.from &&
(
<Grid.Row>
<Grid.Column>
<Message negative>
<Message.Header>You must be logged in to visit the page</Message.Header>
</Message>
</Grid.Column>
</Grid.Row>
)
The problem is when the user reloads, the page the state associated with the location is not cleared and the error message is shown on each refresh once user has been redirected to the login component. I am looking for a way to clear the state. I think it must be possible without setting some app state.
UPDATE : I am adding the complete PrivateRoute for clarity:
`
export default function PrivateRoute({ component: Component, ...rest }) {
const isAuthenticated = localStorage.getItem(ACCESS_TOKEN);
return (
<Route
{...rest}
render={props => (
isAuthenticated ? (
<Component {...props} />
) : (
<Redirect to={{
pathname: '/input-number',
state: { from: props.location },
}}
/>
)
)}
/>);
}
`
When creating the history object for your <Router> during the initial setup of the React app, you could replace the state on the history's location with a state that does not include the from prop.
const history = createHistory();
if (history.location && history.location.state && history.location.state.from) {
const state = { ...history.location.state };
delete state.from;
history.replace({ ...history.location, state });
}
Also, it is possible to replace the state without merging location it statys untouched:
const { state } = this.props.location;
if (state && state.copySurveyId) {
this.duplicateSurvey(state.copySurveyId);
const stateCopy = { ...state };
delete stateCopy.copySurveyId;
this.props.history.replace({ state: stateCopy });
}
This might not be the most ideal approach but in my case the easiest way to deal with this was to use history.replace() and replace the current URL with the same URL but omitting location location state object (should work for params too). However you have to be careful and only replace the URL AFTER you have consumed the value of location state object or params. In my case:
// Somwhere in the app:
history.push("to/app/route", {
contentOfLocationState: true
});
// In the component that is mapped to above route
function (){
try {
// Post to API
Post(...{ contentOfLocationState });
} catch (e) {
// error
} finally {
history.replace("to/app/route");
}
PS: Using the technique that #robert-farley described react router would start acting up and show white screen for me. Might have been something wrong with my setup though

Categories

Resources