ReactJS not passing updated State - javascript

So I'm new to React, I'm trying to pass the data after fetching with axios to Component:
index.js
class App extends React.Component {
state = { pokemon: null, errorMsg: '' };
componentDidMount() {
api
.get('', {
params: {
limit: 50,
offset: showrandom(),
},
})
.then((res) => {
const temporary = res.data.results;
const details = { pokemon: [] };
temporary.forEach((e) => {
api
.get('/' + e.name)
.then((res) => details.pokemon.push(res.data))
.catch((e) => console.log(e));
});
this.setState({ pokemon: details.pokemon });
})
.catch((e) => this.setState({ errorMsg: e }));
}
render() {
if (this.state.pokemon) {
return (
<React.Fragment>
<div
className="container-fluid"
style={{ textAlign: 'center', justifyContent: 'center', marginBottom: '5em' }}
>
<img alt="logo" src="logo" />
<p>by: Ihsan Fajar Ramadhan</p>
</div>
<div className="ui five column grid" style={{ padding: '2em' }}>
<Cards data={this.state.pokemon} />
</div>
</React.Fragment>
);
} else {
return (
<React.Fragment>
<div className="ui" style={{ textAlign: 'center', justifyContent: 'center', marginBottom: '5em' }}>
<img alt="logo" src="logo" />
<p>by: Ihsan Fajar Ramadhan</p>
</div>
<div className="ui" style={{ textAlign: 'center', justifyContent: 'center' }}>
<i className="compass loading icon massive blue"></i>
<p className="ui text blue">Loading...</p>
</div>
</React.Fragment>
);
}
}
}
Card.js
import React from 'react';
const Card = (props) => {
let pokemon = props.data.map((pokemon, i) => {
return (
<div className="column" key={i}>
<div className="ui fluid card">
<a className="image" href="/">
<img alt="nama" src={pokemon.sprites.front_default} />
</a>
<div className="content" style={{ textAlign: 'center' }}>
<a className="header" href="/">
{pokemon.species.name}{' '}
</a>
</div>
</div>
</div>
);
});
if (props.data.length > 0) {
return <>{pokemon}</>;
} else {
return <>fetching</>;
}
};
export default Card;
The loading screen rendered successfully, and after the pokemon state updated it passes the condition where if(this.state.pokemon) which means the state has been updated, then it tries to render the element with component. But the problem is the data props to component is not passed yet, and it renders "fetching" instead {pokemon}. I've been searching for solution but I'm stuck. The strange part is that when I changed content (adding or deleting word), the data is passed to the Component.

first of all, thank you to Chris G for the hint in the comment.
There's the problem: a forEach loop doesn't wait for async code inside, only a for loop does, and you haven't even used await. When you call this.setState({ pokemon: details.pokemon });, none of the api calls has finished yet, and it's still [], exactly what causes the behavior you observe.
So here is my solution:
async loadApi(){
const temp= await api.get('',{
params:{
limit:50,
offset:showrandom()
}
});
const pokemon=[];
for(let i=0; i<temp.data.results.length; i++){
await api.get('/'+temp.data.results[i].name)
.then(res => pokemon.push(res.data))
}
this.setState({pokemon:pokemon});
return pokemon;
}
componentDidMount(){
this.loadApi();
}

Related

How do I associate seperate state to each button?

