React - Passing state as props not causing re-render on child component - reactjs

I have a parent component that initiates state and then once mounted updates it from the results of a get request
const [vehicles, handleVehicles] = useState([])
useEffect(() => {
const token = localStorage.getItem('token')
axios({
//get data from backend
}).then(({data}) => {
handleVehicles(prevState => [...prevState, data])
}).catch((err) => console.log(err))
}, [])
I have the state passed down as a prop into a child component. In my child component I run a check to see if the vehicles array is populated...if it is I return some jsx otherwise I return nothing. My issue is that the state change won't reflect in the prop passed down and cause a re-render. It remains at an empty array unless I refresh the page.
I pass it down via
<RenderTableData vehicles={vehicles} />
My child component is:
const RenderTableData = (props) => {
if (!props.vehicles[0]) {
return null
} else {
return (
props.vehicles[0].map((vehicle) => {
return (
<tr key={vehicle._id}>
<td>{vehicle.name}</td>
<td>{vehicle._id}</td>
<td><button className="has-background-warning">Edit</button></td>
<td><button className="has-background-danger">Remove</button></td>
</tr>
)
})
)
}
}
How would I approach solving this?
Edit - It does actually work as is...For some reason the http request takes an age to return the data (and I was never patient enough to notice)...So I have a new problem now :(

I don't know what exactly is prevState but I think your problem is caused by passing to handleVehicles a function instead of the new value. So your code should be:
const [vehicles, handleVehicles] = useState([])
useEffect(() => {
const token = localStorage.getItem('token')
axios({
//get data from backend
}).then(({data}) => {
handleVehicles([...prevState, data])
}).catch((err) => console.log(err))
}, [])

Why you are using the map function on the object. Your child component should be like below:
const RenderTableData = (props) => {
if (!props.vehicles[0]) {
return null
} else {
return (
props.vehicles.map((vehicle) => {
return (
<tr key={vehicle._id}>
<td>{vehicle.name}</td>
<td>{vehicle._id}</td>
<td><button className="has-background-warning">Edit</button></td>
<td><button className="has-background-danger">Remove</button></td>
</tr>
)
})
)
}
}

I wrote a working example at CodeSandbox. Some comments:
Your effect will run just once, after the component mounts.
If the API returns successfully, a new vehicle list is created with the previous one. But prevState is empty, so this is the same as handleVehicles(data) in this case. If you wanna spread data inside the vehicle list, don't forget to handleVehicles(prevState => [...prevState, ...data]);
useEffect(() => {
const token = localStorage.getItem('token')
axios({
//get data from backend
}).then(({data}) => {
handleVehicles(prevState => [...prevState, data])
}).catch((err) => console.log(err))
}, [])
In your children component, you probably want to map over the vehicles list, not over the first element. So, you should remove the [0] in
const RenderTableData = (props) => {
if (!props.vehicles[0]) {
return null
} else {
return (
props.vehicles[0].map((vehicle) => {
return (
...
)
})
)
}
}

Related

Cannot setstate in nested axios post request in react

I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned. Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(activeId);
}, [activeId);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Edit:
Based on the discussion in the comments.
const handleSaveSections = () => {
// ... Your logic with the `setState` at the end.
}
useEffect(() => {
if (activeId === null) {
return;
}
save2(); // ( or any other function / logic you need )
}, [activeId]);
return (
<button onClick={handleSaveSections}>Click me!</button>
)
As the setState is a async task, you will not see the changes directly.
If you want to see the changes after the axios call, you can use the following code :
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
setTimeout(()=>console.log(activeId),0);
})
useEffect(() => {
}, [activeId]);
const [activeId, setActiveId] = useState(null);
const save1 = () => {
const handleSaveSections = async () => {
activeMetric &&
axios.get(api1, getDefaultHeaders()).then(res => {
if (res.data.length > 0) {
Swal.fire({
text: 'Record already exists',
icon: 'error',
});
return false;
}
else {
const data = {
item1: item1,
item2: item2
}
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
})
}
});
}
handleSaveSections()
}
const save2 = () => {
console.log(activeId); //correct result would be shown here
}
const handleSaveAll = () => {
save1();
save2();
}
return (
<button type="submit" onClick={handleSaveAll}>Save</button>
)

React does not re-render updated map, even with different keys

