cannot access the data inside an array - arrays

I'm having trouble accessing data inside my state which I fetch from my database,
this is my state:
state = {
inputDataArr: [],
inputDataArrTest: [
{
formID: 'test',
inputArr: [1, 2, 3],
labelArr: [4, 5, 6]
}
]
};
this is the collection I'm importing from the database:
{
"_id": {
"$oid": "5ba96b8ebffd923734090df4"
},
"inputArr": [
"value1",
"value2",
"value3"
],
"labelArr": [
"label1",
"label2",
"label3"
],
"formID": "5ba96b83bffd923734090df0",
"__v": 0
}
which is an object with 2 arrays in it,
this is how I fetch it :
componentWillMount() {
fetch('/api/datas')
.then(data => data.json())
.then(datas => {
const filterdDatas = datas.filter(
data => data.formID == this.props.match.params.id
);
this.setState(currentState => ({
inputDataArr: [...currentState.inputDataArr, ...filterdDatas]
}));
});
}
now when I console log inputDataArr and inputDataArrTest I get an array with an object inside,
exactly the same,
but when I console log InputDataArrTest[0] which is what you can see in my state I can see the arrays and the formID,
but when I console log inputDataArr[0] I get undefined, really frustrating, anybody knows why?

Mapping and doing data manipulation inside the render is never a good idea and isn't easy to debug.
My suggestion is to initialize the state to an empty array, and call a method to return the mapped data.
constructor(props) {
super(props);
this.state = {
inputDataArr: []
};
}
render() {
const data = this.state.inputDataArr.map(...)
return <div>{data}</div>
}
This way state is initiated before first render and data will return an empty array.
After componentDidMount the state will update and will re-render with the new data.
As a side note, i would do the mapping in a third then and set the state to the result itself.
Happy Coding.

I'm not sure where are you running the console.log but setState has a callback method that you can run after the actual data has been updated:
this.setState(currentState => ({
inputDataArr: [...currentState.inputDataArr, ...filterdDatas]
}), () => {
// this is the callback for setState, you can access the updated data
console.log(inputDataArr);
});
Note If you run the console.log in render keep in mind that the first render call will always run before the async data has been fetched.
Note #2 Do not use componentWillMount at all and especially not for fetching data.
Use componentDidMount instead.

