Reactjs updated prop is not shown - reactjs

I tried to create a interactable map following this example here: https://docs.mapbox.com/mapbox-gl-js/example/cluster/
In my componentDidMount (where I create a mapboxgl) I implemented clickable markers, when clicked on the markers a popup appears which displays various informations.
After the click I want to call a second function (fetch) to get more data on that specific marker: this.props.getData(id);
I then want to display these data in the same popup as the other information.
My problem is that this.props.testdata is empty on the first click. If I double-click on the marker, the data appear. So my guess is that my component does not notice the change of the state/prop and therefore does not update?
How do I do that or what am I missing?
Map.js
this.map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const id = e.features[0].properties.id;
const infos = e.features[0].properties.infos;
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
if (id == null) {
console.log("Missing id, cant get informations")
return;
}
this.props.getData(id);
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`
Id: ${id}
<br>
Infos: ${infos}
<br>
<br>
Testdata: ${this.props.testdata}
`
)
.addTo(this.map);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
});
App.js (getData function):
getData = (id) => {
if (id== null) {
console.log("Missing id")
return;
}
const {mapCenter, startDate, endDate} = this.state;
const neo4j = require('neo4j-driver')
const driver = neo4j.driver('bolt://xxx', neo4j.auth.basic("xx", "xx-xx"))
const session = driver.session()
session
.run('Here goes a neo4j cypher statment',{id: id})
.then((results)=> {
const data= [];
results.records.forEach((record) => data.push([record.get("r"), record.get("n"), record.get("b")]))
this.setState({
data
});
session.close()
driver.close()
}).catch(e => {
console.log(e)
session.close();
});
};

I am not familiar with neo4j, but it is apparent that getData(id) fetches data from a server. This is going to be an asynchronous operation, so you should add a state property to maybe show a spinner while data is being fetched?
Regarding testdata not being available, I do not see the code where it is being set.
Maybe your setState code should be:
this.setState({
testdata: data
});
//If your data prop is testdata.
As per the current setState, data property of your component state would be set with server response.
Updates:
Temporary fix for async server call:
You can change following methods and try if it fixes your issue:
this.map.on('click', 'unclustered-point', async (e) => {
// ...previous code
await this.props.getData(id);
// This forces the following code to execute synchronously. Basically it should wait for your API call to be complete
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`
Id: ${id}
<br>
Infos: ${infos}
<br>
<br>
Testdata: ${this.props.testdata}
`
)
.addTo(this.map);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
});
getData = (id) => {
//... previous code
// we return a promise to use await in the onClick handler
return session
.run('Here goes a neo4j cypher statment',{id: id})
.then((results)=> {
const data= [];
results.records.forEach((record) => data.push([record.get("r"), record.get("n"), record.get("b")]))
this.setState({
data
});
session.close()
driver.close()
}).catch(e => {
console.log(e)
session.close();
});
}
If you are still facing an issue, please create a sample app and share.

I have not yet managed to fix the original problem.
However, I have found another solution:
In my Map.js I'm calling the this.props.testdata in th UI like this:
<div className="sidebar">
info: {JSON.stringify(this.props.testdata)}
</div>

Related

uswSWRInfinite shows stale data when mutating with local data