May be it's a stupid issue, but I can't seem to see it.
I have a file input which uploads an image, and I receive from API multiple images resized into different dimensions.
Before the images are loaded, I don't have a 'file' property in the image object.
After they are loaded - I have the file property.
The problem is, when the resized images are loaded, the map still renders 'no-component'. Even when the key is changed...
Here is the code with some console debugging for more clarity:
{newsImages.map(image => {
console.log('Has file?', image.hasOwnProperty('file'))
const imageComponent = image.hasOwnProperty('file')
? 'has component' // (<AuthorizedImage fileId={image.file} />)
: 'no component';
console.log('Image component: ', imageComponent);
const key = image.label + (image.hasOwnProperty('file') ? image.file : '-');
console.log('Key: ', key);
return (
<div key={key}>
<FormUploadFile
label={`Image for ${image.label}`}
placeholder={"Choose image"}
onChange={value => console.log(value)}
/>
{imageComponent}
<br />
{key}
</div>
)
})}
So far so good. But the web page itself does not show the changes like you can see below:
EDIT: More details on the component. Here is how the images are processed:
const [newsImages, setNewsImages] = useState([]);
useEffect(() => {
Api.NewsImageSizes.List()
.then(response => {
let newsImages = [];
response['hydra:member'].forEach(size => {
newsImages.push({label: size.label})
})
setNewsImages(newsImages.reverse());
})
.catch(ErrorHandling.GlobalStateError);
}, []);
Then when a file is uploaded...
const generateNewsImages = (file) => {
Api.Files.Upload(file)
.then(response => {
Api.News.GenerateImages(response.id)
.then(response => {
response.images.forEach(image => {
newsImages.forEach((element, index) => {
if (element.label === image.label) {
element.file = image.file;
newsImages[index] = element;
}
})
})
console.log('Setting news images...', newsImages);
setNewsImages(newsImages);
})
.catch(ErrorHandling.GlobalStateError);
})
.catch(ErrorHandling.GlobalStateError);
}
And the console again:
Looks like you are mutating the state in the generateNewsImages function.
.then(response => {
response.images.forEach(image => {
newsImages.forEach((element, index) => { // <-- newsImages is state reference
if (element.label === image.label) {
element.file = image.file; // <-- mutation!!
newsImages[index] = element;
}
})
})
console.log('Setting news images...', newsImages);
setNewsImages(newsImages); // <-- save same reference back into state
})
When updating React state you must create shallow copies of all state (and nested state) that you are updating.
.then(response => {
const nextNewsImages = newsImages.slice(); // <-- shallow copy state
response.images.forEach(image => {
nextNewsImages.forEach((element, index) => {
if (element.label === image.label) {
nextNewsImages[index] = { // <-- new object reference
...nextNewsImages[index], // <-- shallow copy
file: image.file, // <-- update property
};
}
})
})
console.log('Setting news images...', nextNewsImages);
setNewsImages(nextNewsImages); // <-- save new
})

TypeError: can't convert undefined to object In React

I'm able to fetch the data from API, but not able to set the data into react state variable. Using useEffect. It's Weird because Initially Code was working fine, I was able to set the data into state variable, but after writing bunch of code. I'm getting this error.
App.js
const fetchData = async () => {
try {
const response = await axios.get(
"https://60d007f67de0b200171079e8.mockapi.io/bakery"
);
const { data } = response;
return data;
} catch (err) {
console.error(err)
}
};
const extractData = (bakerys) => {
const bakery = bakerys[0];
const header = [];
Object.keys(bakery).forEach((objKeys) => {
const value = bakery[objKeys];
// if(type of value !== 'object'){
header.push(objKeys);
})
return header;
};
export default function App() {
const [bakerys, setBakerys] = useState([]);
const [flatbakery, setFlatbakery] = useState([]);
useEffect(() => {
fetchData().then((randomData) => {
console.log('randomData ->', randomData) // able to console data as an Array of object
setBakerys(randomData); // Not able to set the randomData into state variable
console.log('bakerys', bakerys)
})
}, []);
useEffect(() => {
setFlatbakery(extractData(bakerys));
}, [bakerys]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Edit to see some magic happen!</h2>
<table>
<thead>
<tr>
{flatbakery.map((headers, idx) => (
<th key={idx}>
{headers}
</th>
))
}
</tr>
</thead>
</table>
</div>
);
}
Output
This case would come up when your bakerys array is empty, that is bakerys[0] is essentially undefined. You probably need to add some sort of check before you try to iterate the keys of it.
const extractData = (bakerys) => {
const bakery = bakerys[0];
const header = [];
if(bakery) { // only run the following block if bakery is not undefined(or falsey)
Object.keys(bakery).forEach((objKeys) => {
const value = bakery[objKeys];
// if(type of value !== 'object'){
header.push(objKeys);
})
}
return header;
};
EDIT: It appears I have forgotten to mention WHY bakerys may be empty initially. UseEffect runs when the component mounts as well, so the first time that it is called, bakerys is still an empty array. As subsequent updates are made to it, it will eventually be populated with data, so you should always attempt to check if the value has been populated before attempting to run any operations on it.

