How to observe object.property changes on an observable array with mobx - reactjs

I'm using reactjs and mobx. I have an observable array of Item objects, and I'm trying to display them and show property changes "live" by observing the properties on the objects in the array. The changes are not triggered by a click event on any of the components, but by a response to an API call.
I understand that property changes on objects within the array will not trigger the entire list to re-render (which is good), but I can't get it to re-render the single Item component that should be observing the properties of the Item object.
I have tried a couple methods of getting the Item objects within the array to be observable, but none of these are working for me:
calling 'extendObservable() from the Item's constructor
assigning the props.item to a class member decorated with '#observable'
calling the observable constructor and passing in the item object like this: const item = observable(item)
passing the 'hasUnreadData' field as a separate prop and making that observable via 'observable.box(item.hasUnreadData).
Here's some simplified example code (in typescript):
class Item {
id : string
hasUnreadData : boolean
constructor (data: any) {
this.id = data.id;
// false by default
this.hasUnreadData = data.hasUnreadData;
}
}
#observable items: Item[];
// observes the array and re-renders when items are added/removed (this works)
#observer
class ItemListComponent extends React.Component {
render() {
return (
<List> {
items.map((item: Item, index) => {
<ItemComponent key={item.id} itemModel={item} />
}
}
)
}
}
// should observe the 'hasUnreadData' flag and apply different styles when it re-renders (but this does not work, it only displays the initial state)
#observer
class ItemComponent extends React.Component {
render() {
const item = this.props.item;
return (
<ListItem button divider selected={item.hasUnreadData} />
)
}
}
// imagine this is a promise from an API call
API.fetchData().then((itemId: string) => {
itemToUpdate = items.find(i => i.id === itemId);
itemToUpdate.hasUnreadData = true;
// this does not trigger the ItemComponent to render again as expected.
});
Do I need to clone or otherwise "re-create" the Item object to trigger render? Or am I making come kind of obvious mistake here? Any help appreciated.

Related

Avoid looping through map if single item change

I have a List component as shown bellow. Component renders list of Items and listens for item changes using websocket (updateItems function). Everything works fine except that I noticed that when a single item change my renderItems function loops through all of items.
Sometimes I have more than 150 items with 30 updates in a second. When this happens my application noticeable slows down (150x30=4500 loops) and when another updateItems happens after, its still processing first updateItems. I implemented shouldComponentUpdate in Items component where I compare nextProps.item with this.props.item to avoid unnecessary render calls for items that are not changed. Render function is not called but looks like that just call to items.map((item, index) slowing down everything.
My question is, is there a way to avoid looping through all items and change only the one that updated?
Note that other object data are not changed in this case, only items array within object.
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
object: null, // containing items array with some other data
// such as objectId, ...
};
}
componentDidMount() {
// call to server to retrieve object (response)
this.setState({object: response})
}
renderItems= (items) => {
return items.map((item, index) => {
return (
<Item key={item.id} item={item}/>
);
});
}
// this is called as a websocket onmessage callback
// data contains change item that should be replaced in items array
updateItems = data => {
// cloning object here in order to avoid mutation of its state
// the object does not contains functions and null values and cloning
// this way works in my case
let cloneObject = JSON.parse(JSON.stringify(this.state.object));
let index = // call to a function to get index needed
cloneObject.items[index] = data.change;
this.setState({object: cloneObject});
}
render() {
return (
this.state.object && {this.renderItems(this.state.object.items)}
);
}
}
First I would verify that your Item components are not re-rendering with a console.log(). I realize you have written that they don't in your description but I'm unconvinced the map loop is the total cause of the issue. It would be great if you posted your Component code because I'm curious if your render method is expensive for some reason as well.
The method you are currently using to clone your last state is a deep clone, it's not only slow but it will also cause each shallow prop compare to resolve true every time. (ie: lastProps !== newProps will always resolve true when using JSON.parse/stringify method)
To keep each item's data instance you can do something like this in your state update:
const index = state.items.findIndex(item => item._id === newItem._id);
const items = [
...state.items.slice(0, index),
newItem,
...state.items.slice(index + 1),
];
Doing this keeps all the other items intact, except for the one being updated.
Finally as per your question how to prevent this list re-rendering, this is possible.
I would do this by using moving the data storage out of state and into two redux reducers. Use one array reducer to track the _id of each item and an object reducer to track the actual item data.
Array structure:
['itemID', 'itemID'...]
Object structure:
{
[itemID]: {itemData},
[itemID]: {itemData},
...
}
Use the _id array to render the items, this will only re-render when the array of _ids is changed.
class List() {
...
render() {
return this.props.itemIds.map(_id => <Item id={_id} />);
}
}
Then use another container or better yet useSelector to have each item fetch its data from the state and re-render when it's data is changed.
function Item(props) {
const {id} = props;
const data = useSelector(state => state.items[id]);
...
}
You can try wrapping the child component with React.memo(). I had a similar problem with a huge form (over 50 controlled inputs). Every time I would've typed in an input all the form would've get re-rendered.
const Item = memo(
({ handleChange, value }) => {
return (
<>
<input name={el} onChange={handleChange} defaultValue={value} />
</>
);
},
(prevProps, nextProps) => {
return nextProps.values === prevProps.values;
}
Also, if you're passing through props a handler function as I did above, it's worth mentioning that you should wrap it inside a useCallback() hook to prevent recreation if the arguments to the function did not changed. Something like this:
const handleChange = useCallback(e => {
const { name, value } = e.target;
setValues(prevProps => {
const newProps = { ...prevProps, [name]: value };
return newProps;
});
}, []);
For your scenario I would recommend don't use state for your array rather create state for every individual element and update that accordingly. Something like this
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
individualObject: {}
};
}
object = response; // your data
renderItems= (items) => {
this.setState({
individualObject: {...this.state.individualObject, ...{[item.id]: item}
})
return items.map((item, index) => {
return (
<Item key={item.id} item={item}/>
);
});
}
updateItems = data => {
let cloneObject = {...this.object}
let index = // call to a function to get index needed
cloneObject.items[index] = data.change;
this.setState({
individualObject: {...this.state.individualObject, ...{[item.index]: item}
})
}
render() {
return (
this.renderItems(this.object)
);
}
}

How to manage a variable length array in React state

I may be thinking about this the wrong way, so I appreciate any redirection on my design.
I have a React component which gets a list in props. The list can be from 0 to n in length. I want to to manage an attribute of each list item in the state of the React component. (example below) My gut tells me I'm doing something wrong, since I'm trying to set the state's value via props.
Is there a proper way to accomplish what I'm trying here?
class MyList extends React.Component {
state = {
listItems: {}
}
render(){
return(
{this.renderListItems(this.props.list)}
)
renderListItems = list => {
return list.map( listItem => {
let id = listItem.id
return <ListItem key={id} listItem={listItem} color={this.state.listItems[id].color} />
}
}

How to change the id only for the newly inserted object without affecting the object with the same initial id that's already inside the array?

I'm trying to implement react-beautiful-dnd in my app and I'm running into an issue when I try to drag the component of the same type into the droppable area more than once.
In the state I'm importing an array of objects that contain properties of all the available dragable components. I also have an array of usedComponents which represents the components dragged into the droppable area.
import components from "../schemas/components";
class Editor extends React.Component {
constructor(props) {
super(props);
this.state = {
components: components, //array of all available components
usedComponents: [
components[0],
components[2],
components[7],
components[8],
components[9],
components[10],
components[11],
components[12] //these objects are set initially
],
selectedComponent: null,
};
}
}
component objects look something like this:
{
id: 1,
group: "header",
componentType: "headerLogo",
componentName: "Header 01",
//...rest of the component properties
}
the portion of the onDragEnd handler related to adding new component that gives me trouble looks like this:
onDragEnd(result) {
const { destination, source } = result;
if (source.droppableId !== destination.droppableId) {
//add component
this.setState(prevState => {
const newComponent = prevState.components.filter(
item => item.group === source.droppableId
)[source.index];
let usedComponents = prevState.usedComponents;
usedComponents.splice(destination.index + 1, 0, newComponent);
return {
...prevState,
usedComponents: usedComponents
};
});
}
}
So, where the trouble begins is when usedComponents array already contains a component with an id say 5, and I try to drag another component with the same id into the droppable area. I have another handler that I use for selecting a component on click which is based around the component id and every time there are two or more components with the same id inside the usedComponents array, clicking on any of them selects them all, which is not a desired behavior. Each component should be selected individually, no matter if it's of the same type or not.
To remedy this I tried to change the id of the object set in const newComponent before it is inserted into the .splice() method, but no matter how I try do it, it changes both the id of the newly inserted component as well as the component of the same type (same initial id) already contained in usedComponents array. How do I change the id only for the newly inserted component without affecting the component with the same initial id already contained in usedComponents?
From what I can tell from your description, it sounds like you were doing something like
newComponent = prevState.components.filter(
item => item.group === source.droppableId
)[source.index];
newComponent.id = newId;
(where newId is whatever you want the new id to be), and then trying to insert that back into the array. Instead try using spread syntax:
oldComponent = prevState.components.filter(
item => item.group === source.droppableId
)[source.index];
newComponent = {
...oldComponent,
id: newId
};
After that you should have a new component instead of a reference to the old component to insert into your array.

How can I replace component's state with react?

I have a local state of currentMenu in component of MenuItemContainer
export default class MenuItemsContainer extends React.Component {
constructor () {
super();
this.state = {
currentMenu: [],
};
}
I render menu_items by using function_renderMenuItem like below,
_renderMenuItems(menuitems) {
const { order } = this.props;
return menuitems.map((menuitem) => {
if (menuitem.category_id == this.props.order.currentCategoryId) {
this.state.currentMenu.push(menuitem)
else {
return false;
}
return <MenuItem
key={menuitem.id}
order={order}
dispatch={this.props.dispatch}
channel={this.props.order.channel}
{...menuitem} />;
});
}
What I want to do with currentMenu is that storing menuItems which category_id of menuItem equals to currentCategoryId of order state.
Now I am using push(menuitem) to push those items to the state. However, in currentMenu, it should store only if when category_id of menuItem is equal to currentCategoryId of orders state. So when there are changes of currentCategoryId, it should reset currentMenu and get new menuItem
How can I do this?
Thanks in advance
In order to actually trigger the state change you need to do this via setState
so you can add a setState command at the end of your _renderMenuItems method like so:
this.setState({currentMenu: state.currentMenu})
But the better practice is to have the state already containing the right items and not filter it in the render method.
Im not sure from your example how you are getting the menuItems, but in theory you should build the state from them and then call the _renderMenuItems method (that will use the state)

Change items in list in React when an item has been clicked

I'm quite new to ReactJS and I have trouble understand how different components can communicate with each other.
I do have a component that will render a list and each list item is a different component. I want to keep the components as small as possible.
Now, each list item can have a property named active and if the property is set to true, an additional class is added.
This is the class that defines a single item in the component.
See this below code for my component defining a single list item:
export default class OfficeRibbonTab extends React.Component {
constructor(props) {
super(props);
this.state = {
active: props.active ? props.active : false
}
// Assign all the correct event handlers.
this.setActive = this.setActive.bind(this);
}
setActive() {
this.setState({active: true});
}
render() {
// When the tab is defined as active, add the "active" class on it.
if (this.state.active)
{ var tabClassName = "active"; }
return <li onClick={this.setActive} className={tabClassName}>{this.props.tabName}</li>;
}
}
So, I have propery active which is passed to this component, which I store in the components state.
When I click the list item, I set to state of the current item to be active.
The problem is that I want all the other list items to become inactive, thus setting the state of active to false.
The code below is an overview of my list:
export default class OfficeRibbon extends React.Component {
constructor(props) {
// Call the 'base' constructor.
super(props);
}
render() {
var tabs = [];
// Loop over all the tab elements and define how the element should be rendered.
for (var i = 0; i < this.props.dataSource.tabs.length; i ++)
{
if (i == 1)
{ tabs.push(<OfficeRibbonTab active={true} key={this.props.dataSource.tabs[i].name} tabName={this.props.dataSource.tabs[i].name}></OfficeRibbonTab>); }
else
{ tabs.push(<OfficeRibbonTab key={this.props.dataSource.tabs[i].name} tabName={this.props.dataSource.tabs[i].name}></OfficeRibbonTab>); }
}
return (<div>
<div className="wrap-top">
<OfficeRibbonTitle title={this.props.title}/>
<ul className="tabs">
{tabs}
</ul>
</div>
</div>);
}
}
It doesn't seem like rocket science, but I want to do it the React way without re-inventing the wheel.
Anyone who can guide me in the right direction?
Kind regards
It looks like OfficeRibbonTab manages its own state, which is fine, but it never informs its parent component of the state change. The most common approach would be to supply a callback function to each child component, so that it can then communicate back to the parent:
For example, OfficeRibbon will now contain a function handleTabSelect that gets passed down as a prop to each OfficeRibbonTab. And in OfficeRibbonTab, when a tab is clicked, you simply invoke the callback, and pass in the selected tab's index or id:
OfficeRibbonTab.jsx
setActive(tabIndex) {
this.props.handleTabSelect(tabIndex);
}
OfficeRibbon.jsx
handleTabSelect(tabIndex) {
this.setState({activeIndex: tabIndex});
}
Now in OfficeRibbon, you update your state to set the activeIndex or activeId, again either by the index or an identifier of your choosing. By setting state in OfficeRibbon, we necessarily force a render() of its children. As a result, we simply match the index of the iterator with the activeIndex of your state, when we iterate in render():
<OfficeRibbonTab active={index === this.state.activeIndex} onClick={this.handleTabSelect} ... />

Resources