Have you test filteredDatas before using ...filteredDatas ? Maybe it is not an Array and result to other thing or an empty Array.
Are you sure this.props.match.params.id is defined and equal to the formID property?
If it exists only one object with unique formID why don’t you use Array#find instead of Array.filter and then update your state with
this.setState(prevState => ({
dataArr: filteredDatas ? prevState.dataArr.concat([filteredDatas]) : prevState.dataArr // Here with Array#find, filteredDatas is null or a object not an Array
});

renderLabels() {
const { inputDataArr } = this.state;
return (
!!inputDataArr.length &&
inputDataArr[0].labelArr.map((label, index) => (
<th key={index}>{label}</th>
))
);
}
conditional rendering solved my problem

Related

How to return a value from Firebase to a react component? [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed last year.
I am trying to read a value from RealtimeDatabase on Firebase and render it in an element, but it keeps returning undefined. I have the following code:
const getStudentName = (studentId) => {
firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
return snapshot.val().name;
})
}
const StudentName = (studentId) => ( <p>{getStudentName(studentId)}</p> )
I know it's nothing wrong with the database itself or the value I'm finding, because if I do:
const getStudentName = (studentId) => {
firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
console.log(snapshot.val().name);
return "Test";
})
}
I still see a correct name from my database outputted to console as expected, yet "Test" is not returned to the element. However, if I do it like this:
const getStudentName = (studentId) => {
firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
console.log(snapshot.val().name);
})
return "Test";
}
then "Test" is returned to the element and displayed. I'm very confused, as I don't understand how my console.log() can be reached inside the function but a 'return' statement right after it will not return.
New to React and Firebase, please help! Thank you.
EDIT: I'm sure it's self-explanatory, but you can assume a simple database in the form:
{ "students": [
"0": { "name": "David" },
"1": { "name": "Sally" } ]}
If 'studentId' is 0 then 'console.log(snapshot.val().name)' successfully outputs 'David', but 'David' will not return to the element.
You can't return something from an asynchronous call like that. If you check in the debugger or add some logging, you'll see that your outer return "Test" runs before the console.log(snapshot.val().name) is ever called.
Instead in React you'll want to use a useState hook (or setState method) to tell React about the new value, so that it can then rerender the UI.
I recommend reading the React documentation on the using the state hook, and the documentation on setState.
I'm not sure where you are consuming getStudentName, but your current code makes attaches a real-time listener to that database location. Each time the data at that location updates, your callback function gets invoked. Because of that, returning a value from such a function doesn't make much sense.
If you instead meant to fetch the name from the database just once, you can use the once() method, which returns a Promise containing the value you are looking for.
As another small optimization, if you only need the student's name, consider fetching /students/{studentId}/name instead.
const getStudentName = (studentId) => {
return firebase.database()
.ref("students")
.child(studentId)
.child("name")
.once("value")
.then(nameSnapshot => nameSnapshot.val());
}
With the above code, getStudentName(studentId) now returns a Promise<string | null>, where null would be returned when that student doesn't exist.
getStudentName(studentId)
.then(studentName => { /* ... do something ... */ })
.catch(err => { /* ... handle errors ... */ })
If instead you were filling a <Student> component, continuing to use the on snapshot listener may be the better choice:
const Student = (props) => {
const [studentInfo, setStudentInfo] = useState({ status: "loading", data: null, error: null });
useEffect(() => {
// build reference
const studentDataRef = firebase.database()
.ref("students")
.child(props.studentId)
.child("name");
// attach listener
const listener = studentDataRef.on(
'value',
(snapshot) => {
setStudentInfo({
status: "ready",
data: snapshot.val(),
error: null
});
},
(error) => {
setStudentInfo({
status: "error",
data: null,
error
});
}
);
// detach listener in unsubscribe callback
return () => studentDataRef.off(listener);
}, [props.studentId]); // <- run above code whenever props.studentId changes
// handle the different states while the data is loading
switch (studentInfo.status) {
case "loading":
return null; // hides component, could also show a placeholder/spinner
case "error":
return (
<div class="error">
Failed to retrieve data: {studentInfo.error.message}
</div>
);
}
// render data using studentInfo.data
return (
<div id={"student-" + props.studentId}>
<img src={studentInfo.data.image} />
<span>{studentInfo.data.name}</span>
</div>
);
}
Because of how often you might end up using that above useState/useEffect combo, you could rewrite it into your own useDatabaseData hook.

Issue with state update approach for nested objects

