Reac router component in a list as a state - javascript

does anyone have experience with React routers?Is there a way to create a list of routes and hold it in a usestate? When i try to do the [... prevCountryRoutes] i get the error that prevCountryRoutes is not iterable
const [countriesWithRouteList,setCountriesWithRouteList]=React.useState(JSON.parse(localStorage.getItem("countriesWithRouteList")) || [])
const [countryRoutes,setCountryRoutes]=React.useState()
function addCountryRoute(co){
if(countriesWithRouteList.filter(el => el == co) == null){
console.log('already route exists')
}else{
console.log('country page being added')
setCountryRoutes((prevCountryRoutes)=>{
const newRoute = <Route
key={nanoid()}
path={`/countrypage/${co.replace(/ /g, '%20')}`}
element={
<CountryPage
country={co}
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
return(
[...prevCountryRoutes, newRoute]
)
})
}
setCountriesWithRouteList(prevList => [...prevList, co])
}

The error you are asking about is cause by not declaring an initial countryRoutes state that is iterable. It's undefined.
Anyway, it's a React anti-pattern to store React components and JSX in state. Just store the data and render the derived JSX from state.
I suggest the following refactor:
const [countries, setCountries] = React.useState(JSON.parse(localStorage.getItem("countries")) || []);
function addCountryRoute(co){
if (countries.some(el => el.co === co)){
console.log('already route exists')
} else {
console.log('country page being added')
setCountries((countries) => countries.concat({
id: nanoid(),
co,
}));
}
}
...
{countries.map(country => (
<Route
key={country.id}
path={`/countrypage/${co.replace(/ /g, '%20')}`}
element={
<CountryPage
country={co}
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
))}
And instead of mapping a bunch of routes that differ only in the country path segment, render just a single route where the country code is a route path parameter and the CountryPage component uses the useParams hook to get the code.
Example:
<Route
path="/countrypage/:country"
element={
<CountryPage
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
CountryPage
const { country } = useParams();

Initialize countryRoutes with an array, so the first time can be iterable.
const [countryRoutes,setCountryRoutes] = React.useState([])

Related

Can you pass in a useState function into a component on initialization?

I'm using the useState hook to manage rendering components on screen. I want to initialize it with a component while passing in the useState function to set the screen into the component.
Here is my App.js. The error I get is in regards to passing a function into itself on initialization.
function App() {
//useState hooks to determine which component should render
const [screenLoaded, loadScreen] = useState(() => {
<Home setLoadedScreen = {loadScreen}/>
})
return (
<div className="App">
{screenLoaded}
</div>
);
}
The default value for useState is always in the parentheses, no curly braces are needed in this case. const [state, setState] = useState(default). This state could be change in the future with setState(new value).
one simple method is to give your screen a name for example
<Home /> === "home-screen"
<About /> === "about-screen"
so when you pass the setState method of loadScreen into the component, you can switch them by setting the string, for example if you're in home screen and you want to switch to about screen, you'd write
setLoadedScreen("about-screen")
function App(){
const [screenLoaded, loadScreen] = useState("home-screen")
return (
<div className="App">
{screenLoaded === "home-screen" && <Home setLoadedScreen = "loadedScreen" />}
{screenLoaded === "about-screen" && <About setLoadedScreen = "loadedScreen" />}
</div>
);
}

React Router: Having one Route conditionally render component based on url search param

Here's a scenario example:
I have two components and
The search params look like this: ?A=123&B=456
The Route is like this path="/home"
I want A to load on the route if only A is present in the search params (e.g. ?A=123), I want B to load if there is B in the search params (B also loads if both A and B are in the search params).
Can I do something inside the Route component where I can have a conditional render based on that and how would I go about doing that?
AKA:
<Route
path="/home"
if(urlparam.B){
render={<B/>}
} else {
render={<A/>}
}
</Route>
how can I pass the url to the route so it can check?
You can't do this in the Route component, but you can create a wrapper component to handle this.
You can access the current location object's search string. From here you can create a URLSearchParams object and access the queryString parameters and apply the conditional rendering logic.
location
Locations represent where the app is now, where you want it to go, or even where it was. It looks like this:
{
key: 'ac3df4', // not with HashHistory!
pathname: '/somewhere',
search: '?some=search-string',
hash: '#howdy',
state: {
[userDefined]: true
}
}
URLSearchParams
Code
const Wrapper = ({ location }) => {
const { search } = location;
const searchParams = new URLSearchParams(search);
const hasAParam = searchParams.get("A");
const hasBParam = searchParams.get("B");
if (hasAParam && !hasBParam) { // only A and not B
return <A />;
} else if (hasBParam) { // only B
return <B />;
}
return null; // render nothing
};
...
<Route path="/home" component={Wrapper} />
This assumes you are using react-router-dom#5.
If using v6, then use the useSearchParams hook directly.
useSearchParams
Example:
const Wrapper = () => {
const [searchParams] = useSearchParams();
const hasAParam = searchParams.get("A");
const hasBParam = searchParams.get("B");
if (hasAParam && !hasBParam) { // only A and not B
return <A />;
} else if (hasBParam) { // only B
return <B />;
}
return null; // render nothing
};
...
<Route path="/home" element={<Wrapper />} />
try this:
<Route
path="/home"
render={urlparam?.B ? <B/> : <A/>}
></Route>
In this case you can use the ternary operator to solve your problem

How to share props to a different route using react-router-dom?

I am trying to share my props (data, saveWorkButtonClicked, updateFBRDB) from <ProjectPage /> component route to <Indent /> component route.
But getting the following error:
Uncaught DOMException: Failed to execute 'pushState' on 'History': async (data, setSpinner, updateFBRDB) => {
setSpinner && setSpinner(true);
let rawRoomData = String.raw`${J...<omitted>...
} could not be cloned.
App.js
<Router>
<Switch>
<Route path="/ProjectPage/:projectId" exact component={ProjectPage} />
<Route path="/Indent/" render={(props) => <Indent {...props} />} />
</Switch>
</Router>
ProjectPage.js
history.push("/Indent/",
{
data: { ...project, rooms: project.rooms, ProjectId: project.ProjectId, ClientName: project.ClientName, Address: project.Address, AmountRecieved: project.AmountReceived, SiteEngineerId: project.SiteEngineersId },
saveWorkButtonClicked,
updateFBRDB,
}
)
// saveWorkButtonClicked & updateFBRDB are API calls which will be called in <Indent />
Indent.js
export default function Indent({ data, saveWorkButtonClicked, updateFBRDB }) {
console.log('data in indent', data)
}
NOTE: Please give solutions where this can be implemented without Context/ Redux/ Mobx. Also, I am using react-router-dom v5.2.0
I would suggest an workaround. Have a state which keeps track of when you want to move to next page, so that we can use Redirect component conditionally with your desired data as props.
App.js
<Router>
<Switch>
<Route path="/ProjectPage/:projectId" exact component={ProjectPage} />
</Switch>
</Router>
ProjectPage.js
const [isDone, setIsDone] = useState(false);
const handleClick = () => {
// Do all your works, when you want to `push` to next page, set the state.
setIsDone(true);
}
if(isDone) {
return (
<>
<Route path="/Indent"
render={ props =>
<Indent
{...props}
data={...}
saveWorkButtonClicked={saveWorkButtonClicked}
updateFBRDB={updateFBRDB}
/>
}
/>
<Redirect to="/Indent" />
</>
);
}
return (
<div>Your Normal Profile Page goes here</div>
)
If you want to "share" props, you need to do one of two things. Either have the receiving component be a child of the propsharing component - in which case you can pass them as props directly. Else, you would need to pass them as state via a common ancestor component, which you would need to update by sending a callback down to the component that will update the state.
You can pass state to location with this format
const location = {
pathname: '/Indent/',
state: {
data: { ...project, rooms: project.rooms, ProjectId: project.ProjectId, ClientName: project.ClientName, Address: project.Address, AmountRecieved: project.AmountReceived, SiteEngineerId: project.SiteEngineersId },
saveWorkButtonClicked,
updateFBRDB,
}
}
history.push(location)
And then using withRouter to receive location values
import { withRouter } from 'react-router'
function Indent({ location }) {
const { state } = location
const { data, saveWorkButtonClicked, updateFBRDB } = state || {}
return <></>
}
export default withRouter(Indent)

DOM render not triggering after updating hooks

I am trying to delete an item (const removeItem) from a list using an onClick event in React.
The state is managed with hooks. I know my way of deleting the item is not the right way yet (i'm putting the name to null), but this is not the issue.
After i set a user to null (i update the users object), i expect a render to happen (useEffect) but it does not. If i switch components and go back to this one, it works, but when clicking the X button, nothing happens in the view.
component Home:
import React, { Suspense, useState, useEffect } from "react";
const Home = props => {
const [users, setUsers] = useState(props.users);
console.log(users);
useEffect(() => {}, [users]);
const addItem = e => {
users.push(e);
console.log(e);
e.preventDefault();
};
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null;
}
});
console.log(users);
setUsers(users);
};
return (
<div className="Home">
<form className="form" id="addUserForm">
<input
type="text"
className="input"
id="addUser"
placeholder="Add user"
/>
<button className="button" onClick={addItem}>
Add Item
</button>
</form>
<ul>
{users.map(item => {
return (
<>
<li key={item.id}>{item.name + " " + item.count}</li>
<button className="button" onClick={() => removeItem(item.id)}>
X
</button>
</>
);
})}
</ul>
</div>
);
};
export default Home;
How i get my users object:
import React from "react";
const LotsOfUsers =[...Array(100).keys()].map((item, key) => item = {
name : `User ${key}`,
id: key,
count: 0
})
export default LotsOfUsers;
main App:
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useRouteMatch,
useParams
} from "react-router-dom";
import Home from "./Home"
import CTX from './store'
import LotsOfUsers from "./LotsOfUsers";
export default function App() {
return (
<CTX.Provider value={{}}>
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/topics">
<Topics />
</Route>
<Route path="/">
<Home users={LotsOfUsers} text="hello world"/>
</Route>
</Switch>
</div>
</Router>
</CTX.Provider>
);
}
function About() {
return <h2>About</h2>;
}
function Topics() {
let match = useRouteMatch();
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>
Props v. State
</Link>
</li>
</ul>
{/* The Topics page has its own <Switch> with more routes
that build on the /topics URL path. You can think of the
2nd <Route> here as an "index" page for all topics, or
the page that is shown when no topic is selected */}
<Switch>
<Route path={`${match.path}/:topicId`}>
<Topic />
</Route>
<Route path={match.path}>
<h3>Please select a topic.</h3>
</Route>
</Switch>
</div>
);
}
function Topic() {
let { topicId } = useParams();
return <h3>Requested topic ID: {topicId}</h3>;
}
Looking into your code, I've noticed 2 times that you change users without the use of setUsers.
const addItem = e => {
users.push(e); // <--- here
console.log(e);
e.preventDefault();
};
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null; // <--- here
}
});
console.log(users);
setUsers(users);
};
In that way, you are updating your users, without letting react know about it. On both cases you have to update users with setUsers and not mutating the array directly.
const addItem = e => {
setUsers(users.concat(e)); // sidenote: if e is your event, then you might be looking for e.target.value here
console.log(e);
e.preventDefault();
};
const removeItem = item => {
setUsers(users.filter(user => user.id !== item));
};
Both .concat() and .filter() use your users array, and return a new array based on the changes you want to apply, and then is used by setUsers to update your users.
So, in extend what I'm actualy doing on removeItem is:
const removeItem = item => {
const newUsers = users.filter(user => user.id !== item); // users remains untouched
setUsers(newUsers);
};
Also, you don't need useEffect hook for this scenario to work properly.
I hope this solves your problem.
You are committing THE Fundamental sin of the react universe. "Mutation"
of state.
const removeItem = item => {
users.forEach(user => {
if (user.id === item) {
user.name = null; // <--- HERE
}
});
console.log(users);
setUsers(users);
};
Check this codesandbox for a working demo of this issue.
https://codesandbox.io/s/jovial-panini-unql4
The react reconciler checks to see whether the 2 objects are equal and since you have just mutated and set the value it registers as the same object and there will be no state change triggered. Hence the view will not be re-rendered and useEffect will not be triggered.

Component only rending if I start the flow from the homepage

I am having an issue with my application. My user component only loads UserCard when I start the application from the homepage then click users link there... if I just refresh the users URL... UserCard doesn't get loaded which means something is wrong with my this.props.users. I do see that in chrome it says: Value below was evaluated just now when I refresh but when I go through the flow it doesn't say that. Any help will be appreciated.
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: []
};
}
componentDidMount() {
users = []
axios.get('/getall').then((res) => {
for(var d in res.data) {
users.push(new User(res.data[d]));
}
});
this.setState({ users });
}
render() {
const { users } = this.state;
return (
<Router history={history}>
<Switch>
<PrivateRoute exact path="/" component={Home} />
<Route exact path='/users' render={(props) => <Users {...props} users={users} />}/>
</Switch>
</Router>
)
}
}
PrivateRoute:
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
<Component {...props} /> )} />
)
User.js
export default class Users extends Component {
render() {
console.log(this.props.users);
return (
<Row>
{this.props.users.map(u =>
<UserCard key={u.name} user={u}/>
)}
</Row>
);
}
}
export class User {
constructor(obj) {
for (var prop in obj){
this[prop] = obj[prop];
}
}
getURLName() {
return this.name.replace(/\s+/g, '-').toLowerCase();
}
}
class UserCard extends Component {
render() {
return (
<Link to={'/users/' + this.props.user.getURLName()} >
<div>
// Stuff Here
</div>
</Link>
);
}
}
As per the comments:
The issue here is how you're setting state. You should never modify state directly since this will not cause the component to rerender See the react docs
Some additional thoughts unrelated to the question:
As per the comments - use function components whenever possible, especially with hooks on the way
There is probably no need to create a User class, only to new up little user objects. Simply use plain old JS objects and calculate the link url right in the place its used:
render() {
const { user } = this.props
return <Link to={`/users/${user.name.replace(/\s+/g, '-').toLowerCase()}`} />
}
It might be a good idea to start using a linter such as eslint. I see that you're declaring users = [] without using let or const (don't use var). This is bad practice since creating variables in this way pollutes the global name space. Linters like eslint will help you catch issues like this while you're coding.

Categories