I have a React app which I want to have this query string /contacts?page=${pageNumber}
where page number have to change when I go to the next page, I use newer version of react-router-dom which doesn't have useHistory hook. Now my URL is only http://localhost:3000/.
I have this code:
const Contact = (props) => {
let array = Object.values(props);
const navigate = useNavigate();
const [pageNumber, setPageNumber] = useState(1);
const [contacts, setContacts] = useState([]);
const [entry, setEntry] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [isSynchronizedClicked, setIsSynchronizedClicked] = useState(false);
const handlePaging = (params) => {
setPageNumber(params + 1);
//navigate(`/contacts?page=${pageNumber}`);
};
useEffect(() => {
setTimeout(() => {
if (searchTerm !== '') {
search();
}
}, 500);
}, [searchTerm, search]);
return (
<>
<div style={{ height: 630, width: '100%' }}>
<DataGrid
id={() => searchTerm === '' ? array.map((contact) => contact.id) : contacts.map((contact) => contact.id)}
rows={searchTerm === '' ? array : contacts}
sx={{
'.MuiDataGrid-columnHeaderTitle': {
fontWeight: 'bold',
},
'.MuiDataGrid-columnHeaders': {
backgroundColor: '#d3d3d3'
},
'.MuiDataGrid-row': {
maxHeight: '51px !important',
minHeight: '51px !important'
},
'.MuiDataGrid-virtualScroller': {
overflow: 'hidden'
}
}}
onPageChange={handlePaging}
columns={columns}
pageSize={10}
rowsPerPageOptions={[10]}
checkboxSelection
onRowDoubleClick={e => navigateToDetails(e)}
onSelectionModelChange={e => setEntry(e)}
/>
</div>
{isSynchronizedClicked && <ClickAwayListener onClickAway={() => setIsSynchronizedClicked(false)}>
<div className='synchronize-popup'>
Do you want to synchronize all contacts ?
<div className='second-message'>This will take a while.</div>
<div className='synchronize-popup-buttons-container'>
<button className='buttons reject' onClick={() => setIsSynchronizedClicked(false)}>No</button>
<button className='buttons approve' onClick={synchronize}>Yes</button>
</div>
</div></ClickAwayListener>}
</>
);
};
And this code in App.js:
function App() {
return (
<BrowserRouter>
<div className="app">
<Routes>
<Route path='/' element={<RenderContacts />} />
<Route path='/details/:id' element={<Details />} />
</Routes>
</div>
</BrowserRouter>
);
}
If someone knows how to do that, I'll be grateful.
This is a common gotcha with React setState.
https://reactjs.org/docs/state-and-lifecycle.html
Read the section:
State Updates May Be Asynchronous
I assume this is the failure: pageNumber likely will not be ready when used.
setPageNumber(params + 1);
navigate(`/contacts?page=${pageNumber}`);
Instead, just use params + 1 inside the navigate function.
After navigation, you will want to use useSearchParams() (assuming router v6?) to get the updated value.
Related
I have such a project. Here I want the button border save in the local storage.The buttons are divided into categories. For example when you refresh the page after selecting a sports button, the border of the button disappears. I want save btn border in the localstorage. I saved the categories in memory, but I can't make the border of the selected button.How can I fix it?
import React, { useEffect, useState } from "react";
import SpinnerLoad from './components/SpinnerLoad'
import NewsItem from "./components/NewsItem";
import Category from "./components/data/Category"
const App = () => {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false)
const [selected, setSelected] = useState('');
const fetchValue = (category, index) => {
localStorage.setItem("category", category);
localStorage.setItem("selected", index);
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
setLoading(false);
};
const CategoryButton = ({ category, i }) => (
// passing index --> i to the fetch Value
<button onClick={() =>{ fetchValue(category,i) ; setSelected(i)} }
style={{border : selected === i ? '1px solid red' : null}} >{category}</button>
);
useEffect(() => {
let categoryValue = localStorage.getItem("category") || "all";
fetchValue(categoryValue)
const select = localStorage.getItem("selected") || "";
setSelected(select);
}, []);
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value,i) => {
return <CategoryButton category={value} i={i}/>;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad />
:
state.map((data, index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
key={data.id}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default App;
According to the code looks like you want to display data specific to a category set when the user clicks on the category buttons. and after the click, the correct data is rendered and the current category button receives a change in its style highlighting it is the current state.
I don't understand why you need to store anything in a client's localstorage,
I would not recommend storing too much in localStorage as it is limited and is used by different sites a user visits, I only store authentication tokens in localstorage and I believe that is the norm.
I've tried to create the effect you want without the need to store in local storage
import React, { useState, useCallback, useEffect } from "react";
import ReactDOM from "react-dom";
import { cat } from "../categories.js";
import { news } from "../news.js";
function Example() {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState(null);
useEffect(() => {
function fetchFunction() {
setLoading(true);
for (let i = 0; i < news.length; i++) {
if (news[i].id === selected) {
const current = news[i].c;
setState(current);
}
}
setLoading(false);
}
fetchFunction();
}, [selected]);
return (
<>
<ol
style={{
width: "50%",
listStyle: "none",
display: "flex",
justifyContent: "space-between"
}}
>
{cat.map((item, index) => {
return (
<li key={index}>
<button
style={{ border: selected === item.id && "none" }}
onClick={() => {
setSelected(item.id);
}}
>
{item.name}
</button>
</li>
);
})}
</ol>
<section style={{ width: "100%", height: "70%" }}>
{state.map((item, index) => {
return (
<div
key={index}
style={{
width: "30%",
height: "30%",
background: "red",
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: "1% 0 2% 0"
}}
>
{item.name}
</div>
);
})}
</section>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
You can save the selectedIndex in localStorage and retrieve it in the useEffect..
const CategoryButton = ({ category, i }) => (
// passing index --> i to the fetch Value
// setting selected as string instead of index for type checking
<button onClick={() =>{ fetchValue(category,i) ; setSelected(`${i}`)} }
style={{border : selected === `${i}` ? '1px solid red' : null}} >{category}</button>
);
const fetchValue = (category, index) => {
localStorage.setItem("category", category);
localStorage.setItem("selected", index);
// ...
}
useEffect(() => {
const select = localStorage.getItem("selected") || "";
// passing selectedIndex to the fetchValue, otherwise it becomes
//undefined..
fetchValue(categoryValue,select)
setSelected(select);
},[])
I have a few buttons and "view all" button. The individual buttons load the coresponding data of that index or will show all the data by clicking the "view all" button. Problem I am running into is when I click my "view all" button in the parent it's not updating the state in the child component. On mounting it works as normal but on event handler in the "view all" it doesn't update. Any thoughts on where I am going wrong here?
JS:
...
const Context = createContext(false);
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
"& > *": {
margin: theme.spacing(1)
}
},
orange: {
color: theme.palette.getContrastText(deepOrange[500]),
backgroundColor: deepOrange[500],
border: "4px solid black"
},
info: {
margin: "10px"
},
wrapper: {
display: "flex"
},
contentWrapper: {
display: "flex",
flexDirection: "column"
},
elWrapper: {
opacity: 0,
"&.active": {
opacity: 1
}
}
}));
const ToggleItem = ({ id, styles, discription }) => {
const { activeViewAll, handleChange } = useContext(Context);
const [toggleThisButton, setToggleThisButton] = useState();
const handleClick = () => {
setToggleThisButton((prev) => !prev);
handleChange(discription, !toggleThisButton);
};
return (
<>
<Avatar
className={toggleThisButton && !activeViewAll ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(toggleThisButton)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item, idx) => (
<div key={idx}>Content {item}</div>
))}
</div>
);
};
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(false);
useEffect(() => {
setActiveViewAll(true);
setSelected([...data]);
}, []);
const handleChange = (val, action) => {
let newVal = [];
if (activeViewAll) {
selected.splice(0, 3);
setActiveViewAll(false);
}
if (action) {
newVal = [...selected, val];
} else {
// If toggle off, then remove content from selected state
newVal = selected.filter((v) => v !== val);
}
console.log("action", action);
setSelected(newVal);
};
const handleViewAll = () => {
console.log("all clicked");
setActiveViewAll(true);
setSelected([...data]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer styles={classes} selected={selected} />
</div>
</Context.Provider>
);
}
Codesanbox:
https://codesandbox.io/s/72166087-forked-jvn59i?file=/src/App.js:260-3117
Issue
The issue seems to be that you are mixing up the management of the boolean activeViewAll state with the selected state.
Solution
When activeViewAll is true, pass the data array as the selected prop value to the ToggleContainer component, otherwise pass what is actually selected, the selected state.
Simplify the handlers. The handleViewAll callback only toggles the view all state to true, and the handleChange callback toggles the view all state back to false and selects/deselects the data item.
function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]); // none selected b/c view all true
const [activeViewAll, setActiveViewAll] = useState(true); // initially view all
const handleChange = (val, action) => {
setActiveViewAll(false); // deselect view all
setSelected(selected => {
if (action) {
return [...selected, val];
} else {
return selected.filter(v => v !== val)
}
});
};
const handleViewAll = () => {
setActiveViewAll(true); // select view all
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected} // pass all data, or selected only
/>
</div>
</Context.Provider>
);
}
In the ToggleContainer don't use the array index as the React key since you are mutating the array. Use the element value since they are unique and changing the order/index doesn't affect the value.
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update
Since it is now understood that you want to not remember what was previously selected before toggling activeViewAll then when toggling true clear the selected state array. Instead of duplicating the selected state in the children components, pass the selected array in the context and computed a derived isSelected state. This maintains a single source of truth for what is selected and removes the need to "synchronize" state between components.
const ToggleItem = ({ id, styles, description }) => {
const { handleChange, selected } = useContext(Context);
const isSelected = selected.includes(description);
const handleClick = () => {
handleChange(description);
};
return (
<>
<Avatar
className={isSelected ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(isSelected)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update the handleChange component to take only the selected value and determine if it needs to add/remove the value.
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(true);
const handleChange = (val) => {
setActiveViewAll(false);
setSelected((selected) => {
if (selected.includes(val)) {
return selected.filter((v) => v !== val);
} else {
return [...selected, val];
}
});
};
const handleViewAll = () => {
setActiveViewAll(true);
setSelected([]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange, selected }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={d}>
<ToggleItem id={id} styles={classes} description={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected}
/>
</div>
</Context.Provider>
);
}
I've fetched a tracklist from API and when I click on the track name I have to be redirected to Details page where description of current track is displayed.
This is component where I fetch data and display in the list.
const TrackList = () => {
const url = `http://ws.audioscrobbler.com/2.0/?method=chart.gettoptracks&api_key=key=json`
const [trackList, setTrackList] = useState([])
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
const res = await fetch(url)
const data = await res.json()
setTrackList(data.tracks.track)
console.log(data.tracks.track)
}
return (
<div>
<Container>
<h1 className='mb-5 mt-5'>Top TrackList</h1>
{trackList.map(item => {
return (
<Row className='mt-1' style={{padding: '5px', border: '1px solid #000', display: 'flex', justifyContent: 'flex-start', alignItems: 'center'}}>
<Col lg={1} md={1} sm={1}>
<a href={item.artist.url}><img src={item.image[1]['#text']} /></a>
</Col>
<Col lg={11} md={11} sm={11}>
<Link to='/:mbid'><h6>{item.artist.name}</h6></Link>
<p>"{item.name}"</p>
</Col>
</Row>
)
})}
</Container>
</div>
)
}
Here I created Details page where main info has to be displayed :
const TrackListDetails = () => {
console.log('props', this.props.match.mbid)
return (
<Container>
</Container>
)
}
But Routes I used in App.js
Am I right ?
function App() {
return (
<div>
<Router>
<NavBar />
<Route path="/" component={TrackList}/>
<Route path="/details/:mbid" component={TrackListDetails}/>
</Router>
</div>
);
}
As stated in react router documentation you can pass state property to link element
<Link
to={{
pathname: "/courses",
state: { description: 'some description' }
}}
/>
You can use it in details page like this:
const { state } = useLocation();
const { description } = state;
But the problem is that you have to persist description when user reloads page. That's why I recommend fetching track details when details page is mounted.
In this react component, I want to render the data into my console when the 'TO FROM SELECTED UNITS' button is clicked by the user, but the button has to be pressed twice for the data to show in the console.
Please ignore:
It looks like your post is mostly code; please add some more details.
It looks like your post is mostly code; please add some more details.
It looks like your post is mostly code; please add some more details.
It looks like your post is mostly code; please add some more details.
Here is my code:
const unitTests = props => {
const autoComplete = useContext(AutoCompleteContext)
const loading = useContext(LoadingContext)
const snackbar = useContext(SnackbarContext)
const user = useContext(UserContext)
const query = new URLSearchParams(props.location.search)
const targetObjectFromURL = query.get('object')
const cookies = new Cookies()
const [unitTestsData, setUnitTestsData] = useState(null)
const [database, setDatabase] = useState('')
const [targetObject, setTargetObject] = useState(null)
const [unitTestsMode, setUnitTestsMode] = useState('Object')
const [AddRemoveDatabaseChanges, setAddRemoveDatabaseChanges] = useState(null)
const [AddRemoveDatabaseMode, setAddRemoveDatabaseMode] = useState('ADD')
console.log(unitTestsData);
useEffect(() => {
async function onLoadUnitTests() {
loading.setLoading(true)
const [objectsAutoCompleteResults, databaseAutoCompleteResults] = await Promise.all([
get('get_objects_autocomplete/all/all/b', user.user),
get('get_databases_autocomplete/a', user.user)
])
autoComplete.setObjectsAutoComplete(objectsAutoCompleteResults)
autoComplete.setDatabasesAutoComplete(databaseAutoCompleteResults)
if(targetObjectFromURL) setTargetObject(targetObjectFromURL)
else if(cookies.get('table')) setTargetObject(cookies.get('table'))
loading.setLoading(false)
}
onLoadUnitTests()
}, [])
useEffect(() => {
async function getUnitTests() {
loading.setLoading(true)
const optionalParams = unitTestsMode === 'Object'
? `?type=object&target=${targetObject}`
: `?type=database&target=${database}`
const url = `get_unit_tests${optionalParams}`
const results = await verifiedGet(url, user.user)
if(results.status === 0) {
setUnitTestsData(results.data)
} else if(results.status >= 20 && results.status <= 30) {
snackbar.statusCheck(results)
user.setSessionTokenMatches(false)
} else snackbar.statusCheck(results)
loading.setLoading(false)
}
if(targetObject || database) getUnitTests()
}, [targetObject,database])
const onObjectSelect = (e, value) => {
props.history.push(`/unittests?object=${value}`)
setTargetObject(value)
cookies.set('table', value, { path: '/'})
}
const onNewTestCaseClick = () => {
props.history.push(`/createtest?object=${targetObject}`)
}
const onDatabaseSelect = (e, value) => setDatabase(value)
const onChangeUnitTestsMode = (e) => setUnitTestsMode(e.target.getAttribute('value'))
const onChangeAddRemoveMode = (e) => setAddRemoveDatabaseMode(e.target.getAttribute('value'))
console.log(AddRemoveDatabaseMode);
const addtoproduction = () => {
let databaseChanges = unitTestsData.map(tests => {
return {
"unit_test_id": tests.unit_test_template_id,
"databases": tests.databases
}
})
setAddRemoveDatabaseChanges(databaseChanges)
if(AddRemoveDatabaseChanges != null && AddRemoveDatabaseMode === 'ADD'){
console.log('added data', AddRemoveDatabaseChanges);
} else if (AddRemoveDatabaseChanges != null && AddRemoveDatabaseMode === 'REMOVE') {
console.log('removed data', AddRemoveDatabaseChanges )
}
}
return (
<PageWrapper title='View Unit Tests'>
<div className='Main UnitTestsAutoCompleteWrapper' style={{display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<div style={{width: '30%', marginRight: '10px'}}>
<DropdownSingle
options={['Object', 'Database']}
value='Object'
name='Search unit tests'
onChange={onChangeUnitTestsMode}
/>
</div>
{unitTestsMode === 'Object' ?
<div>
<AutoCompleteSingle
name='Table'
label='Table'
options={autoComplete.objectsAutoComplete}
onChange={autoComplete.onObjectAutoCompleteFieldUpdate}
onSelect={onObjectSelect}
uniqueIdentifier={0}
initialValue={targetObject}
/>
</div> :
<div style={{display: 'flex', justifyContent: 'space-between' }}>
<div style={{width: '100%'}}>
<DropdownSingle
options={['ADD', 'REMOVE']}
value='ADD'
name='Search unit tests'
onChange={onChangeAddRemoveMode}
/>
</div>
<AutoCompleteSingle
name='Database'
label='Database'
options={autoComplete.databasesAutoComplete}
onChange={autoComplete.onDatabaseAutoCompleteFieldUpdate}
onSelect={onDatabaseSelect}
uniqueIdentifier={1}
style={{width: '1000%'}}
/>
<div>
<Button
text='TO/FROM SELECTED UNIT TESTS'
onClick={addtoproduction}
/>
</div>
</div>
}
</div>
{!unitTestsData ? null :
!unitTestsData.length ?
<div>
This table has no associated unit tests
<div style={{marginTop: '20px'}}>
<Button
text='New Test Case'
icon={<AddIcon className='ButtonIcon' />}
onClick={onNewTestCaseClick}
className='Button Dark Main'
style={{width: '200px'}}
/>
</div>
</div>
:
unitTestsMode === 'Object' ?
<UnitTestsObjectView unitTestsData={unitTestsData}/>
: <UnitTestsDatabaseView unitTestsData={unitTestsData} />
}
{!targetObject ? null :
<div style={{position: 'fixed', bottom: '80px', right: '40px'}}>
<Button
text='New Test Case'
icon={<AddIcon className='ButtonIcon' />}
onClick={onNewTestCaseClick}
className='Button Dark Main'
/>
</div>
}
</PageWrapper>
)
}
export default withRouter(unitTests)
I have 2 onClick functions
function VisitGallery(name) {
const history = useHistory();
console.log("visitgallery", name)
history.push("/gallery")
}
function App() {
const accesstoken = "******************"
const [viewport, setviewport] = React.useState({
latitude: ******
longitude: *******
width: "100vw",
height: "100vh",
zoom: 11
})
const [details, setdetails] = React.useState([
])
React.useEffect(() => {
const fetchData = async () => {
const db = firebase.firestore()
const data = await db.collection("data").get()
setdetails(data.docs.map(doc => doc.data()))
}
fetchData();
}, [])
const [selectedpark, useselectedpark] = React.useState(null);
React.useEffect(() => {
const listener = e => {
if (e.key === "Escape") {
useselectedpark(null);
}
};
window.addEventListener("keydown", listener)
return () => {
window.removeEventListener("keydown", listener)
}
}, [])
return (
<div className="App">
<ReactMapGl {...viewport}
mapboxApiAccessToken={accesstoken}
mapStyle="mapbox://**************"
onViewportChange={viewport => {
setviewport(viewport)
}}>
{details.map((details) =>
<Marker key={details.name} latitude={details.lat} longitude={details.long}>
<button class="marker-btn" onClick={(e) => {
e.preventDefault();
useselectedpark(details);
}}>
<img src={icon} alt="icon" className="navbar-brand" />
</button>
</Marker>
)}
{selectedpark ?
(<Popup
latitude={selectedpark.lat}
longitude={selectedpark.long}
onClose={() => {
useselectedpark(null);
}}
>
<div>
<Card style={{ width: '18rem' }}>
<Card.Body>
<Card.Title>{selectedpark.name}</Card.Title>
<Card.Text>
{selectedpark.postalcode}
</Card.Text>
<Button variant="primary" onClick = VisitGallery() >Visit Gallery</Button>
</Card.Body>
</Card>
</div>
</Popup>)
: null}
{
console.log("in render", details)
}
</ReactMapGl>
</div>
);
}
export default App;
The outer onClick is assigned when the marker is first created, and when it is clicked the useselectedpark function is called, details is then assigned to selectedpark.
The inner onClick is assigned to the function VisitGallery(). When the inner onClick is triggered, i want to navigate to another page, hence the history.push().
Ideally, what i want for it to happen is, when the outer onClick is triggered, the cardview shows, and i have an option to visit the next page, which can be triggered by an onClick within the card. However, what is happening right now is both the onClicks are triggered when i click on the thumbnail. How do i fix it such that it is how i want it to be ideally?
ps: do let me know if my explanation is confusing and i will edit it accordingly
Try adding your second onClick into a callback function?
<Button variant="primary" onClick='()=>{ VisitGallery() }' >Visit Gallery</Button>
So that it doesn't automatically invoke the function until the click is triggered.