React setState occurs after second click - reactjs

I have two Components which are functional. The first one is Parent and the second one is Child and on Parent I have some states and a function which retrieve some data from server and base on it, setData is called then this state is passed to Child to be shown.
The problem is when I first click on a button to trigger that function, state does not change but on second click state is changed and data is shown on Child component.
My Parent is like:
const Parent= () => {
const [data, setData] = useState();
const handleShowDetails = (id) => {
const mainData = data
MyServices.GetData(
command,
(res) => {
mainData = res.name;
}
);
setData(mainData);
}
}
return (
<Child
data={data}
handleShowDetails={handleShowDetails}
/>
);
And my Child is like:
const Child= ({data, handleShowDetails}) => {
return(
<div>
<button onClick={() => handleShowDetails()}/>
{typeof data!== "undefined" ? (
<div className="col-1">
{data}
</div>
) : ("")
</div>
)
}
What can I do to fix this issue?
Note: I implemented MyServices for handling API calls by Axios.

You are setting state even before your GetData call is completed, which must be asynchronous. So setData is called with the old value of data.
Try this:
const handleShowDetails = (id) => {
const mainData = data
MyServices.GetData(
command,
(res) => {
mainData = res.name;
setData(mainData);
}
);
}
Or use await pattern:
const handleShowDetails = async (id) => {
const mainData = data
const ans = await MyServices.GetData(command);
setData(ans.name);
);
}
Note: No need to use mainData i guess.

Related

how to pass dynamique data from child to parent in react native

