Using React Virtualized CellMeasurer with changing data order for List and dynamic heights - react-virtualized

I am trying to create a chat application using React Virtualized. So far everything is working but I think think I am doing something wrong when using keyMapper and rowRenderer from the list.
The cache is using the id to store the height in the _rowHeightCache but it seems that the heights are looked up using the index and not the id. Im not sure if I should be passing the id as the rowIndex to CellMeasurer to get the right heights or something of that nature.
The issue is that the heights are wrong due to the changing order of the messages list so the index's don't have the proper heights and also that messages can be removed messing up the index orders. This I think should be fixed by using keyMapper to look up the heights but i must be doing it wrong.
The following is an edited down version of how I am using it.
constructor(props) {
this._cache = new CellMeasurerCache({
defaultHeight: 45,
fixedWidth: true,
keyMapper: (index) => _.get(this.props.messageList[index], ['msgId'])
});
}
render() {
const props = this.filterProps(this.props),
list = this.props.messageList,
rowCount = list.length;
return (
<InfiniteLoader
threshold={0}
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={rowCount}>
{({onRowsRendered, registerChild}) => (
<AutoSizer>
{({width, height}) => (
<List
ref={(ref) => {
this.streamLane = ref;
registerChild(ref);
}}
height={height}
width={width}
onRowsRendered={onRowsRendered}
rowCount={rowCount}
rowRenderer={this._rowRenderer}
deferredMeasurementCache={this._cache}
rowHeight={this._cache.rowHeight}
/>
)}
</AutoSizer>
)}
</InfiniteLoader>
);
}
_rowRenderer({ index, key, parent, style }) {
return (
<CellMeasurer
cache={this._cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}>
<div style={{...style}}>
{this.props.messageList[index]}
</div>
</CellMeasurer>
);
}
_loadMoreRows({startIndex, stopIndex}) {
return new Promise((resolve) => {
if (!!stopIndex || !!startIndex || this.props.hasLastMessage || this.state.isPinned) {
return resolve();
}
this.props.loadOlder(() => resolve);
});
}
_isRowLoaded({index}) {
if (index === 0) return false;
return !!this.props.messageList[index];
}
}
Any help, suggestions or criticisms would be amazing.

The issue is that the heights are wrong due to the changing order of the messages list so the index's don't have the proper heights and also that messages can be removed messing up the index orders.
Your example code doesn't show how the order changes or what you do after it changes, so it's hard to say what you're doing wrong.
But when the order changes (eg you sort the data, or you add new items to the beginning of the list causing older items to shift) then you'll need to let List know that its cached positions may be stale. Do do this in your case, you'd need to call this.streamLane.recomputeRowHeights().
(You can pass the index of the first changed item to recomputeRowHeights but I assume in your case that maybe all items have shifted? I don't know, since I don't have all of your code.)
After you call recomputeRowHeights, List will re-calculate the layout using the heights reported to it by CellMeasurerCache. That cache uses your keyMapper function:
rowHeight = ({index}: IndexParam) => {
const key = this._keyMapper(index, 0);
return this._rowHeightCache.hasOwnProperty(key)
? this._rowHeightCache[key]
: this._defaultHeight;
};
So it won't need to remeasure items, even though their indices have changed.

Related

React-query mutation dosent refresh

I have another fetch of "SpecificList in another component, but when posts excutes it dosent refetch it only refetches on rewindowfocus? What could the reason be?
const { mutate,isLoading:isAdding } = useMutation((values)=>handleAddValue(values),
{
onSuccess:()=> queryClient.invalidateQueries("specificList")
},
)
async function handleAddValue(values){
return await ShoppingListAPI.post("/addToShoppingList",{
product:values.newValue,
list:values.listId
})
}
if(isAdding){ return <p>Loading...</p> }
if (isLoading) { return <p>Loading...</p> }
return (
<Box>
{data.products &&
<Autocomplete onChange={(event, newValue) => mutate({newValue,listId})
} options={data.products} getOptionLabel={(option) => option.name} renderInput={(params) => <TextField {...params} label="Products" />}></Autocomplete>
}
</Box>
)
What could the reason be?
hard to tell without seeing the whole code / a codesandbox reproduction. Some things to keep an eye on are:
matching query keys (upper case / lower case)
make sure the queryClient is stable: either created outside of your App component, or inside it on a state or ref instance. Otherwise, a re-render will create a new client, throwing away the cache.
maybe your mutation is not Successful, so onSuccess is not called.

Dynamic Table updates "too late" ReactJS

