Make Route for multiple variables using React Router - reactjs

I'm trying to make multiple pages about some game character using React Router
For multiple pages, I want to load data from firebase and insert the data into pages.
the all character page formats are same and just need to change detailed data
For example,
we have
character 'a' : Age : 12, Gender : Male
character 'b' : Age: 13, Gender : Female
and each character page would need to show the character's data with loaded data
I need to show the data of only one character at one page.
Here is my code about routing
function App() {
useEffect(() => {
async function getFromDocs() {
const data = await db
.collection('Character')
.doc(curChar)
.get()
.then((snap) => {
return snap.data() as CharProps;
});
setData(data);
}
getFromDocs();
}, [curChar]);
const onCharChange = (text: string) => {
setCurChar(text);
};
return (
<>
<title>Tekken_info 0.1.0</title>
<GlobalStyle />
<Wrapper>
<PageContent>
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/Data" exact={true}>
<Page data={data} />
</Route>
</Switch>
</PageContent>
</Wrapper>
</>
);
}
export default App;
But how I did temporarily was not quite satisfying...
I load character data with useState and load it in to '/Data' page
I don't think this is good way and want to make one route for characters.
For example
when we access to '/a' load data about a only 'a'....
If anything you don't understand about my description and question
let me know

Instead of passing the data as props to Page component, You could move the useEffect into the page itself. so that only curChar is a route path variable and can be accessed inside the page something like:
/Data/curChar
Change the path to
path="/Data/:curChar"
Inside the page
const Page = () => {
let {
curChar
} = useParams();
//move the useEffect here.
useEffect(() => {
async function getFromDocs() {
const data = await db
.collection('Character')
.doc(curChar)
.get()
.then((snap) => {
return snap.data() as CharProps;
});
setData(data);
}
getFromDocs();
}, [curChar]);
return <div > Now showing details of {
curChar
} < /div>;
}

Related

react-router : how can I change the URL to a "pretty" one once I get my post data loaded?