i want to pass the data of text-input from child to parent to submit the dynamic form. when i use useEffect the phone blocked but i don't know why.please can someone help me to solve this problem.thanks to tell me if there are another way to pass the data.
child component
const RenderComponents = ({ sendChildToParent) => {
const [inputsVal, setInputsVal] = useState({});
const handleChange = (name, value) => {
setInputsVal({ ...inputsVal, [name]: value });
};
const senddata = () => {
sendChildToParent(inputsVal);
};
useEffect(senddata);
return (
<>
{getData.length === 0 ? (
<Empty />
) : (
getData.map((item, index) => {
switch (item.type) {
case "TextInput":
return (
<>
<InputText
onChangeText={(text) => handleChange(item.nameC, text)}
ModuleName={item.nameC}
placeholder={item.options.placeholder}
required={item.options.required}
key={index}
/>
</>
);
case "Phone":...
Parent Component
export function TemplateScreen(props) {
const navigation = useNavigation();
const [getData, setData] = React.useState(Mydata);
const [childData, setChildData] = useState([]);
const sendChildToParent = (dataFromChild) => {
setChildData(dataFromChild);
};
//*************************************Child Componenet*************** */
const RenderComponents = () => {
const [userTeam, setUserTeam] = useState({});
[...other code here...];
**********Parent Component*******
return (
<ScrollView>
<RenderComponents />
<Button
title="Submit"
onPress={()=>null}
/>...
The structure of your parent component is fine. The issues are in your child component, in the following lines:
const RenderComponents = ({ sendChildToParent) => {
const [inputsVal, setInputsVal] = useState({});
const handleChange = (name, value) => {
setInputsVal({ ...inputsVal, [name]: value });
};
const senddata = () => {
sendChildToParent(inputsVal);
};
useEffect(senddata);
it's not good practice to duplicate the input value in local state. Pass the value down from the parent component as well as the setter function.
you're not passing a dependency array to your useEffect function, so it runs on every render of the component. This sets off the following chain of events:
the parent renders
the child renders
useEffect runs, setting the value of the state in the parent
the parent re-renders
This is an endless loop and what causes your app to lock.
there's no need to wrap the state setting functions in your own functions unless you are planning to do additional work there later. There's also no need to run those functions in your component lifecycle (useEffect), because they will run when the input changes.
missing bracket in the first line.
You could rewrite the components in the following way:
// parent component
export function TemplateScreen(props) {
const navigation = useNavigation();
const [getData, setData] = React.useState(Mydata);
const [childData, setChildData] = useState({});
return (
<ScrollView>
<RenderComponents childData={childData} setChildData={setChildData} />
...
// child component
const RenderComponents = ({ childData, setChildData }) => {
const handleChange = (name, value) => {
setChildData({ ...childData, [name]: value });
};
return (
...

Chaining useEffects with early return

Example to make the context clear:
I am trying to render a component with two sets of data coming from API calls. I am also returning early if the first API call fails. The second API call depends on the data of the first API result. I don't want to combine both effects because that would mean the whole of component does not render till I get bot API results.
This is the psuedo code
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
callApi1();
}, []);
// early return so that all the complex logic below is not called on ever render
if (!dataFromEffect1) return <div>No Data1</div>;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
useEffect(() => {
const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
callApi2();
}, [data1]);
return (
<div>
{/* no need to null check here, because of the early return on top */}
{dataFromEffect1}
{/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
{dataFromEffect2 ? (
<div>
{dataFromEffect2}
</div>
) : null}
</div>
);
};
Problem
The above code does not work because you cannot add a useEffect conditionally (the early return messes it up)
Trying to find the best workaround for this problem.
Just creat a component for dataFromEffect2:
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () =>
setDataFromEffect1(await (await fetch("/api1")).json());
callApi1();
}, []);
if (!dataFromEffect1) return <div>No Data1</div>;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
return (
<div>
{dataFromEffect1}
<Component data1={data1} dataFromEffect2={dataFromEffect2} setDataFromEffect2={setDataFromEffect2} />
</div>
);
};
const Component = ({ data1, dataFromEffect2, setDataFromEffect2 }) => {
useEffect(() => {
const callApi2 = async () =>
setDataFromEffect2(
await (await fetch("/api2", { headers: data1 })).json()
);
callApi2();
}, [data1]);
return <div>{dataFromEffect2}</div>;
};
You can try something like this. It will kill both useEffects, but it will not run the second one unless it retrieved data from the first. Also, I did not fix this in your code but you should not use async code within useEffect. This can lead to memory leaks and unnexpected bugs. You are also not cleaning up the fetch from within the useEffect. Academind has a nice blog explaining how to fix this and what will happen if you keep the code like this https://academind.com/tutorials/useeffect-abort-http-requests/
const DataList = () => {
const [dataFromEffect1, setDataFromEffect1] = useState([]);
const [dataFromEffect2, setDataFromEffect2] = useState([]);
useEffect(() => {
const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
callApi1();
}, []);
useEffect(() => {
if(!dataFromEffect1.length) return;
const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
callApi2();
}, [dataFromEffect1]);
// early return so that all the complex logic below is not called on ever render
if (!dataFromEffect1) return <div>No Data1</div>;
return (
<div>
{/* no need to null check here, because of the early return on top */}
{dataFromEffect1}
{/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
{dataFromEffect2 ? (
<div>
{dataFromEffect2}
</div>
) : null}
</div>
);
};
Two options ( I went with the second one for now):
create a separate component for abstracting the second useEffect as suggested by #viet in the answer below.
move the useEffect above the early return, but I will have to duplicate the if conditions inside the effect. As described by in the answer below.

React, unexpected multiple result when using map and fetch

I am building Weather App, my idea is to save city name in localStorage, pass a prop to child component, then iterate using map and display each in seperate child of the first child
The problem is that displayed data doubles/triples on render(depending on component when render occurs) so when I have for example city London and add city Berlin it will render:
London,London,Berlin
The problem is not in AddCity component, it's working correctly but in this mix of asynchronous setState/fetching and maping
Please see the code below
App(parent component)
const App = () => {
const [cities, setCities] = useState([]);
const addCity = (newCity)=>{
console.log('adding')
setCities([...cities, newCity]);
let cityId = localStorage.length;
localStorage.setItem(`city${cityId}`, newCity);
}
useEffect(() => {
loadCityFromLocalStore()
}, [])
const loadCityFromLocalStore =()=>{
setCities([...cities, ...Object.values(localStorage)])
}
return (
<div>
<Header />
<AddCity addCity={addCity}/>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
DisplayWeather (first child)
const DisplayWeather = ({displayWeather}) => {
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
const [fetchData, setFetchData] = useState([]);
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=>[...fetchData , data]));
})
}, [displayWeather])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
Weather component
const Weather = ({data}) => {
return (
<li>
{data.name}
</li>
)
}
It looks like the problem comes from calling setFetchData for cities that you already added previously.
One easy way to fix it would be to store fetch data as an object instead of a dictionary so that you just override the data for the city in case it already exists (or maybe even skip the fetch as you already have the data).
For example:
const [fetchData, setFetchData] = useState({});
useEffect(() => {
displayWeather.map(async city=>{
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=> ({...fetchData, [city]: data})));
})
}, [displayWeather])
Then, to map over fetch data you can just use Object.values:
return (
<>
{Object.values(fetchData).map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
If you want to skip already fetched cities you can do something like this instead:
useEffect(() => {
displayWeather.map(async city=>{
if (!fetchData[city]) {
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=> ({...fetchData, [city]: data})));
}
})

wait for useEffect to update state in order to pass down props?

I have a parent component that is fetching data from an api, I would like to fetch it once on render and set it to state and pass it down as props to a child component that will then map over this state. How can I wait for 'bids' to have items before the child component renders? I thought by passing setBids it would wait until bids is set, but instead im getting an empty array to map over
function Parent () {
const [bids, setBids] = useState([]);
useEffect(() => {
const fetchBids = async () => {
const result = await api.GetBids();
setBids(result.body);
};
fetchBids();
}, [ setBids ]);
return ( <Child bids={bids}/>)
}
export default Parent;
function Child(props) {
const { bids, id } = props;
return (
<Fragment>
{bids.map((responseBidId) => {
if (responseBidId === id) {
return (
<button onClick={handleView}>view</button>
);
} else {
return (
<Button
onClick={handleUpload}
>
Upload
</Button>
);
}
})}
</Fragment>
);
}
export default Child;
You could wait for bids to be populated before rendering the child:
const [bids, setBids] = useState();
return bids ? <Child bids={bids}/> : null;
You can't "wait", not really. If you think about it more in terms of "when" should I render the child it might help though.
function Parent () {
const [bids, setBids] = useState(null); // <-- change to null to indicate unset
useEffect(() => {
const fetchBids = async () => {
const result = await api.GetBids();
setBids(result.body);
};
fetchBids();
}, [ setBids ]);
return bids ? <Child bids={bids}/> : null;
}
export default Parent;

React why the state is not updating when calling a function to initialize it?

Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};

Resources