my problem is, that I have a table which should update everytime when the user chooses something from a dropdown component. The problem now is that my table updates "too late" in the frontend. So when the user chooses an option for the first time nothing will happen. Then when the user chooses an option for the second time from the dropdown component, the table will show the data from the option he has picked before. If the user chooses an option for the 3rd time, the table will show the data from the second one and so on.
So how can I fix this? I work with ReactJS and Semantic UI
My Code:
This renders the Row for the existing data
renderTableData() {
return this.state.songs.map((song, index) => {
const { id, nr, songname, link } = song
return (
<Table.Row key={id}>
<Table.Cell>{nr}</Table.Cell>
<Table.Cell>{songname}</Table.Cell>
<Table.Cell>{link}</Table.Cell>
</Table.Row>
)
})
}
The Code in the main render() function of React (Its shown correctly, expect that the data is "outdated":
`<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell width={1}>Nr</Table.HeaderCell>
<Table.HeaderCell width={2}>Songname</Table.HeaderCell>
<Table.HeaderCell width={1}>Link</Table.HeaderCell>
</Table.Row>
</Table.Header>
{this.renderTableData()}
</Table>`
The code when the option from the dropdown gets changed:
onChangeDropdown(e) {
this.setState({game: e.target.textContent}, ()=>{
this.state.songs.length = 0;
for(var i = 0; i< this.state.musicData.length;i++){
if(this.state.musicData[i].game == this.state.game){
for(var j = 0; j<this.state.musicData[i].songs.length;j++){
this.state.songs.push({id: j+1, nr: j+1, songname: this.state.musicData[i].songs[j].name, link: this.state.musicData[i].songs[j].link})
}
break;
}
}
this.renderTableData()
})
}
The game variable in this.setState is correct and also the for-loop works as expected when the user changes the dropdown option, I already checked it with the debugger
I hope you can help me out there, ty
is not that is updating too late, is that you are mutating the state without using setState so React doesn't know what changed, you should refactor your code to always use setState to update the state, not push, something like this:
onChangeDropdown(e) {
this.setState((currentState) => {
const newSongs = [];
const game = e.target.textContent;
musicData.forEach((data) => {
if (data.game === game) {
musicData.songs.forEach((song, index) => {
newSongs.push({
id: index + 1,
nr: index + 1,
songname: song.name,
link: song.link,
});
});
}
});
return {
...currentState,
game,
songs: newSongs,
};
});
}
I changed your for loops to use forEach, less complexity, easier to read
Here is what I did:
create a empty array to store the selected songs (newSongs)
loop all the music data and then loop all the songs inside each item in music data
add the songs from the selected game into newSongs
return newSongs + game to update the selected game, ...currentState is to preserve the other parts of the state between changes
So every time the dropodown changes, I create a new array and run the logic
The setState callback can return an object to replace whole state, so before that you can do any calculation you need to.
Updating the state in React is asyncronous, that's one of the reasons you can't mutate the state directly and need to use setState any time you need to update it

Update prop for dynamically inserted element

New to react... Really banging my head against it with this one... I'm trying to figure out how to get a dynamically inserted component to update when the props are changed. I've assigned it to a parent state object but it doesn't seem to re-render. I've read that this is what's supposed to happen.
I was using ReactDOM.unmountComponentAtNode to re-render the specific elements I needed to, but it kept yelling at me with red text.
I need to hide "chat.message" unless the user has the authority to see it (server just sends empty string), but I still need to render the fact that it exists, and reveal it should the user get authentication. I'm using a css transition to reveal it, but I really need a good way to update the chat.message prop easily.
renderChats(uuid){
let userState = this.state.userStates.find(user => {
return user.uuid === uuid;
});
const children = userState.chats.map((chat) => {
let ChatReactElement = this.getChatMarkup(chat.cuid, chat.message, chat.status);
return ChatReactElement;
});
ReactDOM.render(children, document.getElementById(`chats-${this.state.guid}-${uuid}`));
}
getChatMarkup() just returns JSX and inserts Props... I feel like state should be getting passed along here. Even when I use a for-loop and insert the state explicitly, it doesn't seem to re-render on changes.
getChatMarkup(cuid, message, status){
return(
<BasicChatComponent
key={cuid}
cuid={cuid}
message={message}
status={status}
/>
);
}
I attempted to insert some code line this:
renderChats(uuid){
let userState = this.state.userStates.find(user => {
return user.uuid === uuid;
});
const children = userState.chats.map((chat) => {
let ChatReactElement = this.getChatMarkup(chat.cuid, chat.message, chat.status);
if(chat.status.hidden)
this.setState({ hiddenChatRE: [ ...this.state.hiddenChatRE, ChatReactElement ] }); // <== save elements
return ChatReactElement;
});
ReactDOM.render(children, document.getElementById(`chats-${this.state.guid}-${uuid}`));
}
and later in my code:
this.state.hiddenChatRE.every(ReactElement => {
if(ReactElement.key == basicChats[chatIndex].cuid){
ReactElement.props = {
... //completely invalid code
}
}
});
The only response I see here is my ReactDOM.unmountComponentAtNode(); approach...
Can anyone point me in the right direction here?
Although perhaps I should be kicking myself, I read up on how React deals with keys on their components. So there's actually a fairly trivial answer here if anyone comes looking... Just call your render function again after you update the state.
In my case, something like:
this.setState(state =>({
...state,
userStates : state.userStates.map((userstate) => {
if(userstate.uuid == basicChats[chatIndex].uuid) return {
...userstate,
chats: userstate.chats.map((chat) => {
if(chat.cuid == basicChats[chatIndex].cuid){
//
return {
cuid: basicChats[chatIndex].cuid,
message: basicChats[chatIndex].message,
status: basicChats[chatIndex].status
}
}
else return chat;
})
}
else return userstate;
})
}));
and then, elsewhere in my example:
this.state.userStates.map((userstate) => {
this.renderChats(userstate.uuid);
});
Other than the fact that I'd recommend using indexed arrays for this example to cut complexity, this is the solution, and works. This is because even though it feels like you'd end up with duplicates (that was my intuition), the uid on the BasicChatComponent itself makes all the difference, letting react know to only re-render those specific elements.

Using a custom filter button to filter a list, works on first click but subsequent clicks don't work as well! SPFX

I have 2 SP lists (A and B).
List A has filter buttons next to each list item. When a user clicks a button it should filter List B, only showing the related items.
List A has an Id column which List B matches it's column (MasterItems) with List A's Id.
Here's the code I'm using:
public _getListItems() {
sp.web.lists.getByTitle("ListA").items.get().then((items: any[]) => {
let returnedItems: IListAItem[] = items.map((item) => { return new ListAItem(item); });
this.setState({
Items: returnedItems,
ListAItems: returnedItems,
});
});
sp.web.lists.getByTitle("ListB").items.get().then((items: any[]) => {
let returnedItems: IListBItem[] = items.map((item) => { return new ListBItem(item); });
this.setState({
ListBItems: returnedItems, //This brings in the items from ListB so they can be filtered on this.state.ListB when clicked
});
});
}
private _editItem = (ev: React.MouseEvent<HTMLElement>) => {
this._getListItems(); //This attempts to reset the list when another filter is clicked, but is half working!
const sid = Number(ev.currentTarget.id);
const sid2 = 'DIBR'+sid;
let _item = this.state.ListBItems.filter((item) => { return item.MasterItem == sid2; });
if (_item && _item.length > 0) {
sp.web.lists.getByTitle("ListB").items.get().then((items: any[]) => {
let returnedItems: IListBItem[] =
items.filter(i => _item.some(other => other.Id === i.Id)).map(
(item) => new ListBItem(item)
);
this.setState({
ListBItems: returnedItems,
});
});
}
}
The problem is that when the button is clicked next to an item, it filters correctly on first click!
but if filtered again on the same or different item it will sometimes unset the filter and mix results, other times it will filter correctly. So I'm suspecting I've made a state problem here, but can't seem to discover why.
Regards,
T
UPDATE: I've added a clear filter button which makes things work, but would like the user to be able to click on filter to filter instead of having to clear it each time.
I am doing the same in my SharePoint list
so basically I always set the clear filter function before the filter function,
for example:
function myFilter(){
//my filter code goes here
}
function clearFilter(){
//the clear filter code goes here
}
lets say you are running the function on an item select or a button click or text input change, set the clear filter to run before the filter.
function funcGroup{
clearFilter();
setTimeout(() => {
myFilter();
}, 300);
}
or
function funcGroup{
setTimeout(() => {
clearFilter();
}, 300);
myFilter();
}
I am using this scenario with my SharePoint lists and its working perfect...

react-virtualized - how do I use this as a true infinite scroller

I can't find any code example or docs that answers this:
Achieve almost complete infinite scroll -> unknown # of items, but there is a finite amount that may be infeasible to compute beforehand - e.g. at some point the list needs to stop scrolling
Can I trigger first load of data from within InfiniteScroller/List - it seems you need to pass in a data source that is populated with initial page
I am using this example:
https://github.com/bvaughn/react-virtualized/blob/master/docs/creatingAnInfiniteLoadingList.md
and:
https://github.com/bvaughn/react-virtualized/blob/master/source/InfiniteLoader/InfiniteLoader.example.js
along with CellMeasurer for dynamic height:
https://github.com/bvaughn/react-virtualized/blob/master/source/CellMeasurer/CellMeasurer.DynamicHeightList.example.js
The docs for InfiniteLoader.rowCount say:
"Number of rows in list; can be arbitrary high number if actual number is unknown."
So how do you indicate there are no more rows.
If anyone can post an example using setTimeout() to simulate dynamic loaded data, thanks. I can likely get CellMeasurer working from there.
Edit
This doesn't work the way react-virtualized creator says it should or the infinite loading example implies.
Calls:
render(): rowCount = 1
_rowRenderer(index = 0)
_isRowLoaded(index = 0)
_loadMoreRows(startIndex = 0, stopIndex = 0)
_rowRenderer(index = 0)
end
Do I need to specify a batch size or some other props?
class HistoryBrowser extends React.Component
{
constructor(props,context,updater)
{
super(props,context,updater);
this.eventEmitter = new EventEmitter();
this.eventEmitter.extend(this);
this.state = {
history: []
};
this._cache = new Infinite.CellMeasurerCache({
fixedWidth: true,
minHeight: 50
});
this._timeoutIdMap = {};
_.bindAll(this,'_isRowLoaded','_loadMoreRows','_rowRenderer');
}
render()
{
let rowCount = this.state.history.length ? (this.state.history.length + 1) : 1;
return <Infinite.InfiniteLoader
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={rowCount}
>
{({ onRowsRendered, registerChild }) =>
<Infinite.AutoSizer disableHeight>
{({ width }) =>
<Infinite.List
ref={registerChild}
deferredMeasurementCache={this._cache}
height={200}
onRowsRendered={onRowsRendered}
rowCount={rowCount}
rowHeight={this._cache.rowHeight}
rowRenderer={this._rowRenderer}
width={width}
/>}
</Infinite.AutoSizer>}
</Infinite.InfiniteLoader>
}
_isRowLoaded({ index }) {
if (index == 0 && !this.state.history.length)
// No data yet, force load
return false;
}
_loadMoreRows({ startIndex, stopIndex }) {
let self = this;
for (let i = startIndex; i <= stopIndex; i++) {
this.state.history[startIndex] = {loading: true};
}
const timeoutId = setTimeout(() => {
delete this._timeoutIdMap[timeoutId];
for (let i = startIndex; i <= stopIndex; i++) {
self.state.history[i] = {loading: false, text: 'Hi ' + i };
}
promiseResolver();
}, 10000);
this._timeoutIdMap[timeoutId] = true;
let promiseResolver;
return new Promise(resolve => {
promiseResolver = resolve;
});
}
_rowRenderer({ index, key, style }) {
let content;
if (index >= this.state.history.length)
return <div>Placeholder</div>
else if (this.state.history[index].loading) {
content = <div>Loading</div>;
} else {
content = (
<div>Loaded</div>
);
}
return (
<Infinite.CellMeasurer
cache={this._cache}
columnIndex={0}
key={key}
rowIndex={index}
>
<div key={key} style={style}>{content}</div>
</Infinite.CellMeasurer>
);
}
}
The recipe you linked to should be a good starting place. The main thing its missing is an implementation of loadNextPage but that varies from app to app based on how your state/data management code works.
Can I trigger first load of data from within InfiniteScroller/List - it seems you need to pass in a data source that is populated with initial page
This is up to you. IMO it generally makes sense to just fetch the first "page" of records without waiting for InfiniteLoader to ask for them- because you know you'll need them. That being said, if you give InfiniteLoader a rowCount of 1 and then return false from isRowLoaded it should request the first page of records. There are tests confirming this behavior in the react-virtualized GitHub.
The docs for InfiniteLoader.rowCount say: "Number of rows in list; can be arbitrary high number if actual number is unknown."
So how do you indicate there are no more rows.
You stop adding +1 to the rowCount, like the markdown file you linked to mentions:
// If there are more items to be loaded then add an extra row to hold a
loading indicator.
const rowCount = hasNextPage
? list.size + 1
: list.size

Resources