React Query useMutation set mutationKey dynamically

In my React project using React Query, I have a functional component MoveKeywordModal such that:
when it first loads, it fetches from API endpoint api/keyword_lists to fetch a bunch of keywordLists data. For each of these keywordLists, call it list, I create a clickable element.
When the clickable element (wrapped in a HoverWrapper) gets clicked, I want to send a POST API request to api/keyword_lists/:list_id/keyword_list_items/import with some data.
where :list_id is the id of the list just clicked.
export const MoveKeywordModal = ({
setShowMoveKeywordModal,
keywordsToMove
}) => {
const { data: keywordLists } = useQuery('api/keyword_lists', {})
const [newKeywordList, setNewKeywordList] = useState({})
const { mutate: moveKeywordsToList } = useMutation(
`api/keyword_lists/${newKeywordList.id}/keyword_list_items/import`,
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
const availableKeywordLists = keywordLists
.filter(l => l.id !== activeKeywordList.id)
.map(list => (
<HoverWrapper
id={list.id}
onClick={() => {
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList, data: keywordsToMove }
})
}}>
<p>{list.name}</p>
</HoverWrapper>
))
return (
<>
<StyledModal
isVisible
handleBackdropClick={() => setShowMoveKeywordModal(false)}>
<div>{availableKeywordLists}</div>
</StyledModal>
</>
)
}
Despite calling setNewKeywordList(list) in the onClick of the HoverWrapper, it seems the newKeywordList.id is still not defined, not even newKeywordList is defined.
What should I do to fix it?
Thanks!
react doesn’t perform state updates immediately when you call the setter of useState - an update is merely 'scheduled'. So even though you call setNewKeywordList, the newKeywordList will not have the new value in the next line of code - only in the next render cycle.
So while you are in your event handler, you’ll have to use the list variable:
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList: list, data: keywordsToMove }
})
/edit: I just realized that your call to useMutation is not correct. It doesn’t have a key like useQuery, it has to provide a function as the first argument that takes variables, known as the mutation function:
const { mutate: moveKeywordsToList } = useMutation(
(variables) => axios.post(`api/keyword_lists/${variables.newKeywordList.id}/keyword_list_items/import`),
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
see also: https://react-query.tanstack.com/guides/mutations

State property updated value cannot be accessed in onClick function

I'm using React Hooks. I set the state property questions after an axios fetch call. Now when I click a button, in its function questions state is still empty
const [questions, setQuestions] = useState([]);
const [customComponent, setCustomComponent] = useState(<div />);
useEffect(() => {
axios.get("urlhere").then(res => {
console.log(12, res.data);
setQuestions(res.data);
res.data.map(q => {
if (q.qualifyingQuestionId == 1) {
setCustomComponent(renderSteps(q, q.qualifyingQuestionId));
}
});
});
}, []);
const handleNext = i => {
console.log(32, questions); //questions is still an empty array here
};
const renderSteps = (step, i) => {
switch (step.controlTypeName) {
case "textbox":
return (
<div key={i}>
<input type="text" placeholder={step.content} />
<button onClick={() => handleNext(i)}>Next</button>
</div>
);
}
};
return <>{customComponent}</>;
Do I need to use reducers here and put the custom component in another "file"?
setQuestions does not update state immediately, you should use the prevState instead to access the new value.
Here's a sandbox to match your codes with some explanation on why it was empty > https://codesandbox.io/s/axios-useeffect-kdgnw
You can also read about it here: Why calling react setState method doesn't mutate the state immediately?
Finally I have my own solution
I passed down the data from the fetch function to another component as props
useEffect(() => {
axios.get('url')
.then((data) => {
setCustomComponent(<Questions questions={data} />)
})
}, [])

Resources