Render timeout dialog in other route components - reactjs

I have a timeout dialog that I want to render in various components when a token expires at X minutes (I have hardcoded the times for now). Material-UI Dialog is being used for the dialog pop up. I have two timeouts set, one at 29min:30sec for a warning message and 30min will force the logout.
I have looked at the following example for ideas.
const AlertDialog = props => {
const [warningTimeout, setWarningTimeout] = useState(1770000);
const [limitTimeout, setLimitTimeout] = useState(1800000);
const [open, setOpen] = useState(true);
const handleContinue = () => {
//this will make an api call to refresh the token, where we must update the context with the new token
setOpen(false);
};
const handleLogout = () => {
//this will destroy the session, clear localstorage
};
const warn = () => {
console.log("Warning, 30 minutes has past");
};
const logout = () => {
console.log("You have been logged out");
};
const setTimeouts = () => {
const warnTimeout = setTimeout(warn, warningTimeout);
const logoutTimeout = setTimeout(logout, limitTimeout);
};
useEffect(() => {
setTimeouts();
});
return (
<div>
<Dialog
disableBackdropClick
disableEscapeKeyDown
open={open}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Session Timeout"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
If you wish to continue the current session please press continue.
You have 30 seconds to decide or you will be logged out.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleLogout} color="primary">
Logout
</Button>
<Button onClick={handleContinue} color="primary" autoFocus>
Continue
</Button>
</DialogActions>
</Dialog>
</div>
);
};
export default AlertDialog;
The project has protected routes so they look like this. I want to pass this alert dialog to all the <AdminPrivateRoute>'s
const Routes = () => (
<UserProvider>
<TokenProvider>
<Router>
<Switch>
<Route exact path="/" component={LoginForm} />
<AdminPrivateRoute path="/dashboard" component={Dashboard} />
<AdminPrivateRoute path="/secondary-page" component={SecondaryPage} />
</Switch>
</Router>
</TokenProvider>
</UserProvider>
);
Is wrapping the above components with alert dialog a viable option?
<AdminPrivateRoute path="/dashbaord" component={AlertDialog(Dashboard)} />
Is there a better approach?
Codesandbox for reference, you can use any credentials to log in.

Placing the <AlertDialog /> component outside the <Switch /> seems to do the trick.
const Routes = () => (
<Router>
<AlertDialog />
<Switch>
<Route exact path="/" component={LoginForm} />
<AdminPrivateRoute path="/dashboard" component={Dashboard} />
</Switch>
</Router>
);

Related

Question about useContext and re-rendering