I use uswSWRInfinite to paginate data by a cursor (the id of the last loaded item). When I edit or delete items on page 2+ (for some reason, this doesn't happen on page 1), and I mutate by passing modified data, after revalidation, I see the old data again.
This does not happen if I either 1. mutate without local data (only revalidation) or 2. disable revalidation (only mutate with local data). Both together cause the bug.
Here's the relevant code:
useSWRInfinite setup:
const {
data: repliesPages,
size: repliesPagesSize,
setSize: setRepliesPagesSize,
isLoading: repliesLoading,
error: repliesLoadingError,
mutate: mutateRepliesPages,
} = useSWRInfinite(
getPageKey,
([commentId, lastReplyId]) => BlogApi.getRepliesForComment(commentId, lastReplyId));
The update/delete callbacks:
replies?.map(reply => (
<CommentBody
comment={reply}
onReplyCreated={addLocalReply}
key={reply._id}
onCommentUpdated={(updatedReply) => {
const updatedRepliesPages = repliesPages?.map(page => {
const updatedReplies = page.comments.map(existingReply => existingReply._id === updatedReply._id ? updatedReply : existingReply);
const updatedPage: GetCommentsResponse = { ...page, comments: updatedReplies };
return updatedPage;
});
mutateRepliesPages(updatedRepliesPages); // this works properly if I don't pass data or set revalidate : false
}}
onCommentDeleted={() => {
const updatedRepliesPages = repliesPages?.map(page => {
const updatedReplies = page.comments.filter(existingReply => existingReply._id !== reply._id);
const updatedPage: GetCommentsResponse = { ...page, comments: updatedReplies };
return updatedPage;
});
mutateRepliesPages(updatedRepliesPages); // this works properly if I don't pass data or set revalidate : false
}}
/>
));
The callbacks are triggered after we got the updated item back from the server:
async function onSubmit({ text }: { text: string }) {
if (!text) return;
try {
const updatedComment = await BlogApi.updateComment(comment._id, text);
onCommentUpdated(updatedComment);
} catch (error) {
console.error(error);
alert(error);
}
}
async function deleteComment() {
try {
setDeleteInProgress(true);
await BlogApi.deleteComment(comment._id);
onCommentDeleted();
} catch (error) {
console.error(error);
alert(error);
} finally {
setDeleteInProgress(false);
}
}
Here's a recording of the problem happening:
The behavior I expect, is that SWR shows the updated data after revalidation.
You need to fire a request to update the data on the server (BlogApi).
mutate() will update the data on the client side, but not on the server. You're updating the data locally, then the data revalidates (refetches), which replaces the local data with the server data, undoing your updates.
Add the appropriate request (probably POST) to your code. You can add it immediately before mutateRepliesPage(), or you can include it as part of the function passed to mutate's second argument like in this example.

How could I write this function so it doesn't setState within the foreach everytime

The function collects role Assignment PrincipalIds on an item in SPO. I then use a foreach to populate state with the Title's of these PrincipalIds. This all works fine but it's inefficient and I'm sure there is a better way to do it than rendering multiple times.
private _permJeChange = async () => {
if(this.state.userNames){
this.setState({
userNames: []
});
}
var theId = this.state.SelPermJEDD;
var theId2 = theId.replace('JE','');
var info = await sp.web.lists.getByTitle('MyList').items.getById(theId2).roleAssignments();
console.log(info, 'info');
var newArr = info.map(a => a.PrincipalId);
console.log(newArr, 'newArr');
// const userIds = [];
// const userNames = [];
// const userNameState = this.state.userNames;
newArr.forEach(async el => {
try {
await sp.web.siteUsers.getById(el).get().then(u => {
this.setState(prevState => ({
userNames: [...prevState.userNames, u.Title]
}));
// userNames.push(u.Title);
// userIds.push(el);
});
} catch (err) {
console.error("This JEForm contains a group");
}
});
}
I've left old code in there to give you an idea of what I've tried. I initially tried using a local variable array const userNames = [] but declaring it locally or even globally would clear the array everytime the array was populated! So that was no good.
PS. The reason there is a try catch is to handle any SPO item that has a permissions group assigned to it. The RoleAssignments() request can't handle groups, only users.
Create an array of Promises and await them all to resolve and then do a single state update.
const requests = info.map(({ PrincipalId }) =>
sp.web.siteUsers.getById(PrincipalId).get().then(u => u.Title)
);
try {
const titles = await Promise.all(requests);
this.setState(prevState => ({
userNames: prevState.userNames.concat(titles),
}));
} catch (err) {
console.error("This JEForm contains a group");
}

Rendering elements of array fetched with React does not work

I'm successfully fetching an array of objects from a local ethereum blockchain (successful as in, I've logged the data in componentDidMount and it is what its supposed to be). Its a dynamically sized array
(inventory), so I gotta fetch it element by element. Here are the relevant parts:
async componentDidMount() {
this.setState({fetching: true}, () => {
this.loadData()
.then(result => this.setState({fetching: false}))
})
}
async loadData() {
if (typeof window.ethereum !== 'undefined') {
const web3 = new Web3(window.ethereum)
const netId = await web3.eth.net.getId()
const accounts = await web3.eth.getAccounts()
this.setState({account: accounts[0]})
if(typeof accounts[0] !== 'undefined'){
const balance = await web3.eth.getBalance(accounts[0])
this.setState({balance: balance})
}
else {
window.alert('Please login with MetaMask')
}
try {
const plotRepository = new web3.eth.Contract(PlotRepository.abi, PlotRepository.networks[netId].address)
const plot = await plotRepository.methods.claimPlot(this.state.account).send({from: this.state.account})
this.setState({plot: new web3.eth.Contract(Plot.abi, _plot.plot)})
const size = await this.state.plot.methods.size().call()
const tiles = await this.state.plot.methods.getTiles().call()
await this.loadInventory() // this is where I'm loading the array in question
this.setState({size: size})
this.setState({tiles: tiles})
// logging the array here works
} catch (e) {
window.alert(e)
}
} else {
window.alert('Please install MetaMask')
}
}
async loadInventory() {
for (const [key, value] of Object.entries(inventoryinfo)) {
const item = await this.state.plot.methods.getInventoryItem(key).call()
this.setState({inventory: [...this.state.inventory, {
name: key,
value: parseInt(item[0][1]),
count: item[1]
}]})
}
}
Again, logging the array in the fetching functions works just fine. I'm also using a flag (fetching) which denotes whether all data has been successfully loaded or not, and I only try to render data once everything is loaded:
render() {
const { plot, account, fetching, tiles, inventory } = this.state
return (
<div className="App">
<p>Currently logged in with account: { this.state.account }</p>
<p>Balance: { this.state.balance } </p>
{ fetching ? // make sure things are fetched before rendering them
<p>Loading</p> :
<div>
<UserPlot tileData={tiles} plot={plot} account={account}/>
{
inventory[0].name // inventory[0] is undefined
// inventory.length is not
}
</div>
}
</div>
);
}
Trying to display any element of the array gives me an undefined error. Rendering the array length works though ({inventory.length}). This is weird since I'm doing the same thing with another array that I'm fetching and displaying in the UserPlot component and that works just fine (only difference is that in this case the array is static and I can load it all in one go but I don't think that has anything to do with it)

Why when the state is updated the changes in the rendering are not updated?

I have created a hook to access the collections of the database and its methods.
import { remote } from 'electron'
import { useState, useEffect } from "react"
function useCollections(collections = []) {
let [dbInstances, setDbInstances] = useState(null)
let [data, setData] = useState(null)
// DB METHODS
// Create
let create = async (doc, dbName) => {
await dbInstances[dbName].create(doc)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// Get details
let getDetails = async (id, dbName) => {
let doc = await dbInstances[dbName].read(id)
return doc
}
// Delete
let deleteOne = async (id, dbName) => {
await dbInstances[dbName].deleteOne(id)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// Update
let updateOne = async (id, updatedDoc, dbName) => {
await dbInstances[dbName].archive(id, updatedDoc)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// EFFECTS
useEffect(() => {
console.log('mounting component')
let newDBIs = {}
collections.forEach(col => newDBIs[col] = remote.getGlobal(col))
console.log('db instances settted', newDBIs)
setDbInstances(newDBIs)
}, [])
// When DBs are instantiated, request all docs and set data with response
useEffect(() => {
if (
dbInstances !== null &&
data === null &&
Object.keys(dbInstances).length === collections.length)
{
console.log('setting data')
let newData = {}
collections.forEach(async col => newData[col] = await dbInstances[col].readAll())
console.log('data setted => ', newData)
setData(newData)
}
}, [dbInstances])
return {
data,
create,
getDetails,
deleteOne,
updateOne
};
}
export default useCollections;
In the component where the data returned by the hook is consumed, even though the variable data contains the expected data, these are not rendered.
import WindowsLayout from "../../components/layout/WindowsLayout"
import { useState, useEffect } from "react"
import { remote } from "electron"
import useCollections from "../../hooks/useCollections"
const EditWorkWindow = ({ workId }) => {
let { data, deleteOne, updateOne } = useCollections([
'workDB',
'studioDB',
'rateDB'
])
useEffect(() => {
if (data !== null) console.log(data)
}, [data])
return (
<WindowsLayout title="Edit work window">
<div style={{ height: 243 }} className="window-content">
<div className="padded-more bg-gray-200">
<h2>{JSON.stringify(data)}</h2>
<button onClick={() => console.log(data)}>CLG</button>
</div>
</div>
</WindowsLayout >
)
}
export default EditWorkWindow
The effect hook shows the expected data by console.
<h2>{JSON.stringify(data)}</h2> = {}
When the button is clicked, the expected data is displayed on the console.
I can't understand why if data contains properties, they are not shown in {JSON.stringify(data)}
This is what is shown by the console after clicking on the button
console.log(data) image
And this is example data and their its properties
{
"workDB":[
{
"product":"Work name 1",
"amounts_rates":[
{
"rate":"EflcQflqu2oWWVk2",
"amount":6
},
{
"rate":"FeMIX00pwpmZwoVW",
"amount":1
}
],
"date":"2020-08-31",
"studio":"BCvPeWzMiS8fZsmS",
"_id":"2ZvHMWFODBHYWEBo",
"createdAt":"2020-08-31T09:39:21.077Z",
"updatedAt":"2020-08-31T09:39:21.077Z"
},
{
"product":"Work name 2",
"amounts_rates":[
],
"date":"2020-09-02",
"director":"",
"_id":"PRpp1OQcJnkFKeR0",
"createdAt":"2020-09-01T19:56:33.201Z",
"updatedAt":"2020-09-01T19:56:33.201Z"
}
],
"studioDB":[
{
"name":"Studio name 1",
"_id":"0J1AVXtgDjwBjRS9",
"createdAt":"2020-08-25T10:18:40.004Z",
"updatedAt":"2020-08-25T10:18:40.004Z"
},
{
"name":"Studio name 2",
"_id":"8sFH7gncaM6V7lHh",
"createdAt":"2020-08-25T10:19:45.232Z",
"updatedAt":"2020-08-25T10:19:45.232Z"
}
],
"rateDB":[
{
"name":"Rate name 1",
"value":4.1,
"_id":"EflcQflqu2oWWVk2",
"createdAt":"2020-08-25T10:24:17.357Z",
"updatedAt":"2020-08-25T10:24:17.357Z"
},
{
"name":"Rate name 1",
"value":34,
"_id":"FeMIX00pwpmZwoVW",
"createdAt":"2020-08-25T10:24:25.628Z",
"updatedAt":"2020-08-25T10:24:25.628Z"
}
]
}
Async problem it is.
// When DBs are instantiated, request all docs and set data with response
useEffect(() => {
if (
dbInstances !== null &&
data === null &&
Object.keys(dbInstances).length === collections.length)
{
console.log('setting data')
let newData = {}
collections.forEach(async col => newData[col] = await dbInstances[col].readAll())
console.log('data setted => ', newData)
setData(newData) // <-- 👋LOOK HERE
}
}, [dbInstances])
So you let newData = {} empty object, and send it off to trigger an update re-render by calling setData(), but newData is empty at the moment of calling.
In your rendering function JSON.stringify(data) pickup the data, but it's still empty at the moment of rendering!
It's only when async col => newData[col] = await someValue call is resolved, that your newData's properties will get assigned new values, the newData object stays the same. But by the time it's resolved, the rendering is done already.
Solution: wait till async function call is resolved, then you call setData()
useEffect(() => {
// ...
const promises = collections.map(async col => {
newData[col] = await dbInstances[col].readAll())
})
Promise.all(promises).then(() => { setData(newData) })
})
The reason why you see the updated value when inspecting in console, is because you didn't inspect "quick enough". By the time you mouse-click to expand the object in console, its properties are already assigned values. But if you change
console.log('data setted => ', newData)
// to
console.log('data setted => ', JSON.stringify(newData))
you'll see an empty object.

React: Input Field Seems to Be Returning Itself

I'm still learning React, and am following a code-along. I have a form that uses semantic ui react components, after struggling with this problem for a day or so, I just copy and pasted from their GitHub, and am still having the same problem:
onSearchChange = (e, data) => {
clearTimeout(this.timer);
this.setState({
query: data
});
this.timer = setTimeout(this.fetchOptions, 1000);
};
onChange = (e, data) => {
this.setState({ query: data.value });
this.props.onBookSelect(this.state.books[data.value]);
};
render() {
return (
<Form>
<Dropdown
search
fluid
placeholder="Search for a book by title"
value={this.state.query}
onSearchChange={this.onSearchChange}
options={this.state.options}
loading={this.state.loading}
onChange={this.onChange}
/>
</Form>
);
}
}
The console is throwing me a: Warning: Failed prop type: Invalid prop value supplied to Dropdown. It turned out that my value is being passed as an Object that contains my Dropdown component.
As I was writing this question, I realized that I'm in v16 and they were in 15. So, the answer might be as simple as updating the syntax for v16, but I'm not sure how to do that.
Edit:
I installed React 15.x, and that did fix the problem I was having, so I guess my real question now is why doesn't onSearchChange fire fetchOptions when I change data to data.value. fetchOptions is just an axios call that looks like:
fetchOptions = () => {
if (!this.state.query) return;
this.setState({ loading: true });
axios
.get(`/api/books/search?q=${this.state.query}`)
.then(res => res.data.books)
.then(books => {
const options = [];
const booksHash = {};
books.forEach(book => {
booksHash[book.goodreadsId] = book;
options.push({
key: book.goodreadsId,
value: book.goodreadsId,
text: book.title
});
});
this.setState({ loading: false, options, books: booksHash });
});
};
Doing the exact same tutorial (Rem Zolotykh's Build Real Web App with React series) and ran into the same problem with the newer version of React.
Found the issue and how to fix it:
Change the onSearchChange function to use data.searchQuery instead of data or data.value:
onSearchChange = (e, data) => {
clearTimeout(this.timer);
this.setState({
query: data.searchQuery // <-- Right here
});
this.timer = setTimeout(this.fetchOptions, 1000);
};
That makes it work. (Somewhat unexpectedly, leave it as data.value in the onChange function)
Should be an easy fix, all you need to do is update onSearchChange:
onSearchChange = (e, data) => {
clearTimeout(this.timer);
this.setState({
query: data.value
});
this.timer = setTimeout(this.fetchOptions, 1000);
};
You were saving the argument data to this.state.query. You actually need to store data.value to get the string value. Take a look at how onSearchChange works.
onSearchChange(event: SyntheticEvent, data: object)
event: React's original SyntheticEvent.
data: All props, includes current value of searchQuery.

Resources