I have a route like this one
<Route
path="/post/:post_id"
exact
component={PostPage}
/>
Which loads my <PostPage/> component.
In that component, I load my post data (including its name) using the post_id parameter.
Once the data is successfully loaded based on that post_id; I would like to use the name of that post to make the browser URL prettier.
Example:
/post/123* : goes to my <PostPage/> component
which loads the post datas for post ID 123; including its name: post-foo-bar
I would like the browser URL to change to /post/123/post-foo-bar
I guess that using a wrong name should redirect to the good one :
/post/123/post-not-so-foo-bar would redirect to /post/123/post-foo-bar
How could I achieve this ?
Thanks !
Assuming you are using react-router v6 (it'd be easier/different in v5)
I went through the base tutorial for react-router, so it's a pretty basic app example:
https://stackblitz.com/edit/github-ylwf81?file=src%2Froutes%2Finvoice.jsx
Resulting webpage: https://github-ylwf81--3000.local.webcontainer.io/invoices/1995/Santa%20Monica
The main things to note in this, are the route definitions
For the invoices which would be analogous to your posts:
<Route path="invoices" element={<Invoices />}>
<Route
index
element={
<main style={{ padding: '1rem' }}>
<p>Select an invoice</p>
</main>
}
/>
<Route path=":invoiceId">
<Route index element={<Invoice />} />
<Route path=":invoiceName" index element={<Invoice />} />
</Route>
</Route>
Note the use of an index route for just the invoiceId, and a separate route for invoiceId + invoiceName.
Because they are both output to the same element, they aren't treated as different by React, so it retains state properly. You can see that in the example by the fact that it doesn't go back to the loading state.
As for the navigation to add in the invoiceName to the URL, it's just a basic navigation in a useEffect when the name of your object is defined (after an async call). And because this always triggers when the invoice.name changes, and how we aren't actually using the :invoiceName variable, it doesn't matter at all what the user inputs for that as long as the :invoiceId is correct
useEffect(() => {
if (!invoice?.name) {
return;
}
navigate(`/invoices/${invoice.number}/${invoice.name}`);
}, [invoice?.name, invoice?.number]);
In react-router-dom v5 you can declare an array of matching paths to render a routed component on. I suggest adding a matching path that includes a route param for the pretty name.
Example:
<Route
path={["/post/:post_id/:postName", "/post/:post_id"]}
component={PostPage}
/>
Then use an useEffect hook on the `PostPage component to check the following cases:
If post_id param and post id mismatch, then fetch post by id, then redirect to path with post name.
If postName param, current fetched post, and pretty name
mismatch, then redirect to current path with correct post name.
If no postName param, current fetched post, then redirect to path with post name.
Code
const { params, path } = props.match;
const { post_id, postName } = params;
const [post, setPost] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchPost = async () => {
setLoading(true);
try {
const post = await fetchPostById(post_id);
setPost(post);
history.replace(
generatePath(`${path}/:postName`, {
post_id: post.id,
postName: post.name
})
);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
if (post_id && post_id !== post?.id) {
fetchPost();
} else if (postName && postName !== post?.name) {
history.replace(
generatePath(path, {
post_id: post.id,
postName: post.name
})
);
} else if (!postName && post?.name) {
history.replace(
generatePath(`${path}/:postName`, {
post_id: post.id,
postName: post.name
})
);
}
}, [history, post, post_id, postName, path]);

push a route to change url without reload page

I have a react app.
my routing looks like this =>
<AppRoute path="/profile" exact={true} menuTitle="Profile" component={pages.Profile} />
<AppRoute path="/profile/post_feed" exact={true} menuTitle="Post Feed" component={pages.PostFeed} />
<AppRoute path="/profile/story_feed" exact={true} menuTitle="Story Feed" component={pages.StoryFeed} />
now in profile, there is some filter, so I would like to save them in the URL, to do so, I am using the following.
const setQueryParams = (filterId: number | null) => {
history.push(`${urlParams.pathname}?filterId=${filterId}`);
};
const handleFilterSelectionChange = (id: number) => {
setQueryParams(id);
//....
};
but this trigger a COMPLETE refresh of the page... and all the query that are associated. I put the following effect in my Profile component ( the one called from the route)
useEffect(() => {
console.log('FY');
}, []);
And this trigger at load, then for each filter, the page is fully refreshed. But I would like those thing to trigger once, and when the route don't change (only the query parameters) those effect don7t get retriggered. Is it possible ?
Try to use replace instead push
const setQueryParams = (filterId: number | null) => {
history.replace({ search: `?filterId=${filterId}` })// CHANGE HERE
};
const handleFilterSelectionChange = (id: number) => {
setQueryParams(id);
//....
};

Variable is undefined when component is re-rendered in React

In my app, I have a list of university departments. When you click a specific department, you are taken to the department landing page (/department/:deptId). I am using React Router's useParams hook to get the department id from the URL and then find that specific department object from my array of departments passed down as props.
This works fine when navigating from the list of departments to the individual department page, but if I refresh the page, I get the following error: Uncaught TypeError: can't access property "Name", dept is undefined
My code is below:
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
const Department = props => {
const { id } = useParams();
const [dept, setDept] = useState({});
useEffect(() => {
const unit = props.depts.find(item => item.Id === Number(id));
setDept(unit);
}, [id]);
return (
<div>
<h1>{dept.Name}</h1>
</div>
);
};
export default Department;
I'm not sure why this happens. My understanding is that the props should remain the same, and the useEffect should run when the page is refreshed. Any idea what I'm missing?
More code below:
The depts array is passed as props from the App component, which is getting it from an axios call in a Context API component.
import { UnitsContext } from './contexts/UnitsContext';
function App() {
const { units } = useContext(UnitsContext);
return (
<>
<Navigation />
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/people" component={Persons} />
<Route exact path="/department/:id">
<Department depts={units} />
</Route>
</Switch>
</>
);
}
// Context Component. Provider wraps `index.js`
export const UnitsContext = createContext();
export const UnitsContextProvider = props => {
const url = 'http://localhost:5000';
const [units, setUnits] = useState([]);
useEffect(() => {
axios
.get(`${url}/api/units`)
.then(res => {
setUnits(res.data);
})
.catch(err => {
console.log(err);
});
}, []);
return (
<UnitsContext.Provider value={{ units }}>
{props.children}
</UnitsContext.Provider>
);
};
the problem is most probably with this,
useEffect(() => {
const unit = props.depts.find(item => item.Id === Number(id));
setDept(unit); // <<
}, [id]);
Nothing else in ur code sets State except setDept(unit);
So, My best guess is props.depth find matches nothing and returns null. Thats why dept.Name results with the error
From MDN,
The value of the first element in the array that satisfies the provided testing function. Otherwise, undefined is returned

React setting Stateful variables in map function

I am trying to set up a react app where a list of buttons are displayed, the user can press a button and be taken to a page with information about a country. I am creating the buttons programmatically using a .map function. I am using a SQL database to store country names, and information about the countries, and then calling a flask route to pull the data into my react app. For that, I am using an async function.
This is the process that I would like to have happen:
I set up some stateful variables in my App.js main router component. I then pass as props my setState functions to my component with the buttons and the .map function. For each button, there is the option to set the state of the variables in the App.js component. I would then set the variables in App.js to the values associated with the button clicked. From there, I could pass those stateful variables to my country page component for display.
What actually happens:
I pass the props to my country component, expecting a country and country details to pass along with it, but I end up getting undefined. It looks like undefined might be the last element of the dataset, as I have gotten Zimbabwe as the result before. Here is my code for the App.js router:
export default function App() {
const [cname, setCName] = useState('')
const [pdf, setPdf] = useState('')
const [details, setDetails] = useState('')
return (
<div className="App">
<BrowserRouter>
{/* <Route exact path="/" component = { Home }/> */}
<Route path="/cia" component = {(props) => <CIALanding {...props} setCName={setCName} setPdf={setPdf} setDetails={setDetails}/>}/>
<Route path="/country" component={(props) => <Country {...props} setCName={setCName} details={details} cname={cname}/>}/>
<Route path="/countrypage" component={CountryPage}/>
</BrowserRouter>
</div>
);
}
Here is the code for my landing page (with the .map function)
export default function CIALanding(props) {
const [countriesList, setCountriesList] = useState([])
const getCountries = async () => {
const response = await fetch('http://127.0.0.1:5000/countries');
const data = await response.json();
setCountriesList(data['country_list'].map((country) => {return (
<Link to={{pathname:'/country',
}}>
<Country cname1={country[0]} details={country[2]} setCName={props.setCName}>{country[0]}</Country>
</Link>
)}))
}
useEffect(() => {
getCountries()
},[])
return (
<div>
{countriesList}
</div>
)
}
Here is my code for the Country Component
export default function Country(props) {
return (
<div>
{console.log(props.cname)}
<Button onClick={props.setCName(props.cname1)}>{props.cname1}</Button>
</div>
)
}
Thanks a lot for the help!
I will not exactly anwser to your question but I propose some refactoring and maybe that will solve your problem.
Firstly I will move fetching code to the App component, it will allow easier access to this data by components (I added some nice handling of fetching status change). Here you will render proper Routes only if data is fetched successfully.
const App = () => {
const [status, setStatus] = useState(null);
const [countries, setCountries] = useState([]);
const getCountries = async () => {
setStatus('loading');
try {
const response = await fetch('http://127.0.0.1:5000/countries');
const data = await response.json();
setCountriesList([...data['country_list']]);
setStatus('success')
} catch (error) {
setSatus('error');
}
}
useEffect(() => {
getCountries();
}, [])
if (!status || status === 'error') {
return <span>Loading data error</span>
}
if (status === 'loading') {
return <span>Loading...</span>
}
return (
<div className="App">
<BrowserRouter>
<Route path="/cia" component={(props) => <CIALanding {...props} countries={countries} />
<Route path="/country/:countryId" component={(props) => <Country {...props} countries={countries} />
</BrowserRouter>
</div>
);
}
Second thing - to display proper country page you don't need to set any data into state, only thing you need is to set route /country/:countryId and Links with proper paths where countryId can be unique country identyficator as number or code. With setup like this only data needed in component is array of countries and which country is loaded is decided by routing
Landing component will be nice and simple (you definitely shouldn't keep React components in state, only data)
const CIALanding = ({countries}) => (
<div>
{
countries.map(({countryName, countryId}) => (
<Link to={`/country/${countryId}`}>{countryName}</Link>
))
}
</div>
)
So now we have nice list of countries with proper links. And then country page will know which data to display by param countryId
const Country = ({match, countries}) => {
//match object is passed by Route to this component and inside we have params object with countryId
const {countryId} = match.params;
const country = countries.find(country => country.countryId === countryId);
if (country) {
return (
<div>
Show info about selected country
</div>
);
}
return (
<div>
Sorry, cannot find country with id {countryId}
</div>
)
}
And you can access proper country page by clicking on Link and additionally by entering path for example .../country/ENG in browser (I don't know your data structure so remeber to use correct data for countryId) ;)
Sorry if this don't resolve your problems but I hope it contains at least some nice ideas for refactoring ;)

How connect Context with Redirect

I want to send information to second page if the user is logged in . I would like use Context to that.
Something about my code :
const Login = () => {
...
const [logged, setLogged] = React.useState(0);
...
const log = () => {
if (tempLogin.login === "Login" && tempLogin.password == "Haslo") {
setLogged(1);
}
...
return (
{logged == 1 && (
<Redirect to="/page" />
)}
I want to send logged to /page but i don't know how . None guide help me .Page is actually empty React file.
There are 2 ways handle that:
Passing state to route(as described in docs):
{logged == 1 && (
<Redirect to={{ path: "/page", state: { isLoggedIn: true } }} />
)}
And your component under /page route will access that flag as this.props.location.state.isLoggedIn
Utilize some global app state(Redux, Context API with <Provider> at root level or anything alike).
To me second option is better for keeping auth information:
Probably not only one target component will want to check if user is authorized
I'd expect you will need to store some authorization data to send with new requests(like JWT token) so just boolean flah accessible in single component would not be enough.
some operation on auth information like say logout() or refreshToken() will be probably needed in different components not in single one.
But finally it's up to you.
Thanks skyboyer
I solved this problem with Context method .I will try tell you how i do this becouse maybe someone will have the same problem
I created new file
import React from "react";
import { Redirect } from "react-router";
const LoginInfo = React.createContext();
export const LoginInfoProvider = props => {
const [infoLog, setInfoLog] = React.useState("");
const login = name => {
setInfoLog(name);
};
const logout = () => {
setInfoLog("old");
};
const { children } = props;
return (
<LoginInfo.Provider
value={{
login: login,
logout: logout,
infolog: infoLog
}}
>
{children}
</LoginInfo.Provider>
);
};
export const LoginInfoConsumer = LoginInfo.Consumer;
In App.js add LoginInfoProvider
<Router>
<LoginInfoProvider>
<Route exact path="/" component={Login} />
<Route path="/register" component={Register} />
<Route path="/page" component={Page} />
</LoginInfoProvider>
</Router>
In page with login (parts of code in my question) i added LoginInfoConsumer
<LoginInfoConsumer>

Resources