Hello
I am trying to associate a like button with each PaperCard component as shown in the code below. I have included the relevant code. Currently, The like button shows up and every time you click it the counter increases BUT all the buttons share the same state. So I am trying to fix that. I am new to JS and React.
Any help will be appreciated. Thanks!
function Home() {
const [likes, setLikes] = useState(0);
const incrementLikes = () => {
const addToLikes = likes + 1;
setLikes(addToLikes)
}
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);}
return (
<div>
<div style={{display:'flex', justifyContent:'center'}}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Grid key={paper.title}>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</Grid>
))}
<div style={{display:'flex', justifyContent: 'center'}}>
<Button variant="contained" onClick={loadMorePapers}>Load More</Button>
</div>
</div>
)
}
The element from the map callback is extracted as a component, and now every button has its own state.
function Home() {
return (
<div>
<div style={{ display: "flex", justifyContent: "center" }}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<LikeButton paper={paper} key={paper.title} />
))}
<div style={{ display: "flex", justifyContent: "center" }}>
<button variant="contained" onClick={loadMorePapers}>Load More</button>
</div>
</div>
);
}
function LikeButton(paper) {
const [likes, setLikes] = useState(0);
const incrementLikes = () => {
const addToLikes = likes + 1;
setLikes(addToLikes);
};
return (
<div key={paper.title}>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</div>
);
}
Create a new functional component called LikeButton (or something relevant) to house the state for each button independently.
In that component, add the state values you want to track per each button. In your case it seems to just be the likes.
So could be something like:
const LikeButton = () => {
const [likes, setLikes] = useState(0); //likes controlled by state of component
const incrementLikes = () => {
setLikes((prevState) => prevState + 1);
};
return <button onClick={incrementLikes}>Likes: {likes}</button>;
};
Then add that component in place of your existing button and remove the state for likes in the Home component. E.g.:
function Home() {
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);
};
return (
<div>
<div style={{ display: "flex", justifyContent: "center" }}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Grid key={paper.title}>
<LikeButton/>
<PaperCard title={paper.title} abstract={paper.abstract} />
</Grid>
))}
<div style={{ display: "flex", justifyContent: "center" }}>
<Button variant="contained" onClick={loadMorePapers}>
Load More
</Button>
</div>
</div>
);
}
Should you want to control state from the Home component, you can pass the likes as props, but it doesn't seem necessary for what you want.
In this situation you should consider using a reusable button component in order to control state within the component itself. Then you do not have to worry about the buttons sharing the same state. Here would be a simple example of a button component that will track it's count independent of the other buttons that are rendered:
import React, { useState } from 'react';
export default function CounterButton() {
const [count, setCount] = useState(0);
function incrementLikes() {
setCount(count + 1);
}
return (
<button onClick={incrementLikes}>
{count} likes
</button>
);
}
You could simply render these buttons like in the pseudo code below:
{[0, 1, 2, 3].map((num: number, index: number) => (
<div key={index}>
<CounterButton />
</div>
))}
I think you're doing too much in one component. The "likes" in your example are for an individual paper, not for the whole site, right?
Maybe something like this...
function Home() {
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);
}
return (
<div>
<div style={{display:'flex', justifyContent:'center'}}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Paper {...paper} key={paper.title} />
))}
<div style={{display:'flex', justifyContent: 'center'}}>
<Button variant="contained" onClick={loadMorePapers}>Load More</Button>
</div>
</div>
);
}
function Paper(props){
const [likes, setLikes] = useState(0);
const incrementLikes = () => setLikes(likes + 1)
return (
<Grid>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</Grid>
)
}
If the data from the api has a key/id you can pass that to your incrementLikes function and use it to increment the likes for the right item.
const [apiData, setApidData] = useState(...)
const incrementLikes = (id) => {
const updated = apiData.map((paper) => {
if (paper.id === id) {
return {
...paper,
likes: paper.likes + 1
};
}
return paper;
});
setApidData(updated);
};
Then pass the id in the button
<button onClick={() => incrementLikes(paper.id)}>Likes: {paper.likes}</button>
// Get a hook function
const { useState } = React;
const PaperCard = ({ title, abstract }) => {
return (
<div>
<p>{title}</p>
<p>{abstract}</p>
</div>
);
};
const Header = () => {
const [apiData, setApidData] = useState([
{
title: 'Testing likes',
id: 1,
likes: 0,
abstract: 'abs',
},
{
title: 'More likes',
id: 3,
likes: 5,
abstract: 'abstract',
}
]);
const incrementLikes = (id) => {
const updated = apiData.map((paper) => {
if (paper.id === id) {
return {
...paper,
likes: paper.likes + 1
};
}
return paper;
});
setApidData(updated);
};
const loadMorePapers = (e) => {};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<h1>Latest Papers</h1>
</div>
{apiData.map((paper) => (
<div key={paper.title}>
<button onClick={() => incrementLikes(paper.id)}>Likes: {paper.likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract} />
</div>
))}
<div style={{ display: 'flex', justifyContent: 'center' }}>
<button variant='contained' onClick={loadMorePapers}>
Load More
</button>
</div>
</div>
);
};
// Render it
ReactDOM.render(<Header />, document.getElementById('react'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

pass a data from a react component to another component which are on different routes

I have a CountryList react component
import React from "react";
import { Link } from "react-router-dom";
import { BsSearch } from "react-icons/bs";
export default function CountryList({
countries,
}: {
countries: any;
}): JSX.Element {
const [filter, setFilter] = React.useState("");
const [sortType, setSortType] = React.useState("");
console.log(filter);
const sorted = countries.sort((a: { name: string }, b: { name: any }) => {
const isReversed = sortType === "asc" ? 1 : -1;
return isReversed * a.name.localeCompare(b.name);
});
const onSort = (sortType: React.SetStateAction<string>) => {
console.log("changed");
setSortType(sortType);
};
return (
<div style={{ marginTop: "3rem" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
}}
>
<div>List of countries</div>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ position: "relative", marginRight: "1rem" }}>
<input
type="text"
placeholder="Filter"
name="namePrefix"
style={{ padding: "0.35rem" }}
onChange={(e: any) => {
setFilter(e.target.value);
}}
/>
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
<BsSearch size="16" />
</div>
</div>
<div style={{ width: "8rem" }}>
<div className="btn-group">
<button
type="button"
className="btn dropdown-toggle sort-button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{sortType === "asc"
? "Ascending"
: sortType === "desc"
? "Descending"
: "Select"}
</button>
<ul className="dropdown-menu sort-button">
<li>
<button
className="dropdown-item"
type="button"
onClick={() => onSort("asc")}
>
Ascending
</button>
</li>
<li>
<button
className="dropdown-item"
type="button"
onClick={() => onSort("desc")}
>
Descending
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="country-list-items">
{countries &&
sorted.map((item: any, index: number) => (
<div key={index}>
<Link style={{ display: "block" }} to={`/regions`}>
{item.name}
</Link>
</div>
))}
</div>
<div
style={{ marginTop: "20px", display: "flex", justifyContent: "center" }}
>
{countries && countries.length > 10 ? (
<button className="secondary-button">Load More</button>
) : (
<p>There are no more countries</p>
)}
</div>
</div>
);
}
Now from this component I need to pass the data of selected country id while the user clicks on the Link of the respective country, which I will be able to get by {item.code}. Also on clicking the Link the user will be redirected to /regions route where the list of regions of the selected country from this component will be shown. This is the RegionList Component:
import React from "react";
import { Link } from "react-router-dom";
import { BsSearch } from "react-icons/bs";
export default function RegionList(): JSX.Element {
return (
<div style={{ marginTop: "3rem" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
}}
>
<div>List of regions</div>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ position: "relative", marginRight: "1rem" }}>
<input
type="text"
placeholder="Filter"
style={{ padding: "0.35rem" }}
/>
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
<BsSearch size="16" />
</div>
</div>
<div style={{ width: "8rem" }}>
<select name="sort" id="sort">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
</div>
</div>
<div className="country-list-items">
<div>
<Link style={{ display: "block" }} to={`/cities`}>
Alaska
</Link>
</div>
</div>
<div
style={{ marginTop: "20px", display: "flex", justifyContent: "center" }}
>
<button className="secondary-button">Load More</button>
<p>There are no more countries</p>
</div>
</div>
);
}
I need to pass the country id from the CountryList component to this RegionList component because I will do a GET network call in the RegionList component using the selected country id passed from the CountryList component. But I am not able to pass the country id data from CountryList component to RegionList component as they are on different routes and they do not have any common parent component. This is the route file for Countries
import { Route, Routes } from "react-router-dom";
import React from "react";
import CountryComponent from "../components/CountryComponent";
export class CountryRoute extends React.Component {
render() {
return (
<Routes>
<Route path="/" element={<CountryComponent />} />
</Routes>
);
}
}
here <CountryComponent /> is the mother component of CountryList
This is the route file for Regions:
import { Route, Routes } from "react-router-dom";
import React from "react";
import RegionComponent from "../components/RegionComponent";
export class RegionsRoute extends React.Component {
render() {
return (
<Routes>
<Route path="/" element={<RegionComponent />} />
</Routes>
);
}
}
here <RegionComponent /> is the mother component of RegionList
Here is the Main Component where all the components are called
import React from "react";
import { Routes, Route } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import styled from "styled-components";
import "styled-components/macro";
import { CountryRoute } from "../country/route";
import { RegionsRoute } from "../region/route";
import { CitiesRoute } from "../cities/route";
const MainContainer = styled.div`
min-height: 100%;
margin: 5rem;
`;
export const Main = (): JSX.Element => {
return (
<>
<>
<MainContainer>
<div style={{ textAlign: "center" }}>
<b>GEO SOFTWARE</b>
</div>
<div>
<div>
<Routes>
<Route path={"/countries*"} element={<CountryRoute />} />
<Route path={"/regions*"} element={<RegionsRoute />} />
<Route path={"/cities*"} element={<CitiesRoute />} />
</Routes>
</div>
</div>
<ToastContainer
toastClassName={"toastContainer e-12"}
hideProgressBar
position="bottom-left"
closeButton={false}
autoClose={5000}
bodyClassName={"toastBody"}
/>
</MainContainer>
</>
</>
);
};
Now how can I pass the selected country code data from CountryList to the RegionList component.
You can use Query Params for this. In the CountryList you can use the Link like this:
<Link style={{ display: "block" }} to={`/regions?country=COUNTRY_ID`}>
Then in the RegionsList youn can get that Query Parameter from the url and use as you want.
Check this example https://reactrouter.com/web/example/query-parameters
You could set up a simple "store" to keep track of the selected country independently of your component hierarchy.
The simplest possible store
A stripped down, simplest implementation possible might look something like this:
const data = {}
export default {
setCountry: c => data.country = c,
getCountry: () => data.country
}
Because the "store" data is a singleton, any component that imports the store will get the same info, regardless of where it is in the component tree.
import store from './store';
export default () => (
<div>{store.getCountry()}</div>
)
Listening for changes, etc.
The example above omits some details that may be important, depending on what you're doing, like updating views that have already rendered when the country value changes.
If you need that sort of thing you could make the store an event emitter so your components can listen for updates:
import Emitter from 'events';
class CountryStore extends Emitter {
data = {}
getCountry () {
return this.data.country;
}
setCountry (c) {
this.data.country = c;
this.emit('change'); // notify interested parties of the change
}
}
export default new CountryStore();
With the emitter in place, components can register for change notifications when they mount:
import store from './store';
function SomeComponent () {
useEffect(() => {
store.on('change', () => {
// do stuff when store changes happen
}, [])
})
return (<div>...</div>)
}
Custom Hook
To make it easy to do this wherever its needed you could wrap it all up in a custom hook that handles it all and returns the current value and a setter [country, setCountry] just like useState would:
const useCountry = () => {
const [country, setCountry] = useState(store.getCountry());
const handler = () => setCountry(store.getCountry());
useEffect(() => {
store.on('change', handler);
return () => store.off('change', handler);
})
return [country, c => store.setCountry(c)];
}
Then your components have it easy:
import useCountry from './useCountry.js';
export default function SomeComponent () {
const [country, setCountry] = useCountry();
return (
<div>
<div>Current Country: {country}</div>
<button onClick={() => setCountry(Math.random())}>Change Country</button>
</div>
)
}
There are off-the-shelf libraries that will do all of this and more for you, but I thought it might be more helpful to explain an actual rudimentary implementation.
You can have some sort of global state country_id which is initially equal to null.
When user clicks on a country, set that country_id to be equal to the clicked country id.
Now, Inside you RegionList component you can access the country id through country_id state.
You can achieve the state management by different ways:
Prop drilling
Context API
Use Redux or Recoil to handle state-management
As others have pointed out, this is 100% what context is for.
It looks like this:
import React, { createContext, useContext } from 'react';
const MyCountryContext = createContext(null);
export const useCountry = () => useContext(MyCountryContext);
export const MyCountryContext = ({children}) => {
const [country,setCountry] = useState();
return (
<MyCountryContext.Provider value={[country,setCountry]}>
{children}
</MyCountryContext.Provider>
)
}
Use it like this:
export const Main = (): JSX.Element => {
return (
<MyCountryContext>
...rest of your tree
</MyCountryContext>
);
}
Then, in any components that are below MyCountryContext you can use the hook just like useState:
import { useCountry } from './MyCountryContext';
const MyComponentThatUsesCountry = () => {
const [country,setCountry] = useCountry();
return (...)
}

