Re-render flat-list in functional component - reactjs

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.

Related

ReactJS, update State with handler but data is empty

I have this situation:
I want to update some state (an array) which is used to map different React components.
Those componentes, have their own handleUpdate.
But when I call to handleUpdate the state that I need to use is empty. I think is because each handler method was mounted before the state was filled with data, but then, how could I ensure or use the data in the handler? In other words, the handler needs to update the state that fill it's own state:
const [data, setData] = useState([]);
const [deliver, setDeliver] = useState({items: []});
const handleUpdate = (value, position) => {
// This set works
setDeliver({
items: newItems
});
// This doesn't work because "data" is an empty array - CRASH
setData(data[position] = value);
};
useEffect(() => {
const dataWithComponent = originalData.map((item, i) => ({
...item,
entregado: <SelectorComponent
value={deliver?.items[i].delivered}
key={i}
onUpdate={(value) => handleUpdate(value, i)}
/>
}));
setData(dataWithComponent); // This is set after <SelectComponent is created...
}
}, [originalData]);
The value that you pass don't come from originalData, so the onUpdated don't know what it's value
You run on originalData using map, so you need to pass item.somthing the the onUpdate function
const dataWithComponent = originalData.map((item, i) => ({
...item,
entregado: <SelectorComponent
value={deliver?.items[i].delivered} // you can't use items use deliver.length > 0 ? [i].delivered : ""
key={i}
onUpdate={() => handleUpdate("here you pass item", i)}
/>
}));
I'm not sure, but I think you can do something like that. I hope you get some idea to work it.
// This doesn't work because "data" is an empty array - CRASH
let tempData = [...data].
tempData[position] = value;
setData(tempData);

Rendering and best practices in React