Considering a situation where I have an App component, and Contexts components, called 'AuthContext' and 'usersAndProductsContext'.
In the context components I have few states, which are initialized in App component and from there will be given to other components to share the states like so:
return (
<div className="App">
<usersAndProductsContext.Provider
value={{
usersVal: usersVal,
productsVal: productsVal,
toggle_pressed_login_flag: toggle_pressed_login_flag,
handleAddUser: handleAddUser,
}}
>
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
edit_currentLoggedUserId: edit_currentLoggedUserId,
currentLoggedUserId: currentLoggedUserId,
isLoggedInEditVal: isLoggedInEditVal,
}}
>
<Routes>
<Route exact path="/" element={<Login />} />
<Route exact path="farmers" element={<Farmers />} />
<Route exact path="customer" element={<Customer />} />
<Route exact path="register" element={<Register />} />
<Route exact path="farmershop/:id" element={<Farmer />} />
<Route exact path="mystore/" element={<MyStore />} />
</Routes>
</AuthContext.Provider>
</usersAndProductsContext.Provider>
</div>
);
when I pass to other components, as I did using 'Provider', it means I'm sharing with all other components these states which are given within the 'value' prop.
Does that mean that every change of the passed-on-using-provider states, in other component, will mean that the information which was passed on in all components has now changed?
Or did it become some sort of local copy?
To emphasize what I mean:
function MyStore() {
let authCtx = useContext(AuthContext);
let usersAndItemsCtx = useContext(usersAndProductsContext);
...
if I change usersAndItemsCtx from MyStore, will it mean that all other component's state that I have changed?
Hope it made sense?
Regards!
It depends on what you mean by change. If you mean mutating the state then no, but if you mean update the state then yes.
If you do authCtx.someProperty = false;, then do not expect the other components to rerender. The actual way to update the context would by passing a setter function down the context and using it.
Looking at this, there seems to be a handleAddUser, if this is called and it updates the context only then a change will reflected in other components.
value={{
usersVal: usersVal,
productsVal: productsVal,
toggle_pressed_login_flag: toggle_pressed_login_flag,
handleAddUser: handleAddUser,
}}
Here is a sandbox where I have mutated the context. Note, how the setInterval logs the mutated value but there is no update to the UI because the component never rerendered. These kind of bugs are common when mutating state (or context by extension).
The relevant code:
const TextContext = createContext();
function App() {
const [text, setText] = useState({
val: "string",
count: 0
});
return (
<>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<TextContext.Provider value={[text, setText]}>
<Wrapper />
</TextContext.Provider>
</div>
</>
);
}
function Wrapper(props) {
return (
<div style={{ padding: "20px" }}>
<TextControlWithHook />
<br />
<Random />
</div>
);
}
function Random() {
const [text, setText] = useContext(TextContext);
const mutateContext = () => {
text.val = "1231232";
};
return <button onClick={mutateContext}>MUTATE</button>;
}
function TextControlWithHook(props) {
const [text, setText] = useContext(TextContext);
useEffect(() => {
console.log("outside", { text });
const id = setInterval(() => {
console.log(text);
}, 5000);
return () => clearInterval(id);
}, [text]);
return (
<input
className="form-control"
value={text.val}
onChange={(e) => {
const val = e.target.value;
setText((text) => ({ ...text, val }));
}}
/>
);
}

React : Cycle life problem expected / redirect problem

For my first project with React, i tried to use useContext() for call my data mocked.
But after that, i have a problem.
The problem :
You go to a housing page, you change the apartment ID in the URL with a Dummy ID and you update it.
Great! The redirection to the 404 is done.
So you go back to the home page by clicking on the link of the 404.
You go back to any housing page => the 404 is always present.
You repeat, you then return to the home page and you click again on a housing card and WOW the problem no longer exists.
My Provider
import React, { useState, createContext } from "react"
import fetchLocationData from "../../services/localFetch"
export const FetchDataContext = createContext()
export const FetchDataProvider = ({ children }) => {
const [locationData, setLocationData] = useState({})
const [locationsData, setLocationsData] = useState([])
const [allLocationLoading, setAllLocationLoading] = useState(false)
const [isLocationLoading, setIsLocationLoading] = useState(false)
const [errorAPI, setErrorAPI] = useState(false)
const [error404, setError404] = useState(false)
async function fetchLocationById(locId) {
try {
setError404(false)
const response = await fetchLocationData.getLocById(locId)
response ? setLocationData(response) : setError404(true)
} catch (err) {
console.log(err)
setErrorAPI(true)
} finally {
setIsLocationLoading(true)
console.log("in provider :", error404)
}
}
async function fetchAllLocations() {
try {
const response = await fetchLocationData.getAll()
setLocationsData(response)
} catch (err) {
console.log(err)
setErrorAPI(true)
} finally {
setAllLocationLoading(true)
}
}
return (
<FetchDataContext.Provider
value={{
errorAPI,
error404,
isLocationLoading,
allLocationLoading,
locationData,
locationsData,
fetchLocationById,
fetchAllLocations,
}}
>
{children}
</FetchDataContext.Provider>
)
}
My router
function App() {
return (
<>
<GlobalStyle />
<BlocPage>
<Header />
<FetchDataProvider>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/apartment/:locId" element={<ProfileLocation />} />
<Route path="*" element={<Error />} />
</Routes>
</FetchDataProvider>
</BlocPage>
<Footer />
</>
)
}
My housing page
function ProfileLocation() {
const { locId } = useParams()
const {
errorAPI,
error404,
locationData,
isLocationLoading,
fetchLocationById,
} = useContext(FetchDataContext)
useEffect(() => {
fetchLocationById(locId)
}, [])
const rating = parseInt(locationData.rating)
if (errorAPI) {
return (
<span>
Oups une erreur est survenue ... Veuillez recommencer ultérieurement.
</span>
)
}
if (error404) {
return <Navigate to="/error404" />
}
return (
<div>
{isLocationLoading ? (
<>
<Slider pictures={locationData.pictures} />
<ResponsiveWrapper>
<FirstSectionWrapper>
<Title>{locationData.title}</Title>
<Geolocation>{locationData.location}</Geolocation>
<TagsWrapper>
{locationData.tags.map((tag, index) => (
// eslint-disable-next-line react/no-array-index-key
<Tag key={`${tag}-${index}`} label={tag} />
))}
</TagsWrapper>
</FirstSectionWrapper>
<SecondSectionWrapper>
<OwnerWrapper>
<HomeOwnerName>{locationData.host.name}</HomeOwnerName>
<HomeOwerPicture
src={locationData.host.picture}
alt={locationData.host.name}
/>
</OwnerWrapper>
<StarsWrapper>
<StarScale ratingValue={rating} starType="full" />
<StarScale ratingValue={rating} starType="empty" />
</StarsWrapper>
</SecondSectionWrapper>
</ResponsiveWrapper>
<CollapseSectionWrapper>
<CollapseWrapper>
<Collapse
pageType="profil"
label="Description"
contentType="paragraph"
contentText={locationData.description}
/>
</CollapseWrapper>
<CollapseWrapper>
<Collapse
pageType="profil"
label="Équipements"
contentType="list"
contentText={locationData.equipments}
/>
</CollapseWrapper>
</CollapseSectionWrapper>
</>
) : (
<Loader />
)}
</div>
)
}
export default ProfileLocation
Maybe it’s an async code problem?
The issue is that only navigating forward to a route rendering a component that calls fetchLocationById will ever manage to clear/set the error404 state back to false.
I suggest instead of passing error404 back to the components that the FetchDataProvider handles rendering the error UI upon the error condition. Having it redirect to a specific error page.
Example:
<FetchDataProvider>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/apartment/:locId" element={<ProfileLocation />} />
<Route path="/404" element={<Error404 />} /> // <-- add route
<Route path="*" element={<Error />} />
</Routes>
</FetchDataProvider>
...
const Error404 = () => (
<div>Oups une erreur est survenue ... Veuillez recommencer ultérieurement.</div>
);
...
import { useNavigate } from 'react-router-dom';
export const FetchDataProvider = ({ children }) => {
const navigate = useNavigate();
...
async function fetchLocationById(locId) {
try {
setError404(false)
const response = await fetchLocationData.getLocById(locId)
if (response) {
setLocationData(response);
} else {
navigate("/error404", { replace: true });
}
} catch (err) {
navigate("/error404", { replace: true });
} finally {
setIsLocationLoading(true);
}
}
...
return (
...
);
}
...
function ProfileLocation() {
const { locId } = useParams();
const {
locationData,
isLocationLoading,
fetchLocationById,
} = useContext(FetchDataContext);
useEffect(() => {
fetchLocationById(locId);
}, [fetchLocationById, locId]); // <-- don't forget to add proper dependencies
const rating = parseInt(locationData.rating);
return (
<div>
{isLocationLoading ? (
<>
<Slider pictures={locationData.pictures} />
<ResponsiveWrapper>
<FirstSectionWrapper>
<Title>{locationData.title}</Title>
<Geolocation>{locationData.location}</Geolocation>
<TagsWrapper>
{locationData.tags.map((tag, index) => (
// eslint-disable-next-line react/no-array-index-key
<Tag key={`${tag}-${index}`} label={tag} />
))}
</TagsWrapper>
</FirstSectionWrapper>
<SecondSectionWrapper>
<OwnerWrapper>
<HomeOwnerName>{locationData.host.name}</HomeOwnerName>
<HomeOwerPicture
src={locationData.host.picture}
alt={locationData.host.name}
/>
</OwnerWrapper>
<StarsWrapper>
<StarScale ratingValue={rating} starType="full" />
<StarScale ratingValue={rating} starType="empty" />
</StarsWrapper>
</SecondSectionWrapper>
</ResponsiveWrapper>
<CollapseSectionWrapper>
<CollapseWrapper>
<Collapse
pageType="profil"
label="Description"
contentType="paragraph"
contentText={locationData.description}
/>
</CollapseWrapper>
<CollapseWrapper>
<Collapse
pageType="profil"
label="Équipements"
contentType="list"
contentText={locationData.equipments}
/>
</CollapseWrapper>
</CollapseSectionWrapper>
</>
) : (
<Loader />
)}
</div>
);
}

React Router Link component not rendering when redirect

I'm using the material-ui library's component Autocomplete with React flux to create a Search Bar that finds users.
I want every option to be a link to a page with more information of that user. Currently, it only works the first time I click on an option. Every time after that it changes the URL but the page is not re-rendered.
This is the Autocomplete code:
export function SearchBar() {
const [url, setUrl] = useState("/perfil/");
const { store, actions } = useContext(Context);
useEffect(() => {
actions.search();
}, [url]);
const artistas = store.artistas.slice();
return (
<Autocomplete
id="autocomplete"
freeSolo
disableClearable
options={artistas}
getOptionLabel={(option) => option.nombre + " " + option.apellido}
renderOption={(option) => (
<Link
className="text-black text-decoration-none"
onClick={() => setUrl(url + option.id)}
to={`/perfil/${option.id}`}
>
{option.nombre} {option.apellido}
</Link>
)}
renderInput={(params) => (
<TextField
{...params}
size="small"
placeholder="Search for your artist"
margin="normal"
variant="outlined"
InputProps={{ ...params.InputProps, type: "search" }}
/>
)}
/>
);
}
This is the page where it is redirected:
export const Perfil = () => {
const { user_id } = useParams();
return (
<>
<About user_id={user_id} />
<Galeria user_id={user_id} />
</>
);
};
The flux action is this:
search: async () => {
try {
const response = await fetch(
process.env.BACKEND_URL + "/api/artistas"
);
const data = await response.json();
setStore({
artistas: data,
});
} catch (error) {
console.log("Error loading message from /api/artistas", error);
}
}
At last, this is the layout page:
return (
<div>
<BrowserRouter basename={basename}>
<ScrollToTop>
<Navbar />
<Routes>
<Route element={<Inicio />} path="/" />
<Route element={<Login />} path="/login" />
<Route element={<Registro />} path="/registro" />
<Route element={<Perfil />} path="/perfil/:user_id" />
<Route element={<Producto />} path="/producto/:theid" />
<Route
element={<ConfiguracionUsuario />}
path="/configuracion/:theid"
/>
<Route element={<h1> Not found! </h1>} />
</Routes>{" "}
<Footer />
</ScrollToTop>{" "}
</BrowserRouter>{" "}
</div>
);
I fixed it. The problem was in the Galeria component. It took the id by props:
useEffect(() => {
actions.producto_galeria(props.user_id);
}, []);
But we should have used a stored variable with the ID that changes on every click with a flux action, instead of props. So now, I get the ID as follows:
flux new action:
get_artista_id: (artista_id) => {
setStore({ artista_id: artista_id });
}
Link new onClick:
<Link
className="text-black text-decoration-none"
onClick={() => {
actions.get_artista_id(option.id);
}}
to={`/perfil/${option.id}`}
>
{option.nombre} {option.apellido}
</Link>
useEffect on Galeria to detect the change on the ID:
useEffect(() => {
actions.producto_galeria(store.artista_id);
}, [store.artista_id]);
Link tags work first move to url. After that onClick logic will start after moved link.
So if you want to use Link tag with onClick then i recommend two ways.
react-router-dom v5 or lower : just don't use 'to' property and just use onClick and in the onClick property you can use useHistory hook inside onClick.
You can use under 2. Option here too.
react-router-dom v6 or lower : use 'a' tag with onClick property you can use useNavigate hook inside onClick.

Where to call http requests regarding optimization in React

I have an axios request that allows me to get user information if he's logged and display it on his profile page.
My question is where to call it to avoid too many request and enhance optimization.
I've seen people calling it on top in App.js but I find it weird. I'm currently calling it in my container component that render UserProfile or LogForm depending if user is logged, but then a request is made each time user clicks on his profile page and I don't feel that it is the right way.
Here is my code:
Profile.tsx
const Profile = () => {
const dispatch = useDispatch();
const userId = useSelector((kinaccess: RootStateOrAny) => kinaccess.formsReducer.signInForm.success.userId);
useEffect(() => {
dispatch(Action.getUserInfo(userId));
}, [userId, dispatch]);
return <div>{userId ? <UserProfile /> : <LogForm />}</div>;
};
UserProfile.tsx
const UserProfile = () => {
const userInfo = useSelector((kinnacess: RootStateOrAny) => kinnacess.userReducer.user.success);
if (!userInfo) return null;
const { name, firstName, email } = userInfo;
return (
<section className='user-profile'>
<h3>
Hello {firstName} {name}
</h3>
<p>Is your mail adress still {email}</p>
</section>
);
};
An my Routes.tsx
const Routes = () => {
return (
<Routing>
<Route index element={<Home />} />
<Route path='contact' element={<ContactForm />} />
<Route path='profile' element={<Profile />} />
<Route path='*' element={<Error404 />} />
</Routing>
);
};

history.push is doing an extra redirect, and ignoring the search parameters, if the user is already on the redirect page

I have a simple Navbar with a Form within it:
const NavBar = () => {
let history = useHistory()
...
...
return (
...
<Form inline onSubmit={handleSubmit}>
<InputGroup style={{width: "90%"}}>
<Form.Control id="navbar-search" placeholder="Pesquise" size="sm"/>
<Form.Control as="select" size="sm">
<option>Ações</option>
<option>Declarações</option>
</Form.Control>
<InputGroup.Append size="sm">
<Button size="sm" type="submit" variant="outline-secondary">
<SearchIcon fontSize="small" />
</Button>
</InputGroup.Append>
</InputGroup>
</Form>
...
)
handleSubit is suppose to redirect (using history.push) to a path that I'm going to use the input value as a search parameter. I'm using react-router-dom.
const handleSubmit = (e) => {
let baseEndpoint = e.target[1].value === "Ações" ? "actions" : "quotes"
history.push({
pathname: `/${baseEndpoint}/query`,
search: `?description=${e.target[0].value}`,
})
}
Everything looks good except if the user is in the page the handleSubmit is going to redirect, ie, at /${baseEndpoint}/query.
If the user is in this page, history goes to /${baseEndpoint}/query?description=${e.target[0].value} and re-renders automatically to /${baseEndpoint}/query?.
I've also tried using history.replace, but it didn't work.
const handleSubmit = (e) => {
let baseEndpoint = e.target[1].value === "Ações" ? "actions" : "quotes"
let url = `/${baseEndpoint}/query`
if(history.location.pathname === url) {
history.replace({
pathname: url,
search: `?description=${e.target[0].value}`,
})
return
}
history.push({
pathname: url,
search: `?description=${e.target[0].value}`,
})
}
What is causing this behaviour? What am I doing wrong?
Thanks a lot!
(EDIT) My Switch and Routes:
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
class Wrapper extends Component {
render() {
return (
<>
<Container className="main">
<MetaData />
<Navbar />
<this.props.component {...this.props} />
<br />
<br />
<Footer />
</Container>
</>
)
}
}
export default function App() {
return (
<Router>
<Switch>
<Route exact path="/"
render={props => (
<Wrapper {...props} component={Home} />
)}
/>
<Route
exact path="/actions"
key="action-home"
render={props => (
<Wrapper
{...props}
image={bozoAction}
baseEndpoint="actions"
component={EntityHome}
/>
)}
/>
<Route
path="/actions/query"
key="action-query"
render={props => (
<Wrapper
{...props}
image={bozoAction}
baseEndpoint="actions"
component={EntityQuery}
/>
)}
/>
<Route
path="/actions/:id(\d+)"
key="action-entity"
render={props => (
<Wrapper
{...props}
image={bozoAction}
baseEndpoint="actions"
component={Entity}
/>
)}
/>
<Route
exact path="/quotes"
key="quote-home"
render={props => (
<Wrapper
{...props}
image={bozoQuote}
baseEndpoint="quotes"
component={EntityHome}
/>
)}
/>
<Route
path="/quotes/query"
key="quote-query"
render={props => (
<Wrapper
{...props}
image={bozoQuote}
baseEndpoint="quotes"
component={EntityQuery}
/>
)}
/>
<Route
path="/quotes/:id(\d+)"
key="quote-entity"
render={props => (
<Wrapper
{...props}
image={bozoQuote}
baseEndpoint="quotes"
component={Entity}
/>
)}
/>
...
</Switch>
</Router>
)
}
Sandbox: https://codesandbox.io/s/0y8cm
Form some reason the sandbox is not redirecting and ignoring the search parameters, but we can evaluate the error doing the following: use the navbar to search one of the entities. Check the form (clicking in "Mais filtros") and see the provided query param at the input. Do another search in the navbar to the same entity. Check the form and see no initial values.
What's causing the behavior is the form submission event, it refreshes the page resulting in the loss of the query params, the solution to this is e.preventDefault() when you submit the form :
const handleSubmit = (e) => {
e.preventDefault(); // prevent the page refresh
let baseEndpoint = e.target[1].value === "Ações" ? "actions" : "quotes";
history.push({
pathname: `/${baseEndpoint}/query`,
search: `?description=${e.target[0].value}`
});
};
You can add history.go() if you absolutely have to refresh the page, but i wouldn't recommend that, the goal is to let the child components know that the search has changed,
The way your EntityQuery and QueryForm is set up makes it impossible for the child components to know if there is a change, so you need to fix that,
You shouldn't set the initial state value from the props in the constructor as it's anti-pattern , instead, have the initial values empty and use life cycle methods to update them ( componentDidMount and componentDidUpdate )
EntityQuery
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: true,
hasMore: false,
searchParams: {
page: 1,
tags: "",
description: ""
},
entities: []
}
}
updateStateValues = () => {
const _initialTag = this.props.location.search.includes("tags")
? this.props.location.search.split("?tags=")[1]
: "";
const _initialText = this.props.location.search.includes("description")
? this.props.location.search.split("?description=")[1]
: "";
this.setState({
searchParams: {
page: 1,
tags: _initialTag,
description: _initialText
}
})
}
componentDidMount() {
this.updateStateValues()
}
componentDidUpdate(prevProps) {
// add checks for other potential props that you need to update if they change
if (prevProps.location.search.split("?description=")[1] !== this.props.location.search.split("?description=")[1])
this.updateStateValues()
}
And pass the values from the state to the child :
<QueryForm
baseEndpoint={this.props.baseEndpoint}
tagsInitialValues={this.state.searchParams.tags}
textInitialValue={this.state.searchParams.description}
setSearchParams={this.setSearchParams}
/>
Updated CodePen
EDIT:
Fllowing up the OP's comment and the updated SandBox, there's another issue regarding Formik where the initialValues do not update when the props change, see : https://github.com/formium/formik/issues/811
The suggested solution of adding enableReinitialize didn't work, so, to force the component to update, you can use a key that changes when the url changes, in this case, use this.props.textInitialValue :
QueryForm :
<Formik
key={this.props.textInitialValue}
...
Try adding e.prevenDefault() at the start of the handleSubmit function.
Maybe <form> submission is causing a page reload and hence, removing the search parameters.
Try to prevent the default behavior of the form on submission by changing your handleSubmit() handler to:
const handleSubmit = (e) => {
e.preventDefault()
let baseEndpoint = e.target[1].value === "Ações" ? "actions" : "quotes"
history.push({
pathname: `/${baseEndpoint}/query`,
search: `?description=${e.target[0].value}`,
})
}
Let me if it works *

Resources