Major EDIT
I have quite huge object which is 3 level deep. I use it as a template to generate components on the page and to store the values which later are utilized, eg:
obj =
{
"group": {
"subgroup1": {
"value": {
"type": "c",
"values": []
},
"fields_information": {
"component_type": "table",
"table_headers": [
"label",
"size"
],
}
},
"subgroup2": {
"value": {
"type": "c",
"values": []
},
"fields_information": {
"component_type": "table",
"table_headers": [
"label",
"size"
],
}
},
},
}
Thanks to this I can dynamically generate view which is, as a template, stored in DB.
I'm struggling with 2 things. Firstly, updating values basing on user input for textbox, checkboxes and similar.
I'm doing it this way:
const updateObj = (group, subgroup, value) => {
let tempObj = {...obj}
tempObj[group][subgroup].value.value = value
toggleObj(tempObj)
}
I know that the spread operator is not in fact doing deep copy. However it allows me to work on the object and save it later. Is that an issue? Do I have to cloneDeep or it is just fine? Could cloneDeep impact performance?
Second case is described below
export const ObjectContext = React.createContext({
obj: {},
toggleObj: () => {},
});
export const Parent = (props) => {
const [obj, toggleObj] = useState()
const value = {obj, toggleObj}
return (
<FormCreator />
)
}
const FormCreator = ({ catalog }) => {
const {obj, toggleObj} = React.useContext(ObjectContext)
return (<>
{Object.keys(obj).map((sectionName, sectionIdx) => {
const objFieldsInformation = sectionContent[keyName].fields_information
const objValue = sectionContent[keyName].value
...
if (objFieldsInformation.component_type === 'table') {
return (
<CustomTable
key={keyName + "id"}
label={objFieldsInformation.label}
headers={objFieldsInformation.table_headers}
suggestedValues={[{label: "", size: ""}, {label: "", size: ""}, {label: "", size: ""}]}
values={objValue.values}
sectionName={sectionName}
keyName={keyName}/>
)
}
...
})}
</>)
}
const CustomTable= (props) => {
const { label = "", headers = [], suggestedValues = [], values, readOnly = false, sectionName, keyName } = props
const {obj, toggleObj} = React.useContext(ObjectContext)
//this one WORKS
useEffect(() => {
if (obj[sectionName][keyName].value.type === "complex") {
let temp = {...obj}
temp[sectionName][keyName].value.values = [...suggestedValues]
toggleObj(temp)
}
}, [])
//this one DOES NOT
useEffect(() => {
if (obj[sectionName][keyName].value.type === "c") {
let temp = {...obj, [sectionName]: {...obj[sectionName], [keyName]: {...obj[sectionName][keyName], value: {...obj[sectionName][keyName].value, values: [{label: "", size: ""}, {label: "", size: ""}, {label: "", size: ""}]}}}}
toggleObj(temp)
}
}, [])
return (
//draw the array
)
}
Please refer to CustomTable component.
As on the example Object above, I have 2 CustomTables to be printed. Unfortunately, one useEffect that should work is not working properly. I'm observing, that values field is set only for the last "table" in Obj. When I'm doing shallow copy of obj, it works fine. But I'm afraid of any repercussion that might happens in future.
I'm also totally new to using createContext and maybe somehow it is the issue.
Kudos to anyone understanding that chaos :)
The main issue appears to be that you are not providing your context. What you have is literally passing the blank object and void returning function. Hence why calling it has no actual effect, but mutating the value does.
export const ObjectContext = React.createContext({
obj: {},
toggleObj: () => {},
});
export const Parent = (props) => {
const [obj, toggleObj] = useState({})
const value = {obj, toggleObj}
return (
<ObjectContext.Provider value={value}>
<FormCreator />
</ObjectContext.Provider>
)
}
Ideally you would also make this component above wrap around FormCreator and render it as props.children instead. This is to prevent the entire sub-tree being rerendered every time toggleObj is called. See the first part of this tutorial to get an idea of the typical pattern.
As to the question about mutating state, it absolutely is important to keep state immutable in React - at least, if you are using useState or some kind of reducer. Bugs arising from state mutation come up all the time on Stack Overflow, so often in fact that I recently made a codesandbox which demonstrates some of the more common ones.
I also agree with #SamuliHakoniemi that a deeply nested object like this is actually better suited to the useReducer hook, and might even go one further and suggest that a proper state management library like Redux is needed here. It will allow you to subdivide reducers to target the fragments of state which actually update, which will help with the performance cost of deeply cloning state structure if or when it becomes an actual issue.

Re-render flat-list in functional component

