React Router v2.7 to v6 onEnter Migration - javascript

I am trying to migrate an application running on router v3, using the onEnter attribute for route after auth.
onEnter function in YAMain.jsx
static onEnter(store) {
return (nextState, replaceState, callback) => {
// Check if the user is logged in and allowed to make requests before letting them proceed
store.dispatch({
type: IS_LOGGED_IN,
onLoggedIn: () => {
store.dispatch({
type: GET_SELECTED_LOCATION_AND_CLASSROOM,
onSuccess: callback,
onFailure: () => {
// Get all of the required information from the store
const profile = getProfile(store.getState());
const selectedClassroom = getSelectedClassroom(store.getState());
const selectedLocation = getSelectedLocation(store.getState());
// No location has been selected by an admin, go to the locations page
if (profile.get('accessLevel') !== 'E' && !selectedLocation.get('id')) {
// Return early if this is the page we are going to
if (nextState.location.pathname.startsWith('/location')) {
return callback();
}
replaceState('/location');
return callback();
}
// No classroom has been selected by a user, go to the classrooms page
if (!selectedClassroom.get('id')) {
// Return early if this is the page we are going to
if (nextState.location.pathname.startsWith('/classroom')) {
return callback();
}
replaceState('/classroom');
return callback();
}
return callback();
}
});
},
onNotLoggedIn: () => {
replaceState('/login');
callback();
},
onFailure: (error) => {
if (isTimeGateError(error)) {
replaceState('/locked');
callback();
}
}
});
};
render function in YARouter.jsx, both classes extend component.
render() {
return (
<BrowserRouter>
<Routes>
{/* Handles the main logic and renders all but one of the pages */}
<Route
exact path="/"
element={<YAMain/>
}
onEnter={YAMain.onEnter(this.props.store)}
>
<Route path="/" element={YADashboard}/>
{/* Locations page displays the available list of locations */}
<Route
path="location"
element={YALocations}
/>
{/* Classrooms page displays the available list of classrooms */}
<Route
path="classroom"
element={YAClassrooms}
/>
this is not the entirety of the routing but should be enough to give you an idea of what's going on.
This is what I have now, I have tried a number of things suggested on various places. I'm trying to understand how I can either make this work so I can move on and work on this and make it proper later, OR make it proper now and fix this issue.
How can I go about ensuring proper redirection for user authentication, I've spent 2 days at work trying to figure anything out and am completely stuck.
Thanks.

If you are just looking for a way to call onEnter when the route is matched and rendered then I think calling it in a mounting useEffect hook in a wrapper component is probably what you are after.
Example:
const YAMainWrapper = ({ children, onEnter }) => {
useEffect(() => {
onEnter();
}, []);
return children;
};
...
render() {
return (
<BrowserRouter>
<Routes>
{/* Handles the main logic and renders all but one of the pages */}
<Route
path="/"
element={(
<YAMainWrapper onEnter={YAMain.onEnter(this.props.store)}>
<YAMain />
</YAMainWrapper>
)}
>
<Route path="/" element={<YADashboard />} />
{/* Locations page displays the available list of locations */}
<Route path="location" element={<YALocations />} />
{/* Classrooms page displays the available list of classrooms */}
<Route path="classroom" element={<YAClassrooms />} />
...
</Route>

Related

How can I make an asynchronous setState before following a Link?

I want to be able to 'capture' a selection of a user clicking a link that takes them to another page. I need the users selection to display a detail page of thebselected image.
I'm facing the problem that the browser follows the link before react updated the state "key". The state-Change of key is not passed to the details page. Is there an easy way to fix that without fetch?
export default class IGallery extends React.Component {
constructor(props) {
super(props);
this.state = {
countryList: [],
wallet: "",
key: "",
};
}
handleClick = (_key) => {
console.log("before setState", this.state.key);
this.setState({ key: _key }, () =>
console.log("after setState", this.state.key)
);
};
render() {
return (
<div className={styles.gallery}>
<h1>Gallery</h1>
<ImageList cols={6} rowHeight={320}>
{this.state.countryList.map((data, idx) => (
<ImageListItem key={idx} className={styles.imageItem}>
<Link to="/item" onClick={() => this.handleClick(data.key)}>
<img src={data.image} width={320} />
<ItemDetail id={this.state.key}></ItemDetail>
</Link>
<ImageListItemBar
title={"Braintube"}
subtitle={data.key}
actionIcon={<FavoriteBorder fontSize="large" color="pink" />}
actionPosition="top"
/>
</ImageListItem>
))}
</ImageList>
</div>
);
}
}
I expect that
<ItemDetail id={this.state.key}></ItemDetail>
passes the state value to the child component itemDetail.
Here is my Routing Path from index.js
<Router>
<Header childToParent={childToParent}/>
<Switch>
<Route path="/" exact={true}>
<Home></Home></Route>
<Route path="/project-space">
<ProjectSpace childToParent={wallet}></ProjectSpace>
</Route>
<Route path="/about">
<About></About></Route>
<Route path="/item"><ItemDetail></ItemDetail></Route>
</Switch>
<FooterMain></FooterMain>
</Router>
I think we need to take a step back and understand the React paradigm to answer this question.
In React, state goes only one way and is not retained when a component is unmounted. Right now, we have the following
Router > SomePage > IGallery (State = ....)
and we're trying to redirect to:
Router > ItemPage
As you can see here, moving away to ItemPage will drop state because Router will re-render and SomePage will be unmounted.
Therefore, we have two options:
Pass this item id in the url parameter which will then be handled by the next page
Move the state to the router parent and pass the state's setter + getter down to the page components (unrecommended)
For your situation, option one is more intuitive.

localStorage removing elements in array after browser refresh

I have a react app and I want to persist the array of favorites when the page refreshes.
The data is set correctly, I can see it in the dev tools. But when i refresh the page, the data is removed. Any ideas why this may be?
Link to sandbox - https://codesandbox.io/s/sad-surf-sqgo0q?file=/src/App.js:368-378
const App = () => {
const [favourites, setFavourites] = useState([]);
useEffect(() => {
localStorage.setItem("favourites", JSON.stringify(favourites));
}, [favourites]);
useEffect(() => {
const favourites = JSON.parse(localStorage.getItem("favourites"));
if (favourites) {
setFavourites(favourites);
}
}, []);
return (
<FavContext.Provider value={{ favourites, setFavourites }}>
<HashRouter>
<Routes>
<Route path={"/"} element={<Dashboard />} />
<Route path={"/favorites"} element={<Favorites />} />
</Routes>
</HashRouter>
</FavContext.Provider>
);
};
export default App;
Make sure to set item if array is not empty
useEffect(() => {
if(favourites.length) localStorage.setItem("favourites", JSON.stringify(favourites));
}, [favourites]);
Yes, it is because when you reload the app the useEffect will trigger and you have an empty array in your favorite for the first time so it set the empty array in local storage.
You can fix it by adding a simple check
useEffect(() => {
if(favourites.length > 0){
localStorage.setItem("favourites", JSON.stringify(favourites));
}
}, [favourites]);
By this setItem only work when there is something in the favorites state

How to call a react component from another component and pass required props

At the moment, I have the following routes in my App.js file:
<Switch>
<Route exact path="/new-job"
render={(props) => <NewJob jobName={jobName} setMenuSelection={handleMenuSelection} />}
/>
<Route exact path="/past-jobs"
render={(props) => <PastJobs setMenuSelection={handleMenuSelection} />}
/>
</Switch>
Now within my PastJobs component, I have the following button with onClick process:
<Button
onClick={() => {
setConfirmDialog({
isOpen: true,
title: `Copy Job ${item.id}?`,
onConfirm: () => { onCopy(item.job_info) }
})
}}
>
Copy
</Button>
that calls the following function:
const onCopy = (job_info) => {
setConfirmDialog({
...confirmDialog,
isOpen: false
})
history.push({
pathname: '/new-job',
state: { detail: job_info }
})
}
Within my <NewJob /> component, I have now setup the following as I thought I could access the state.detail but unfortunately it's null, i.e.:
import { useLocation } from 'react-router-dom';
function NewJob( { jobName, setMenuSelection } ) {
const { state } = useLocation();
if (typeof state !== 'undefined') {
const myVal = state.detail
console.log("myVal", myVal )
}
}
The issue that I am having and unsure how to approach is that within my onCopy function that is called from button onClick, how do I call the the <NewJob /> component whose path is exact path="/new-job" in App.js above and pass in the prop job_info ?
Direct calls to components actually does not exist. But what you are looking can be achieved in different ways.
Using state machine with event bus (redux, redux-saga)
Render props https://reactjs.org/docs/render-props.html
Bunch of callbacks drilled via props (HOC's)
Ref's https://reactjs.org/docs/refs-and-the-dom.html
I suggest to read more about them to actually understand if it matches your use case. Anyhow it is great experience to develop your skills also!

getDocs function does not work in first run

I try to get all docs from Firebase when user connect from localhost:3000/ (it automatically redirects to /profile) but it does not work in the first run. Then when the page is refreshed by a user, it works. How can I run it in first try? Code below:
try {
const querySnapshot = await getDocs(collection(db, "links"));
await querySnapshot.forEach((doc) => {
if (stUser.uid == doc.data().uid) {
links.push(doc.id);
}
});
} catch (e) {
console.log(e);
}
Redirects:
function first() {
if (!isLoggedIn.isLoggedIn) return <Redirect to="/auth" />;
}
function second() {
if (isLoggedIn.isLoggedIn) return <Redirect to="/profile" />;
}
return (
<div>
<Route exact path="/">
<Redirect to="/auth" />
</Route>
{handleRoute(images)}
<Route path="/auth" component={Dashboard}>
{second()}
</Route>
<Route strict path="/profile" component={HomePage}>
{first()}
</Route>
</div>
);
The problem here is that you are not setting this variable in your state, as mentioned by Frank in the comments, so whatever changes you make to this variable might not be refreshed until actually force them by refreshing the page. I recommend you try something like the following code:
const updatedArray = links;
await querySnapshot.forEach((doc) => {
if (stUser.uid == doc.data().uid) {
updatedArray.push(doc.id);
}
});
setLinks(updatedArray);
Also, you will need to set this earlier in your code:
const [cars, setLinks] = useState([]);
finally, I would recommend you to check this documentation for a useState deepdive.

React - Get Components to use data in props rather than new API request

I have a React application which makes a couple of API requests in a couple of components. I'm trying to modify my code so that rather than these components making new requests on componentDidMount() instead they use data that has been made available in props. I have this working for one component but not the other - as far as I can see there's no difference in who I'm handling these components so I think my method must be at fault.
The function which makes the API call is in app.js below. The result of the call is saved to state (wantedCards or ownedCards depending on the API call) and then passed to the component as a prop in React Router (BrowserRouter)
class App extends Component {
state = {
username: "",
ownedCards: "",
wantedCards: "",
data: "null",
loading: false,
error: false,
};
loadCardData = (props) => {
let path = props.path.split("/");
console.log("NEW PATH2 IS " + path[2]);
if (path[2] == "own" && props.ownedCards.length < 1) {
console.log("OWN TRUE");
var url = `https://apicall/getOwnedCards?user=${path[1]}`;
return axios
.get(`${url}`)
.then((result) => {
this.setState({
data: result.data,
ownedCards: result.data,
loading: false,
error: false,
});
})
.catch((error) => {
console.error("error: ", error);
this.setState({
error: `${error}`,
loading: false,
});
});
} else if (path[2] == "want" && props.wantedCards.length < 1) {
console.log("WANT TRUE");
var url = `https://webhooks.mongodb-realm.com/api/client/v2.0/app/cards-fvyrn/service/Cards/incoming_webhook/getWantedCards?user=${path[1]}`;
return axios
.get(`${url}`)
.then((result) => {
this.setState({
data: result.data,
wantedCards: result.data,
loading: false,
error: false,
});
})
.catch((error) => {
console.error("error: ", error);
this.setState({
error: `${error}`,
loading: false,
});
});
} else {
return;
}
render(props) {
return (
<div className="App">
<BrowserRouter path="foo">
<Navigation />
<Switch>
<Route
exact
path="/:username"
render={(props) => (
<Redirect to={`/${props.match.params.username}/own`} />
)}
/>
<Route
exact
path="/:username/own"
render={(props) => (
<NewTable
status="Own"
p1={props.location.pathname.split("/")[1]}
p2={props.location.pathname.split("/")[2]}
p3={props.location.pathname.split("/")[3]}
p4={props.location.pathname.split("/")[4]}
data={this.state.data}
ownedCards={this.state.ownedCards}
loadCardData={this.loadCardData}
path={props.location.pathname}
/>
)}
/>
<Route
exact
path="/:username/own/team/:id"
render={(props) => (
<CardColumns
p1={props.location.pathname.split("/")[1]}
p2={props.location.pathname.split("/")[2]}
p3={props.location.pathname.split("/")[3]}
p4={props.location.pathname.split("/")[4]}
status="Own"
ownedCards={this.state.ownedCards}
data={this.state.data}
loadCardData={this.loadCardData}
path={props.location.pathname}
/>
)}
/>
<Route
exact
path="/:username/want"
render={(props) => (
<NewTable
data={this.state.data}
wantedCards={this.state.wantedCards}
loadCardData={this.loadCardData}
p1={props.location.pathname.split("/")[1]}
p2={props.location.pathname.split("/")[2]}
p3={props.location.pathname.split("/")[3]}
p4={props.location.pathname.split("/")[4]}
status="Want"
path={props.location.pathname}
/>
)}
/>
<Route
exact
path="/:username/want/team/:id"
render={(props) => (
<CardColumns
p1={props.location.pathname.split("/")[1]}
p2={props.location.pathname.split("/")[2]}
p3={props.location.pathname.split("/")[3]}
p4={props.location.pathname.split("/")[4]}
status="Want"
wantedCards={this.state.wantedCards}
data={this.state.data}
loadCardData={this.loadCardData}
path={props.location.pathname}
/>
)}
/>
<Route
render={function () {
return <p>Not found</p>;
}}
/>
</Switch>
</BrowserRouter>
</div>
);
}
}
Then in my NewTable and CardColumns components I call the loadCardData function in componentDidMount() passing in components' props.
componentDidMount() {
this.props.loadCardData(this.props);
}
When I first go to the path="/:username/own" route the API request is made:
and app.js state is updated:
and the NewTable component props are set:
However when I then go to the path="/:username/own/team/:id" route the api is called again
and console.log reports OWN TRUE which tells me that
if (path[2] == "own" && props.ownedCards.length < 1) {
console.log("OWN TRUE");
....
.... including axios call
}
is being met (i.e. props.ownedCards is empty). However if I look at the component I can see props are set:
However if I then navigate back to the path="/:username/own" route no API call is made and the app makes use of the data in props.
Can anyone advise what the correct way is to makes sure that the components use the data in props rather than keep going back to the API?
The fundamental problem here was that in my table component I was using an to link to other pages and this forced components to remount.
Changing this to <Link to= meant that state was not lost (which was passed down to child components via context api) and I could stop making unneccesary calls!

Categories

Resources