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

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
})

Related

React Native not rerendering after state change

I realize that this is a common problem but I haven't found a solution that doesn't feel like it's just covering up the real problem. I have code in a component that is not being updated when the state changes. I'm pretty sure that the state is changing because when I save my code while the simulator is on the screen, it updates, but it doesn't updating when going to that screen normally. Also, in the code segment below there are two very similar functions. The first one works and renders properly. The second one works but doesn't rerender after updating.
fetchUsername().then((uname) => {
setUsername(uname);
return uname;
}).then((uname) =>
(fetchPaymentsInChat(props.threadID)
.then((payments) => {
return Promise.all(
payments.map(pid => {
return fetchPayment(pid)
.then(paymentDetails => {
return paymentDetails.sender === uname ? paymentDetails.value * paymentDetails.users.length : 0
})
})
);
}).then((paymentValues) => {
const paymentSum = paymentValues.reduce((acc, val) => parseInt(acc) + parseInt(val), 0);
setAmmountReceive(paymentSum);
}).catch(err => {
alert(err);
}),
fetchPaymentsInChat(props.threadID)
.then((payments) => {
return Promise.all(
payments.map(pid => {
return fetchPayment(pid)
.then(paymentDetails => {
return paymentDetails.users.includes(username) ? paymentDetails.value : 0
})
})
);
}).then((paymentValues) => {
const paymentSum = paymentValues.reduce((acc, val) => parseInt(acc) + parseInt(val), 0);
setAmmountOwe(paymentSum);
}).catch(err => {
alert(err);
}))

display live shell output in react dom using electron ipc

I can only access ipcRenderer in preload.js ( i disabled nodeIntegration ) so how do i display the output line by line whenever i get the output in preload.js
main.js
function execShellCommands(commands) {
let shellProcess = spawn("powershell.exe", [commands[0]])
shellProcess.stdout.on("data", (data) => {
mainWindow.webContents.send("sendToRenderer/shell-output", data.toString())
})
shellProcess.stderr.on("data", (data) => {
mainWindow.webContents.send("sendToRenderer/shell-output", "stderr: " + data.toString())
})
shellProcess.on("exit", () => {
mainWindow.webContents.send("sendToRenderer/shell-output", "shell-exited")
commands.shift()
if (0 < commands.length) {
execShellCommands(commands)
}
})
}
ipcMain.on("sendToElectron/execShellCommands", (event, args) => {
execShellCommands(args)
})
preload.js
let API = {
execShellCommands: (action) => ipcRenderer.send("sendToElectron/execShellCommands", action)
}
contextBridge.exposeInMainWorld("ElectronAPI", API)
ipcRenderer.on("sendToRenderer/shell-output", (event, output) => {
console.log(output)
})
react's App.jsx
ElectronAPI.execShellCommands(["spicetify apply"])
The output in printed one by one in the console but how do i display the output in react DOM (App.jsx) one by one in a p tag?
First, you need to expose the shell-output listener in you API so your components can access it:
shellOutput: (callback) => {
const channel = "sendToRenderer/shell-output";
const subscription = (_event, output) => callback(output);
ipcRenderer.on(channel , subscription);
return () => {
ipcRenderer.removeListener(channel, subscription);
};
}
On your components, you can now access this function with window.ElectronAPI.shellOutput. To log the outputs, you can create a state to store them, and set the listener with a useEffect():
const [outputs, setOutputs] = useState([]);
useEffect(() => {
const removeOutputListener = window.ElectronAPI.shellOutput(output => {
setOutputs(previousOutputs => [
...previousOutputs,
output
]);
});
return removeOutputListener;
}, []);
return (
<div>
{outputs.map((output, i) => (
<p key={i}>{output}</p>
))}
</div>
);

Trying to get data from api and map to another component in React

I'm trying to map an array of movies which I get from an API.
The data is fetched successfully but when I try to map the values and display, it becomes undefined and does not show anything.
I'm new to React so any help and advice would be helpful.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios
.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
})
.catch((error) => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return (
<div>
{items.map((item) => {
<p>{item.title}</p>;
})}
</div>
);
The data is stored like this:
0: {
adult: false,
backdrop_path: '/9eAn20y26wtB3aet7w9lHjuSgZ3.jpg',
id: 507086,
title: 'Jurassic World Dominion',
original_language: 'en',
...
}
You're not returning anything from your map
{
items.map((item) => {
// Add a return
return <p>{item.title}</p>
})
}
First, your items value is an empty array[] as you have initialized with setState([]) and your useEffect() runs only after your component is rendered which means even before you could do your data fetching, your HTML is being displayed inside which you are trying to get {item.title} where your items is an empty array currently and hence undefined. You will face this issue often as you learn along. So if you want to populate paragraph tag with item.title you should fast check if your items is an empty array or not and only after that you can do the mapping as follow and also you need to return the element from the map callback. If it takes some time to fetch the data, you can choose to display a loading indicator as well.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
}).catch(error => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return ( < div > {
items.length !== 0 ? items.map((item) => {
return <p > {
item.title
} < /p>
}) : < LoadingComponent / >
}
<
/div>
);
Good catch by Ryan Zeelie, I did not see it.
Another thing, since you're using promises and waiting for data to retrieve, a good practice is to check if data is present before mapping.
Something like :
return (
<div>
{ (items.length === 0) ? <p>Loading...</p> : items.map( (item)=>{
<p>{item.title}</p>
})}
</div>
);
Basically, if the array is empty (data is not retrieved or data is empty), display a loading instead of mapping the empty array.

Why does React state return undefined but page still loads from state OK?

I am attempting to develop a React app which makes a call to a database to load a set of pages to a board to build a drag and drop decision tree.
I am only just starting out with React, so keen to hear about anything I'm doing wrong here.
Using 'useEffect' the pageTree function will load the pages up on the first load and on every refresh, however the pages state returns with an empty array instead of the current pages.
Strangely enough the pages all show up on the board with the pages.map function which works on the pages state... (which returns as empty on console.log...)
If I add a page to the array it saves the change to the database, but then will only show the new page on the board. You will then have to refresh to see the new set of pages (including the added page).
Calls to add or delete a page are called by the layout menu buttons in the parent component.
Console after refresh
Additionally, if I move a page, the state will console OK:
Page state in console after moving a page. DB call and state update works OK
function PageTree({AddNewPageFunc}) {
const [pages, setPages] = useState([]);
const movePage = useCallback((droppedPage) => {
const updatedPages = pages.map(page => droppedPage._id == page._id ? droppedPage : page);
setPages(updatedPages);
}, [pages]);
const [{isOver}, drop] = useDrop(() => ({
accept: ItemTypes.PAGECARD,
drop(page, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let x = Math.round(page.x + delta.x);
let y = Math.round(page.y + delta.y);
page.x = x;
page.y = y;
movePage(page);
setNewPagePosition(page);
return undefined;
},
}), [movePage]);
const setNewPagePosition = async (pageDetails) => {
console.log("function called to update page position");
Api.withToken().post('/pageupdate/'+pageDetails._id,
pageDetails
).then(function (response) {
console.log("moved page: ",response.data)
}).catch(function (error) {
//console.log(error);
});
}
React.useEffect(() => {
AddNewPageFunc.current = AddNewPage
}, [])
const AddNewPage = useCallback(() => {
console.log("calling add new page function")
console.log("the pages before the API call are ",pages)
Api.withToken().post('/addblankpage/'
).then(function (response) {
console.log("produced: ",response.data);
setPages(pages.concat(response.data))
console.log("the pages after updating state are: ",pages)
}).catch(function (error) {
//console.log(error);
});
}, [pages]);
const handleDelete = async (id) => {
Api.withToken().post('/deletepages/'+id
).then(function (response) {
let index = pages.findIndex(function(item){
return item.id === response.data._id;
});
const PageRemoved = pages.splice(index, 1);
setPages(PageRemoved);
}).catch(function (error) {
//console.log(error);
});
}
useEffect(() => {
Api.withToken().get('/pages/')
.then(res => {
setPages(res.data);
console.log('res data ',res.data);
console.log('pages ',pages);
})
}, []);
return (
<div ref={drop} style={styles}>
{pages.map((page) => (<PageCard page={page} id={page._id} key={page._id} handleDelete={() => handleDelete(page._id)} handleMaximise={() => handleMaximise(page)} handleCopy={() => handleCopy(page)}/>))}
</div>
)
}
export default PageTree;
As Danielprabhakaran pointed out, the issue was the callback in React.useEffect. On adding a new page it needed to send the updated page state back to the parent component.
Using console.log on a state after an API call seems to be fraught, even if using .then(console.log(state)
function PageTree({AddNewPageFunc}) {
const [pages, setPages] = useState([]);
const movePage = useCallback((droppedPage) => {
const updatedPages = pages.map(page => droppedPage._id == page._id ? droppedPage : page);
console.log("updated pages ",updatedPages);
setPages(updatedPages);
console.log("set pages ",pages);
}, [pages]);
const [{isOver}, drop] = useDrop(() => ({
accept: ItemTypes.PAGECARD,
drop(page, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let x = Math.round(page.x + delta.x);
let y = Math.round(page.y + delta.y);
page.x = x;
page.y = y;
movePage(page);
setNewPagePosition(page);
return undefined;
},
}), [movePage]);
const setNewPagePosition = async (pageDetails) => {
console.log("function called to update page position");
Api.withToken().post('/pageupdate/'+pageDetails._id,
pageDetails
).then(function (response) {
console.log("?worked ",response)
}).catch(function (error) {
//console.log(error);
});
}
React.useEffect(() => {
AddNewPageFunc.current = AddNewPage
}, [pages])
const AddNewPage = useCallback(() => {
console.log("calling add new page function")
console.log("the pages before the API call are ",pages)
Api.withToken().post('/addblankpage/'
).then(function (response) {
console.log("produced: ",response.data);
setPages(pages.concat(response.data))
console.log("the pages after updating state are: ",pages)
}).catch(function (error) {
//console.log(error);
});
}, [pages]);
const handleDeletedCallback = (deletedIndex) => {
console.log("delete callback fired")
setPages(pages.splice(deletedIndex, 1));
}
useEffect(() => {
Api.withToken().get('/pages/') //can add in a prop to return only a given tree once the app gets bigger
.then(res => {
setPages(res.data);
console.log('res data ',res.data);
console.log('pages ',pages);
})
}, []);
return (
<div ref={drop} style={styles}>
{pages.map((page, index) => (<PageCard page={page} id={page._id} key={page._id} index={index} deleteCallback={handleDeletedCallback} handleMaximise={() => handleMaximise(page)} handleCopy={() => handleCopy(page)}/>))}
</div>
)
}
export default PageTree;

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

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 (
...
)
})
)
}
}

Resources