I have an empty array that I pass to my flat-list.
I also use useEffect to fetch data from the server and update the list.
However, after setting the new state of the array with the data, the flat-list is not re-rendered.
const [listData, setlistData] = React.useState<Transaction[]>([])
const [dataUpdated, setDataUpdated] = React.useState<boolean>(false)
React.useEffect(() => {
if(route.params.showAllData)
{
fetchTransactions(1, TRANSACTION_PAGE_SIZE, 1)
.then((res: Transaction) => {
console.log(`TransactionsScreen: userEffect [] => fetched ${JSON.stringify(res)}`);
setlistData(prevState => ({...prevState, ...res}));
setDataUpdated(true)
})
.catch(err => {
//TODO: handle error scenerio
ToastAndroid.show(`Failed to fetch transacrions`, ToastAndroid.SHORT)
})
}}, [])
<FlatList
data={listData}
renderItem={item => _renderItem(item)}
ItemSeparatorComponent={TransactionListSeparator}
extraData={dataUpdated} // extraData={listData <--- didn't work either}
keyExtractor={item => item.id.toString()}/>
I tried to add the data array as extraData value but it didn't work, I also tried to add another boolean notifying that the data was updated but it didn't work either.
How can re-render the flat-list correctly?
You can force flatlist to rerender by passing the updated list as an extraData prop, i.e extraData={listData}. However, when using functional components a common mistake is passing the same instance of the list data as the prop. This will not trigger a rerender even if the content in the list or the length of the list has changed. FlatList sees this as the same and will not rerender.
To trigger a rerender you have to create an entirely new instance of the list.
Egs:
//This update will NOT trigger a rerender
const copy = listData;
copy.push(something)
setListData(copy)
////////
<FlatList extraData={listData}
.....
/>
//This WILL trigger a rerender
const copy = [...listData]
copy.push(something)
setListData(copy)
////////
<FlatList extraData={listData}
.....
/>
I solved the problem this way :
useEffect(() => {
setDataUpdated(!dataUpdated);
}, [listData]);
NOTE :
Your updating the dataUpdated to true directly. I do not recommend this, do it this way instead : setDataUpdated(!dataUpdated)
Plus ensure that all elements of the FlatList have a unique key, if not it will not re-render at any cost
Try this, make sure you are returning your child component and giving dependency array with useEffect upon which chnage you want to re-render your component
const [listData, setlistData] = React.useState<Transaction[]>([])
const [dataUpdated, setDataUpdated] = React.useState<boolean>(false)
React.useEffect(() => {
if(route.params.showAllData)
{
fetchTransactions(1, TRANSACTION_PAGE_SIZE, 1)
.then((res: Transaction) => {
console.log(`TransactionsScreen: userEffect [] => fetched ${JSON.stringify(res)}`);
setlistData(prevState => ({...prevState, ...res}));
setDataUpdated(true)
})
.catch(err => {
//TODO: handle error scenerio
ToastAndroid.show(`Failed to fetch transacrions`, ToastAndroid.SHORT)
})
}}, [listData])
return (
<FlatList
data={listData}
renderItem={item => _renderItem(item)}
ItemSeparatorComponent={TransactionListSeparator}
extraData={dataUpdated} // extraData={listData <--- didn't work either}
keyExtractor={item => item.id.toString()}/>
)
You can't spread the array as you did. For example, if you have two arrays.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
And spread them like:
const arr3 = { ...arr1, ...arr2 };
The result will be:
{ 0: 4, 1: 5, 2: 6 }
Change curly braces to the square.
setlistData(prevState => ([...prevState, ...res]));
Since your FlatList expects data to be an array, not an object.

own sortFunction for dataTable