Can't pass form input values from function to component in react

I have a multi-step form using kendo and basically what i want to do is to pass the values from the function Charging to one of the steps so i can use the user inputs in a fetch API GET request. Normally i would have done it with props, but the thing here is that i am passing data from a function. I tried useContext but i can't make it work and i went through the kendo form code many times in order to grasp its methodology but i still can't pass the values. I am getting the values from onStepSubmit() handler and i can get them outside this callback with useState, but even then i can't pass them.
Here is the code for the main function where i get the values
import * as React from "react";
import "./Main.css";
import { Form, FormElement } from "#progress/kendo-react-form";
import { Button } from "#progress/kendo-react-buttons";
import { Stepper } from "#progress/kendo-react-layout";
import { SelectVehicle } from "./components/SelectVehicle";
import { PaymentMethod } from "./components/PaymentMethod";
import chargeIcon from "../../../../img/svg-6.svg";
import { ChargingStep } from "./components/ChargingStep";
import { Payment } from "./components/Payment";
import axios from "axios";
import { AuthContext } from "../../../../shared/context/auth-context";
import Notification from "../../Vehicles/Vehicles1/components/Notification";
const stepPages = [SelectVehicle, PaymentMethod, ChargingStep, Payment];
export const Charging = () => {
const [step, setStep] = React.useState(0);
const [formState, setFormState] = React.useState({});
const [steps, setSteps] = React.useState([
{ label: "Select Vehicle", isValid: undefined },
{ label: "Method", isValid: undefined },
{ label: "Charging", isValid: undefined },
{ label: "Payment", isValid: undefined },
]);
const auth = React.useContext(AuthContext);
const [vehicleId, setVehicleId] = React.useState(false);
const [notify, setNotify] = React.useState({
isOpen: false,
message: "",
type: "",
});
const lastStepIndex = steps.length - 1;
const isLastStep = lastStepIndex === step;
const isPreviousStepsValid =
steps
.slice(0, step)
.findIndex((currentStep) => currentStep.isValid === false) === -1;
const onStepSubmit = React.useCallback(
//add fetch vehicle data based on ID
(event) => {
const { isValid, values } = event;
axios
.get(process.env.REACT_APP_BACKEND_URL + `/cars/user/${auth.userId}`)
.then((response) => {
for (var i = 0; i < response.data.vehicles.length; i++) {
if (values.vehicleID == response.data.vehicles[i]._id) {
setVehicleId(true);
return;
} else {
setVehicleId(false);
return;
}
}
});
const currentSteps = steps.map((currentStep, index) => ({
...currentStep,
isValid: index === step ? isValid : currentStep.isValid,
}));
setSteps(currentSteps);
setStep(() => Math.min(step + 1, lastStepIndex));
setFormState(values);
if (isLastStep && isPreviousStepsValid && isValid && vehicleId) {
// Send to api the data
//alert(JSON.stringify(values));
setNotify({
isOpen: true,
message: "Submitted Successfully",
type: "success",
});
} else if (isLastStep && isPreviousStepsValid && isValid && !vehicleId) {
setNotify({
isOpen: true,
message: "Wrong vehicle ID input",
type: "error",
});
}
},
[
step,
steps,
setSteps,
setStep,
setFormState,
lastStepIndex,
isLastStep,
isPreviousStepsValid,
]
);
const onPrevClick = React.useCallback(
(event) => {
event.preventDefault();
setStep(() => Math.max(step - 1, 0));
},
[step, setStep]
);
return (
<div>
<div className="vehicle__title">
<div className="main__title">
<img src={chargeIcon} alt="charging" />
<div className="main__greeting">
<h1>Charging Simulator</h1>
<p>Simulate a Charge</p>
</div>
</div>
</div>
<div className="wrapper__simulator">
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
<Stepper value={step} items={steps} />
<Form
initialValues={formState}
onSubmitClick={onStepSubmit}
render={(formRenderProps) => (
<div style={{ alignSelf: "center" }}>
<FormElement style={{ width: 480 }}>
{stepPages[step]}
<span
style={{ marginTop: "40px" }}
className={"k-form-separator"}
/>
<div
style={{
justifyContent: "space-between",
alignContent: "center",
}}
className={"k-form-buttons k-buttons-end"}
>
<span style={{ alignSelf: "center" }}>
Step {step + 1} of 4
</span>
<div>
{step !== 0 ? (
<Button
style={{ marginRight: "16px" }}
onClick={onPrevClick}
>
Previous
</Button>
) : undefined}
<Button
primary={true}
disabled={
isLastStep
? !isPreviousStepsValid && !vehicleId
: false
}
onClick={formRenderProps.onSubmit}
>
{isLastStep ? "Submit" : "Next"}
</Button>
</div>
</div>
</FormElement>
</div>
)}
/>
</div>
</div>
<Notification notify={notify} setNotify={setNotify} />
</div>
);
};
export default Charging;
And here is the code for the component where i need these values. In the async componentDidMount function i want the url to be http://localhost:8765/evcharge/api/providers/${values.stationID}/${values.pointID} and get the params.
class OneStep extends React.Component {
data = [
{ text: "100%", id: 1 },
{ text: "75%", id: 2 },
{ text: "50%", id: 3 },
];
state = {
value: { text: "100%", id: 1 },
cost: {text: "", id: null}
};
providers = [];
async componentDidMount() {
const url = "http://localhost:8765/evcharge/api/providers";
const response = await fetch(url);
const data = await response.json();
for (var i = 0; i < data.providers.length; i++) {
this.providers.push({
text: "Provider: " +
data.providers[i]
.Title + " Cost: " + data.providers[i].kWhCost,
id: i + 1 ,
});
}
}
numberFrom = getRandomInt(30, 50, 0);
cost = getRandomInt(0.5, 2, 2);
handleChange = (event) => {
this.setState({
value: event.target.value,
});
console.log(this.data);
console.log(this.providers);
};
handleSecondChange = (event) => {
this.setState({
cost: event.target.value
})
}
render() {
return (
<div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div style={{ width: "50%", marginRight: "25px" }}>
<Button
style={{
width: "50%",
marginRight: "25px",
marginTop: "35px",
textTransform: "capitalize",
color: "#0779e4",
fontWeight: "600",
fontSize: "18px",
right: "50px",
}}
disabled={true}
look="flat"
>
From: {this.numberFrom}
</Button>
</div>
<Button
style={{
width: "50%",
marginRight: "25px",
marginTop: "35px",
textTransform: "capitalize",
color: "#0779e4",
fontWeight: "600",
fontSize: "18px",
}}
disabled={true}
look="flat"
>
Cost per kWh: {this.cost}
</Button>
</div>
<br />
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div style={{ width: "25%", marginRight: "25px" }}>
<DropDownList
data={this.data}
dataItemKey="id"
value={this.state.value}
onChange={this.handleChange}
textField="text"
defaultItem={{ text: "To" }}
/>
</div>
<div style={{ width: "75%", marginRight: "25px" }}>
<DropDownList
data={this.providers}
dataItemKey="id"
value={this.state.providers}
onChange={this.handleSecondChange}
textField="text"
defaultItem={{ text: "Select Provider..." }}
/>
</div>
</div>
<br />
<div
style={{
display: "flex",
justifyContent: "space-between",
height: "250px",
}}
>
<div style={{ width: "50%", marginLeft: "15px" }}>
<Button
style={{
width: "50%",
marginRight: "25px",
marginTop: "35px",
textTransform: "capitalize",
color: "#ff5349",
fontWeight: "600",
fontSize: "18px",
right: "30px",
}}
disabled={true}
look="flat"
>
<CountUp
start={0}
end={parseInt(
(parseFloat(this.state.value.text) - this.numberFrom) *
this.cost
)}
duration={15}
useEasing={true}
decimals={2}
prefix="Expected Cost: "
suffix=" €"
useGrouping={true}
delay={3}
/>
</Button>
</div>
<div
style={{ width: "50%", marginRight: "25px", marginBottom: "450px" }}
>
<div className="g-container">
<div className="g-number">
<CountUp
start={30}
end={parseInt(this.state.value.text)}
duration={15}
useEasing={true}
decimals={2}
suffix=" %"
useGrouping={true}
delay={3}
/>
</div>
<div className="g-contrast">
<div className="g-circle"></div>
<ul className="g-bubbles">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
}
}
export const ChargingStep = <OneStep />;
Instead of exporting and using the rendered output of the OneStep component, just export and use the OneStep component itself, then you can pass whatever props you need to it.
Note there's no issue here with having a functional and class-based component as from the outside they're identical.
Start with the file that contains OneStep, change the export statement from export const ChargingStep = <OneStep />; to export const ChargingStep = OneStep; (or ideally just rename OneStep to ChargingStep and export it directly). Note you'll also have to do this with the other step components so they all work the same (but this is how React components should be exported and used anyway).
Then in the Charging component you can change the line in the return statement from {stepPages[step]} to something like:
const StepPage = stepPages[step];
return (
// ...
<StepPage relevantProp={value}/>
// ...
)
Or you can add special handling for just the ChargingStep step if you don't want to pass those same components to all the other steps, which I'd recommend here.
Further Refactoring
You might consider slightly changing the way you keep track of what step the user is on from a straight index lookup to using string names so you can tell which component is going to be rendered.
You could do something like this:
const stepPages = {
"SelectVehicle": SelectVehicle,
"PaymentMethod": PaymentMethod,
"ChargingStep": ChargingStep,
"Payment": Payment,
};
const stepPageNames = ["SelectVehicle", "PaymentMethod", "ChargingStep", "Payment"];
Then to get the step you're on:
const stepPageName = stepPageNames[step];
const StepPage = stepPages[stepPageName];
Then you can do things like:
let stepPage = <StepPage />
if (stepPageName === "ChargingStep") {
stepPage = <StepPage relevantProp={value}/>
}
And place stepPage in your return statement.

React Router does not update component if url parameter changes

I just implemented a global search in my website and I started having issues with React-Router. It is not updating the view if the url changes parameters.
For example, navigating from /users/454545 to /teams/555555 works as expected. However, navigating from /teams/111111 to teams/222222 changes the url but the component is still /teams/111111.
Here is my code fo the Search Input field.
const SearchResult = ({ id, url, selectResult, text, type }) => (
<Row key={id} onClick={() => selectResult(url)} width='100%' padding='5px 15px 5px 15px' style={{cursor: 'pointer'}}>
<Column alignItems='flex-start' style={{width: '100%'}}>
<Label textAlign='left' color='#ffffff'>{text}</Label>
</Column>
<Column style={{width: '100%'}}>
<Label textAlign='right' color='#ffffff'>{type}</Label>
</Column>
</Row>
)
const SearchInput = (props) => {
const { isSearching, name, onChange, onClear, results } = props;
return (
<Section width='100%' style={{display: 'flex', position: 'relative'}}>
<Wrapper height={props.height} margin={props.margin}>
<i className="fas fa-search" style={{color: 'white'}} />
<input id='search_input' placeholder={'Search for a team, circuit, or user'} name={name} onChange={onChange} style={{outline: 'none', backgroundColor: 'transparent', borderColor: 'transparent', color: '#ffffff', width: '100%'}} />
{onClear && !isSearching && <i onClick={onClear} className="fas fa-times-circle" style={{color: '#50E3C2'}} />}
{isSearching &&
<Spinner viewBox="0 0 50 50" style={{marginBottom: '0px', height: '50px', width: '50px'}}>
<circle
className="path"
cx="25"
cy="25"
r="10"
fill="none"
strokeWidth="4"
/>
</Spinner>
}
</Wrapper>
{results && <Section backgroundColor='#00121A' border='1px solid #004464' style={{maxHeight: '400px', position: 'absolute', top: '100%', left: '0px', width: '97%', overflowY: 'scroll'}}>
<Section backgroundColor='#00121A' style={{display: 'flex', flexDirection: 'column', padding: '15px 0px 0px 0px', justifyContent: 'center', alignItems: 'center', width: '100%'}}>
{results.length === 0 && <Text padding='0px 0px 15px 0px' color='#ffffff' fontSize='16px'>We didn't find anything...</Text>}
{results.length !== 0 && results.map(r => <SearchResult selectResult={props.selectResult} id={r._id} url={r.url} text={r.text} type={r.type} />)}
</Section>
</Section>}
</Section>
)
}
export default SearchInput;
The parent component is a nav bar which looks something like this. I've slimmed it down for readability.
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import SearchInput from '../shared/inputs/SearchInput';
const TopNav = (props) => {
const [search, setSearch] = useState(null);
const [searchResults, setSearchResults] = useState(null);
const debouncedSearchTerm = useDebounce(search, 300);
const [isSearching, setIsSearching] = useState(false);
function clearSearch() {
document.getElementById('search_input').value = '';
setSearchResults(null);
}
function searchChange(e) {
if (!e.target.value) return setSearchResults(null);
setSearch(e.target.value);
setIsSearching(true);
}
async function updateQuery(query) {
const data = {
search: query
}
const results = await api.search.query(data);
setSearchResults(results);
setIsSearching(false);
}
function selectResult(url) {
props.history.push(url);
setSearchResults(null);
}
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
useEffect(() => {
if (debouncedSearchTerm) {
updateQuery(debouncedSearchTerm);
} else {
setSearchResults(null);
}
}, [user, debouncedSearchTerm])
return (
<ContentContainer style={{boxShadow: '0 0px 0px 0 #000000', position: 'fixed', zIndex: 1000}} backgroundColor='#00121A' borderRadius='0px' width='100%'>
<Section style={{display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50px'}} width='1200px'>
<SearchInput height={'30px'} margin='0px 20px 0px 0px' isSearching={isSearching} selectResult={selectResult} onChange={searchChange} onClear={clearSearch} results={searchResults} />
</Section>
</ContentContainer>
)
}
function mapStateToProps(state) {
return {
user: state.user.data,
notifs: state.notifs
}
}
export default connect(mapStateToProps, { logout, fetchNotifs, updateNotifs })(TopNav);
Tl;DR
Using react-router for site navigation. Doesn't update component if navigating from /teams/111111 to /teams/222222 but does update if navigating from /users/111111 to /teams/222222.
Any and all help appreciated!
When a URL's path changes, the current Component is unmounted and the new component pointed by the new URL is mounted. However, when a URL's param changes, since the old and new URL path points to the same component, no unmount-remount takes place; only the already mounted component receives new props. One can make use of these new props to fetch new data and render updated UI.
Suppose your param id is parameter.
With hooks:
useEffect(() => {
// ... write code to get new data using new prop, also update your state
}, [props.match.params.parameter]);
With class components:
componentDidUpdate(prevProps){
if(this.props.match.params.parameter!== prevProps.match.params.parameter){
// ... write code to get new data using new prop, also update your state
}
}
Use KEY:
Another approach could be to use the unique key prop. Passing a new key will force a
component to remount.
<Route path="/teams/:parameter" render={(props) => (
<Team key={props.match.params.parameter} {...props} />
)} />
Re-render does not cause component to re-mount so use useEffect hook to call initializing logic in your component whenever props changes and update your state in the callback.
useEffect(() => {
//Re initialize your component with new url parameter
}, [props]);

Disconnect between UI and backend data

I'm having trouble with a PUT request updating properly. I have a table titled 'issues' with the following columns:
title: title of issue,
description: description of issue,
zipcode: zipcode of issue,
upvotes: number of upvotes an issue receives // defaults to 0
For each issue created, a card is displayed that presents the information above. I want to be able to click on a button that increases the upvote column by +1 on every click. Right now, there is a disconnect between the click and the actual data. The UI's number is always 1 ahead of the backend. If the UI displays 17, for example, the backend reads 16, and so on. I've tried a bunch of different combinations thus far to no avail.
I'm using React and Node/Express for this project.
A thought:
I didn't know if it was related to the my component hierarchy or not, but include both components just in case.
Below is the code my for the card displaying the individual 'issues':
/** #jsx jsx */
import React, { useState } from 'react';
import { jsx } from '#emotion/core';
import axios from 'axios';
import { Button, Card } from 'semantic-ui-react'
function IssueCard({ issue }) {
const [currentIssue, setCurrentIssue] = useState(issue);
const [editedIssue, setEditedIssue] = useState(currentIssue);
// Local storage management
const token = window.localStorage.getItem('token');
// Issue ID to be used as dynamic param
const id = issue.id;
console.log("EDITED ISSUE", editedIssue);
// Once chevron is clicked, the # of upvotes increases by 1
function upvoteIssue() {
// Change state to +1 for upvote
setEditedIssue({ ...editedIssue, upvotes: editedIssue.upvotes + 1 });
axios
.put(`http://localhost:3000/issues/${id}`, editedIssue, {
headers: {
Authorization: token
}
})
.then(response => {
// When I log the response, the first click doesn't increase upvote, but the second click does
console.log("RESPONSE", response.data)
setCurrentIssue(response.data)
})
.catch(error => {
console.log(error);
})
};
return (
<>
<Card>
<Card.Content>
<Card.Header>{ issue.title }</Card.Header>
<Card.Meta>{ issue.zipcode }</Card.Meta>
<Card.Description>
{ issue.description }
</Card.Description>
</Card.Content>
<div
css={{
marginLeft: '12px',
marginBottom: '10px'
}}
>
<Button
size='huge'
icon='heart'
label={{ as: 'p', basic: true, content: editedIssue.upvotes }}
labelPosition='right'
onClick={upvoteIssue}
/>
</div>
</Card>
</>
)
};
export default IssueCard;
Below is the code for the parent component, which is just a profile page that lists the issues if there are any:
/** #jsx jsx */
import React, { useState, useEffect } from 'react';
import { css, jsx } from '#emotion/core';
import { useTheme } from 'emotion-theming';
import axios from 'axios';
import { Link } from 'react-router-dom';
import Banner from './Banner';
import { Button, Card, Icon, Image } from 'semantic-ui-react'
import IssueCard from './IssueCard';
import profile_placeholder from '../images/profile_placeholder.png';
function Profile(props) {
const [currentUser, setCurrentUser] = useState("");
const [issues, setIssues] = useState([]);
const [isEditingUser, setIsEditingUser] = useState(false);
const [isEditingIssue, setIsEditingIssue] = useState(false);
const [issueToUpdate, setIssueToUpdate] = useState({})
let token = window.localStorage.getItem('token')
let id = window.localStorage.getItem('id')
// Importing theme colors
const theme = useTheme();
useEffect(() => {
fetchUser();
fetchIssues();
}, [])
// Fetches user data to populate profile card with proper information
function fetchUser() {
axios
.get(`http://localhost:3000/users/${id}`, {
headers: {
Authorization: token
}
})
.then(response => {
setCurrentUser(response.data);
})
.catch(error => {
console.log(error);
})
};
// Fetches the issues created by the logged user
function fetchIssues() {
axios
.get(`http://localhost:3000/users/${id}/issues`, {
headers: {
Authorization: token
}
})
.then(response => {
setIssues(response.data);
})
.catch(err => {
console.log(err)
})
};
return (
<>
<Banner />
<div
css={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginBottom: '50px'
}}
>
<div
css={{
display: 'flex',
marginTop: '50px'
}}
>
<Card>
<Image src={profile_placeholder} wrapped ui={false} />
<Card.Content>
<Card.Header>{ currentUser.username }</Card.Header>
<Card.Meta>{ currentUser.email }</Card.Meta>
<Card.Description>
{ currentUser.biography }
</Card.Description>
</Card.Content>
<Card.Content extra>
<a>
<Icon name='user' />
Posted Issues: {currentUser.posted_issues}
</a>
</Card.Content>
</Card>
</div>
<div
css={{
display: 'flex',
marginTop: '20px'
}}
>
<Link to="/addIssue">
<Button
icon
labelPosition="left"
color="facebook"
size="huge"
>
<Icon name="add" />
Add Issue
</Button>
</Link>
<Button
icon
labelPosition="left"
color="facebook"
size="huge"
>
<Icon name="redo" />
Edit Profile
</Button>
</div>
<div
css={{
marginTop: '20px',
backgroundColor: theme.colors.turquoise,
width: '50%'
}}
>
<h3
css={{
margin: '0px 10px',
color: theme.colors.white
}}
>Issues created by { currentUser.username }:</h3>
</div>
{issues.map(issue => <IssueCard issue={issue} key={issue.id} /> )}
</div>
</>
)
}
export default Profile;

Categories