I am a beginner in React, I have a question regarding rendering, which consists of I use useEfect to render the change in a variable, which is decorated in a useState
Here are the statements:
const CardRepo: React.FC<DadosCardRepo> = ({ Nome }) => {
const [nomeRepo, setNomeRepo] = useState(Nome);
const [data, setData] = useState({});
const [languages, setLanguages] = useState<Languages[]>([]);
This is a component, which receives an object with a Name, and sets this value Name in nameRepo
Here are the two methods:
useEffect(() => {
getRepo(nomeRepo)
.then(data => {
setData(data);
});
}, [nomeRepo]);
useEffect(() => {
getLanguages(data['languages_url'])
.then(data => {
let total = 0;
const languagesRef: Languages[] = [];
Object.keys(data).map((key) => {
total += data[key];
languagesRef.push({ Nome: key, Porct: data[key] });
});
languagesRef.map((language, i, array) => {
setLanguages((oldValues) => [...oldValues, { Nome: language.Nome.toLowerCase(), Porct: +((language.Porct * 100) / total).toFixed(2) }]);
});
});
}, [data]);
Where the first useEfect I run when making a change to nomeRepo, and in this method, I make a request to the getRepo service
And the second one, when I change the date, and in this method, I make a request to the getLanguages ​​service, and do the processing of the response to make a list with it, where I assign languages.
Here is the listing:
{
languages.map((el, i) => {
return (
<div className="progress-bar" id={el.Nome} key={i} style={{ width: el.Porct + '%' }}></div>
);
})
}
My question is related to rendering, I know that I will only be able to list something, if I use useEfect in the variable "languages", where I will be assigning values ​​and with the changes in the variable to render again, otherwise I couldn't.
But when I make any changes to the code, it renders only where it was changed, but it assigns the same values ​​that were assigned, for example: [1,2,3] => [1,2,3,1,2,3].
I tried to put setLanguages ​​([]), but it has the same behavior
I was wondering how to fix this and if it's a good practice to make calls and list that way.
first, there is one bad practice where you create a derived state from props. nomeRepo comes from props Nome, you should avoid this derived states the most of you can. You better remove nomeRepo state and use only Nome.
your first use Effect becomes:
useEffect(() => {
getRepo(Nome)
.then(data => {
setData(data);
});
}, [Nome]);
you second useEffect has a setState triggered inside a map. That will trigger your setState multiple times, creating several rerenders. It's best to prepare your array of languages first, then call setState once.
Also, you should use forEach instead of map if you are not consuming the returned array, and only running some script on each element.
One thing to notice, is you are adding new languages to old ones. I assume that languages should be related data['languages_url'] value, this way you wouldn't include the old languages to the setLanguages.
One thing you could consider is to keep your total values instead of percentages. you can store total into a variable with useMemo, that will depend on languages. And declare a your percentage function into a utils folder and import it as need it.
refactored second useEffect:
// calculate total value that gets memoized
const totalLangCount = useMemo(() => languages.reduce((total, val) => total + val.count , 0), [languages])
useEffect(() => {
getLanguages(data['languages_url'])
.then(data => {
const languagesRef: Languages[] = [];
Object.keys(data).forEach((key) => {
// prepeare your languages properties
const nome = key.toLowerCase();
const count = data[key];
languagesRef.push({ nome, count });
});
// avoid multiple setLanguages calls
setLanguages(languagesRef)
});
}, [data]);
languages rendered:
{
languages.map((el, i) => {
return (
// here we are using array's index as key, but it's best to use some unique key overall instead
<div className="progress-bar" id={el.nome} key={i} style={{ width: percentage(el.count, total) }}></div>
);
})
}
percentage function reusable:
// utils percentage exported
export const percentage = (count, total) => ((count * 100) / total).toFixed(2) + '%'

What's the React best practice for getting data that will be used for page render?

I need to get data that will be used for the page that I'm rendering. I'm currently getting the data in a useEffect hook. I don't think all the data has been loaded before the data is being used in the render. It's giving me an error "property lastName of undefined" when I try to use it in the Chip label.
I'm not sure where or how I should be handling the collection of the data since it's going to be used all throughout the page being rendered. Should I collect the data outside the App function?
const App = (props) => {
const [teams] = useState(["3800", "0200", "0325", "0610", "0750", "0810"]);
const [players, setPlayers] = useState([]);
useEffect(() => {
teams.forEach(teamId => {
axios.defaults.headers.common['Authorization'] = authKey;
axios.get(endPoints.roster + teamId)
.then((response) => {
let teamPlayers = response.data.teamPlayers;
teamPlayers.forEach(newPlayer => {
setPlayers(players => [...players, newPlayer]);
})
})
.catch((error) => {
console.log(error);
})
});
}, []);
let numPlayersNode =
<Chip
variant="outlined"
size="small"
label={players[1].lastName}
/>
return (...
You iterate over a teamPlayers array and add them one at a time, updating state each time, but players is always the same so you don't actually add them to state other than the last newPlayer.
Convert
teamPlayers.forEach(newPlayer => {
setPlayers(players => [...players, newPlayer]);
});
to
setPlayers(prevPlayers => [...prevPlayers, ...teamPlayers]);
Adds all new players to the previous list of players using a functional state update.
You also have an initial state of an empty array ([]), so on the first render you won't have any data to access. You can use a truthy check (or guard pattern) to protect against access ... of undefined... errors.
let numPlayersNode =
players[1] ? <Chip
variant="outlined"
size="small"
label={players[1].lastName}
/> : null
You should always create a null check or loading before rendering stuff. because initially that key does not exists. For example
<Chip
variant="outlined"
size="small"
label={players.length > 0 && players[1].lastName}
/>
this is an example of a null check
For loading create a loading state.
When functional component is rendered first, useEffect is executed only after function is returned.
and then, if the state is changed inside of useEffect1, the component will be rendered again. Here is a example
import React, {useEffect, useState} from 'react'
const A = () => {
const [list, setList] = useState([]);
useEffect(() => {
console.log('useEffect');
setList([{a : 1}, {a : 2}]);
}, []);
return (() => {
console.log('return')
return (
<div>
{list[0]?.a}
</div>
)
})()
}
export default A;
if this component is rendered, what happen on the console?
As you can see, the component is rendered before the state is initialized.
In your case, error is happened because players[1] is undefined at first render.
the simple way to fix error, just add null check or optional chaining like players[1]?.lastName.

ReactJs UseState : insert element into array not updating

I am trying to use React Hooks but somehow my state is not updating. When I click on the checkbox (see in the example), I want the index of the latter to be added to the array selectedItems, and vice versa
My function looks like this:
const [selectedItems, setSelectedItems] = useState([]);
const handleSelectMultiple = index => {
if (selectedItems.includes(index)) {
setSelectedItems(selectedItems.filter(id => id !== index));
} else {
setSelectedItems(selectedItems => [...selectedItems, index]);
}
console.log("selectedItems", selectedItems, "index", index);
};
You can find the console.log result
here
An empty array in the result, can someone explain to me where I missed something ?
Because useState is asynchronous - you wont see an immediate update after calling it.
Try adding a useEffect which uses a dependency array to check when values have been updated.
useEffect(() => {
console.log(selectedItems);
}, [selectedItems])
Actually there isn't a problem with your code. It's just that when you log selectedItems the state isn't updated yet.
If you need selectedItems exactly after you update the state in your function you can do as follow:
const handleSelectMultiple = index => {
let newSelectedItems;
if (selectedItems.includes(index)) {
newSelectedItems = selectedItems.filter(id => id !== index);
} else {
newSelectedItems = [...selectedItems, index];
}
setSelectedItems(newSelectedItems);
console.log("selectedItems", newSelectedItems, "index", index);
};

FlatList not updating with React Hooks and Realm

I am writing a custom hook to use it with realm-js.
export default function useRealmResultsHook<T>(query, args): Array<T> {
const [data, setData] = useState([]);
useEffect(
() => {
function handleChange(newData: Array<T>) {
// This does not update FlatList, but setData([...newData]) does
setData(newData);
}
const dataQuery = args ? query(...args) : query();
dataQuery.addListener(handleChange);
return () => {
dataQuery.removeAllListeners();
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[query, ...args]
);
return data;
}
In my component:
const MyComponent = (props: Props) => {
const data = useRealmResultsHook(getDataByType, [props.type]);
return (
<View>
<Text>{data.length}</Text>
<FlatList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
/>
</View>
);
};
In the previous component, when doing setData(newData), the data.length gets updated correctly inside the Text. However, the FlatList does not re-render, like the data did not change.
I used a HOC before and a render prop with same the behavior and it was working as expected. Am I doing something wrong? I'd like to avoid cloning data setData([...newData]); because that can be a big amount of it.
Edit 1
Repo to reproduce it
https://github.com/ferrannp/realm-react-native-hooks-stackoverflow
The initial data variable and the newData arg in the handler are the links to the same collection. So they are equal and setData(newData) won’t trigger component’s re-render in this case.
It might be helpful to map Realm collection to the array of items’ ids. So, you will always have the new array in the React state and render will occur properly. It's also useful to check only deletions and insertions of the collection to avoid extra re-renders of the list. But in this case, you should also add listeners to the items.
function useRealmResultsHook(collection) {
const [data, setData] = useState([]);
useEffect(
() => {
function handleChange(newCollection, changes) {
if (changes.insertions.length > 0 || changes.deletions.length > 0) {
setData(newCollection.map(item => item.id));
}
}
collection.addListener(handleChange);
return () => collection.removeListener(handleChange);
},
[]
);
return data;
}

Resources