I get an error when calling this.props.fetch() in the mySort method. this.props.mySort collects new data from the backend sorted by the respective column. The error is: Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
this.props.fetch is now called in an infinite loop.
How can I fix this?
Best regards,
Joachim
constructor(props) {
super(props)
let columns = [
{ field: "foo", header: "bar", sortable: true, sortFunction: this.mySort }
]
this.state = {
cols: columns
}
this.colOptions = []
for (let col of this.state.cols) {
this.colOptions.push({ label: ` ${col.header}`, value: col })
}
}
mySort = (e) => {
if (e.order === 1) {
console.log("1")
this.props.fetch({...)}
} else {
...}
}
render() {
let columnData = this.state.cols.map((col, i) => {
return <Column className="columnheader" style={col.style} key={col.field} field={col.field}
header={col.header} sortable={col.sortable} sortFunction={col.sortFunction} body={col.body} expander={col.expander} />
})
return(
<DataTable value={fetchResults} >
{columnData}
</DataTable>
)
}
You shouldn't be doing a fetch from inside a render, and the way it is, seems it's being called when every single column is rendered causing multiple state updates resulting in an infinite loop
Instead, move the fetch to componentDidMount. And whenever the sort field is changed, handle the change event and sort the data client side and reset your data, so that the whole table is already sorted and can be rendered at once. If the data is huge and needs the server to sort it for whatever reasons, again handle the refetch in the change event, and set the entire sorted data into state at one go.
Something like,
componentDidMount(){
// fetch the data
}
handleSortFieldChange(){
/* sort client side and -> setState()
or fetch again and setState()*/
}
I changed the sortFunction this way:
mySort = (e) => {
if (e.order === 1 && e.field !== this.state.sortField) {
this.setState({
sortField: e.field
},
this.props.fetchSearchResults({
...
}))
} else if (e.order === -1 && e.field !== this.state.sortField) {
this.setState({
sortField: e.field
},
this.props.fetchSearchResults({
...
}))
}
}
Now, there is no infinite loop but the error stays the same.

React Redux, how to properly handle changing object in array?

I have a React Redux app which gets data from my server and displays that data.
I am displaying the data in my parent container with something like:
render(){
var dataList = this.props.data.map( (data)=> <CustomComponent key={data.id}> data.name </CustomComponent>)
return (
<div>
{dataList}
</div>
)
}
When I interact with my app, sometimes, I need to update a specific CustomComponent.
Since each CustomComponent has an id I send that to my server with some data about what the user chose. (ie it's a form)
The server responds with the updated object for that id.
And in my redux module, I iterate through my current data state and find the object whose id's
export function receiveNewData(id){
return (dispatch, getState) => {
const currentData = getState().data
for (var i=0; i < currentData.length; i++){
if (currentData[i] === id) {
const updatedDataObject = Object.assign({},currentData[i], {newParam:"blahBlah"})
allUpdatedData = [
...currentData.slice(0,i),
updatedDataObject,
...currentData.slice(i+1)
]
dispatch(updateData(allUpdatedData))
break
}
}
}
}
const updateData = createAction("UPDATE_DATA")
createAction comes from redux-actions which basically creates an object of {type, payload}. (It standardizes action creators)
Anyways, from this example you can see that each time I have a change I constantly iterate through my entire array to identify which object is changing.
This seems inefficient to me considering I already have the id of that object.
I'm wondering if there is a better way to handle this for React / Redux? Any suggestions?
Your action creator is doing too much. It's taking on work that belongs in the reducer. All your action creator need do is announce what to change, not how to change it. e.g.
export function updateData(id, data) {
return {
type: 'UPDATE_DATA',
id: id,
data: data
};
}
Now move all that logic into the reducer. e.g.
case 'UPDATE_DATA':
const index = state.items.findIndex((item) => item.id === action.id);
return Object.assign({}, state, {
items: [
...state.items.slice(0, index),
Object.assign({}, state.items[index], action.data),
...state.items.slice(index + 1)
]
});
If you're worried about the O(n) call of Array#findIndex, then consider re-indexing your data with normalizr (or something similar). However only do this if you're experiencing performance problems; it shouldn't be necessary with small data sets.
Why not using an object indexed by id? You'll then only have to access the property of your object using it.
const data = { 1: { id: 1, name: 'one' }, 2: { id: 2, name: 'two' } }
Then your render will look like this:
render () {
return (
<div>
{Object.keys(this.props.data).forEach(key => {
const data = this.props.data[key]
return <CustomComponent key={data.id}>{data.name}</CustomComponent>
})}
</div>
)
}
And your receive data action, I updated a bit:
export function receiveNewData (id) {
return (dispatch, getState) => {
const currentData = getState().data
dispatch(updateData({
...currentData,
[id]: {
...currentData[id],
{ newParam: 'blahBlah' }
}
}))
}
}
Though I agree with David that a lot of the action logic should be moved to your reducer handler.

Resources