I have three components; Form, Preview & AppStore. Clicking a button in Form adds an item to the store. This seems to work fine except that the list in the Preview component isn't updating/re-rendering when the store changes even though it has an #observer decorator. What am I missing?
Form has a button and handler function that adds an item to the store:
#inject('AppStore')
#observer class Form extends React.Component{
handleAddItem= (item) =>{
const {AppStore} = this.props;
AppStore.addItem(item);
console.log(AppStore.current.items)
}
render(){
return(
<button onClick={() => this.handleAddItem('Another Item')}>Add Item</button>
)}
}
Preview maps through the items (I'm using a drag and drop hoc so my code might look a bit odd)
#inject('AppStore')
#observer class Preview extends React.Component
...
return(
<ul>
{items.map((value, index) => (
<SortableItem key={`item-${index}`} index={index} value={value} />
))}
</ul>)
...
return <SortableList items={AppStore.current.items} onSortEnd={this.onSortEnd} />;
Here is store:
import { observable, action, computed } from "mobx";
class AppStore {
#observable other = {name: '', desc:'', items: [ 'item 1', 'item 2', 'item 3'], id:''}
#observable current = {name: '', desc:'', items: [ 'item 1', 'item 2', 'item 3'], id:''}
#action addItem = (item) => {
this.current.items.push(item)
}
}
const store = new AppStore();
export default store;
I'm fairly certain this is a case where MobX doesn't extend observability all the way down into your current.items array.
Objects in MobX will extend observability when first constructed/initialized -- so current.items is an observable property in the sense that if you changed it's value to some other primitive, your component would re-render.
For example:
current.items = 1; // changing it from your array to some totally new value
current.items = []; // this _might_ also work because it's a new array
Similarly, if AppStore had a top-level observable items that you were changing, then calling items.push() would also work.
class AppStore {
#observable items = [];
#action additem = (item) => {
this.items.push(item);
}
}
The problem in your case is that items is buried one level deep inside an observable object -- so pushing items into the current.items array isn't changing the value of the property in a way that MobX can detect.
It's admittedly very confusing, and the common MobX pitfalls are sometimes hard to understand.
See also this line in the Object documentation:
Only plain objects will be made observable. For non-plain objects it
is considered the responsibility of the constructor to initialize the
observable properties.
Try to replace your action to be:
#action addItem = (item) => {
this.current.items = this.current.items.concat([item]);
}
Here instead of using push for mutating the property, use concat which is used to merge two arrays and return a whole new array with a new reference that MobX can react to.
Related
So let's say i have a todoStore. It has an action that deletes a todo by id. Note that i tried both filter and splice:
export default class TodosStore {
constructor() {
makeAutoObservable(this)
}
todos = [
{
id: 1,
name: "name1",
completed: true
},
{
id: 15,
name: "name2",
completed: true
},
{
id: 14,
name: "name3",
completed: true
}
]
removeTodo(id) {
// this.todos = this.todos.filter(todo=>todo.id != id)
for (let todo of this.todos) {
if (todo.id == id) {
const indexOf = this.todos.indexOf(todo)
this.todos.splice(indexOf, 1)
}
}
}
};
The consuming Todos component(Note that i'm wrapping the Todo with observer):
import { combinedStores } from "."
const ObservableTodo = observer(Todo);
export default observer(() => {
const { todosStore } = combinedStores
return (
<div >
{todosStore.todos.map(todo=>{
return(
<ObservableTodo onDelete={()=>{todosStore.removeTodo(todo.id)}} onNameChange={(value)=>{todosStore.editTodoName(todo.id,value)}} key={todo.id} todo={todo}></ObservableTodo>
)
})}
</div>
)
})
The simple Todo component:
export default ({todo,onNameChange,onDelete}) => {
return (
<div style={{padding:'10px',margin:'10px'}}>
<p>ID: {todo.id}</p>
<input onChange={(e)=>{onNameChange(e.target.value)}} value={todo.name}></input>
<p>Completed: {todo.completed ? 'true' : 'false'} <button onClick={onDelete} className="btn btn-danger">Delete</button></p>
</div>
)
}
Even though i'm clearly mutating(as opposed to constructing a new array) the todos array within the store, Todos component rerenders(i see it via console.logs),
and so does every remaining Todo component.
Is there any way around it? Is there anything wrong with my setup perhaps? I'm using latest Mobx(6) and mobx-react.
Todos component is supposed to rerender because it depends on todos array content (because it map's over it). So when you change todos content by adding or removing some todo - Todos component will rerender because it needs to render new content, new list of todos.
Each single Todo rerenders because you have not wrapped it with observer. It is a good practice to wrap every component which uses some observable state, and Todo is clearly the one that does.
You change the length of the todo array, so the map function kicks in. Then while you are iterating over the elements, you are passing new properties to the ObservableTodo component (onDelete, onChange) this will make the ObservableTodo always rerender.
Even though the component is a Mobx observable, it still follows the "React rules", and when react sees new references in component properties, it will render the component.
I am trying to remove a child component by sending a function and an id and then calling that function
when a button in the child is clicked.
Note: The child is a class component, the parent a functional component
Here is the function defined in the parent:
const removeTable = (tableId) => {
const newArray = tables.filter((el) => (el.id !== tableId)
);
console.log(newArray);
setTables(newArray)
}
This is removing elements, but not the one I want. Instead of removing the element with the id I pass it, it keeps that number, starting from 0.
So when I click on the item with a id of 3, it only keeps 0-2. In this example it should keep 0-2 and 4-6. (The array is supposed to be 7 elements long, but somehow it is shortened (before the filter))
What I've tried and Discovered:
I was completely lost, so I decided to create a mock function without using the child:
const removeTabletest = () => {
const key = 1;
const testArr = [{id:1,op:"adsad"}, {id:2,op:"adsad"},{id:3,op:"adsad"} ];
const fml = testArr.filter( (el) => (el.id !== key));
console.log(fml)
}
This function works as I expect.
Finally I stumbled on the fact that when I console.log(tables) at the beginning of the function, I am not getting the same data as in React Dev Tools. The tables array is not the full array I expect.
But when I create another button that is called by the parent (instead of the child), then tables is logged correctly:
Any idea what is going on? or how else I can achieve this?
This sounds like a difficult approach to use with React. Generally what you want to do is just filter your array in the render method based on props or state. If you need a child element to modify what you filter by, like a button, you would pass a callback to the child. Here is an example:
class Table extends React.Component {
constructor(props) {
super(props);
this.state = { hiddenItems: [] };
}
hideItem = item => {
// to hide a row of the table we add it to the list
// of hidden items.
this.setState({ hiddenItems: [...this.state.hiddenItems, item] });
}
render() {
// Create the table element by filtering out hidden items
const table = this.props.items
.filter(item => !this.state.hiddenItems.includes(item))
.map(item => {
return (
<div key={item.id}>{item.contents}</div>
);
});
return (
<>
{table}
// the onClick function could also be passed to a
// child React object
<button onClick={() => this.hideItem(this.props.item[0])}>
Hide item 0
</button>
</>
);
}
}
Where this.props.items would look something like:
[{ id: 0, contents: 'blah blah'},
{ id: 1, contents: (<span>blah</span>)}]
Of course you can also have a function unhiding an item but hopefully this shows the general approach.
I'm trying to create a react app that adds a react component when pressing a button a then re-renders it. I'm using an array of p elements inside state as a test. The event handler function uses setState to modify the array but for some reason it does not re-renders de component so the change (the new element) is not showed in the screen. What I'm doing wrong?
import React, {Component} from "react";
export default class Agenda extends React.Component{
constructor(props){
super(props);
this.list = [];
this.state = {
list:[]
};
this.addItem = this.addItem.bind(this);
const items = ["Hola Mundo 1","Hola Mundo 2","Hola Mundo 3"];
const itemComponent = items.map((item) =>
<p>{item}</p>
);
this.list = itemComponent;
this.state.list = itemComponent;
}
addItem(){
//Adds a new paragraph element at the end of the array
this.list[3]= <p>Hola Mundo 4</p>;
this.setState(
{
list: this.list
}
);
}
render(){
return(
<div id={this.props.id} className={this.props.className}>
{this.state.list}
<button onClick={this.addItem}>
Add item
</button>
</div>
);
}
}
There are several things you are doing which are bad practice in react. One or more of them are likely causing your problem. The issues are:
1) Don't save components in state. Your state should be just the minimum data that determines the components, and the components themselves show up in render. By storing components in state you make it easy to forget to update the state, and thus cause the rendering to not change.
2) Don't duplicate state as instance variables. By doing this, you're only forcing yourself to manually keep two pieces of data in sync with eachother. Instead, have a single source of truth, and have everything else derive from that.
3) Don't mutate state. If you want to add items to an array, create a new array which is a shallow copy of the old one, then append to that new array. React relies on state being immutable to tell whether state has changed, so mutations are an easy way to accidentally make rerendering not happen.
export default class Agenda extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ['Hola Mundo 1', 'Hola Mundo 2', 'Hola Mundo 3'],
};
this.addItem = this.addItem.bind(this);
}
addItem() {
this.setState((oldState) => {
const newList = [...oldState.list];
newList.push('Hola Mundo 4');
return { list: newList };
});
}
render() {
return (
<div id={this.props.id} className={this.props.className}>
{this.state.list.map(item =>
<p>{item}</p>
)}
<button onClick={this.addItem}>
Add item
</button>
</div>
);
}
}
this.list[3]= <p>Hola Mundo 4</p>;
The above statement does not change the reference of the list variable. So set state doesn't see a difference while rendering. Instead do something like this -
this.setState({
list: this.list.concat(<p>Hola Mundo 4</p>)
});
This is a simple replication of a problem i encounter in an actual app.
https://jsfiddle.net/zqb7mf61/
Basically, if you clicked on 'Update Todo" button, the text will change from "Clean Room" to "Get Milk". "Clean Room" is a value in the initial State of the reducer. Then in my React Component, I actually try to clone the state and mutate the clone to change the value to "Get Milk" (Line 35/36). Surprisingly, the initial State itself is also mutated even though I try not to mutate it (as seen in line 13 too).
I am wondering why Object.assign does not work for redux.
Here are the codes from the jsFiddle.
REDUX
const initState = {
task: {id: 1, text: 'Clean Room'}
}
// REDUCER
function todoReducer (state = initState, action) {
switch (action.type) {
case 'UPDATE_TODO':
console.log(state)
let newTodo = Object.assign({}, state) // here i'm trying to not make any changes. But i am surpise that state is already mutated.
return newTodo
default:
return state;
}
}
// ACTION CREATORS:
function updateTodo () {
return {type: 'UPDATE_TODO'};
}
// Create Store
var todoStore = Redux.createStore(todoReducer);
REACT COMPONENT
//REACT COMPONENT
class App extends React.Component{
_onSubmit = (e)=> {
e.preventDefault();
let newTodos = Object.assign({}, this.props.todos) // here i clone the redux state so that it will not be mutated, but i am surprise that it is mutated and affected the reducer.
newTodos.task.text = 'Get Milk'
console.log(this.props.todos)
this.props.updateTodo();
}
render(){
return (
<div>
<h3>Todo List:</h3>
<p> {this.props.todos.task.text} </p>
<form onSubmit={this._onSubmit} ref='form'>
<input type='submit' value='Update Todo' />
</form>
</div>
);
}
}
// Map state and dispatch to props
function mapStateToProps (state) {
return {
todos: state
};
}
function mapDispatchToProps (dispatch) {
return Redux.bindActionCreators({
updateTodo: updateTodo
}, dispatch);
}
// CONNECT TO REDUX STORE
var AppContainer = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App);
You use Object.assign in both the reducer as in the component. This function only copies the first level of variables within the object. You will get a new main object, but the references to the objects on the 2nd depth are still the same.
E.g. you just copy the reference to the task object around instead of actually creating a new task object.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone
Apart from that it would be better to not load the whole state into your component and handle actions differently. Lets just solve this for now. You will have to create a new task object in your onSubmit instead of assigning a new text to the object reference. This would look like this:
newTodos.task = Object.assign({}, newTodos.task, {text: 'Get Milk'})
Furthermore to actually update the store, you will have to edit your reducer as you now assign the current state to the new state. This new line would look like this:
let newTodo = Object.assign({}, action.todos)
I ran into an issue with updating part of the state that is a list that's passed on to children of a component.
I pass in a list to a child, but then have trouble to update that list and have the child reflect the new state;
<ItemsClass items={this.state.items1} />
When I change the value of this.state.items1, the component doesn't render with the new value.
this.setState({items1: []}); // this has no effect
However, if I change the already existing array (not replacing it new a new empty one), the component renders as I wish;
this.setState(state => { clearArray(state.items1); return state; });
That means the state updating function isn't pure, which React states it should be.
The HTML;
<div id='app'></div>
The js;
class ItemsClass extends React.Component {
constructor(props){
super(props);
this.state = {items: props.items};
}
render() {
var items = this.state.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
}
function ItemsFunction(props) {
var items = props.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
class App extends React.Component {
constructor(props){
super(props);
var items = [{id:1, text: 'item 1'}, {id: 2, text: 'item 2'}];
this.state = {
items1: items.slice(),
items2: items.slice(),
items3: items.slice()
};
this.clearLists = this.clearLists.bind(this);
}
clearLists() {
// for items1 and items2, clear the lists by assigning new empty arrays (pure).
this.setState({items1: [], items2: []});
// for items3, change the already existing array (non-pure).
this.setState(state => {
while (state.items3.length) {
state.items3.pop();
}
})
}
render() {
return (
<div>
<button onClick={this.clearLists}>Clear all lists</button>
<h2>Items rendered by class, set list to new empty array</h2>
<ItemsClass items={this.state.items1} />
<h2>Items rendered by class, empty the already existing array</h2>
<ItemsClass items={this.state.items3} />
<h2>Items rendered by function</h2>
<ItemsFunction items={this.state.items2} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
Try it out on codepen.
It seems that the ItemsClass doesn't update even though it's created with <ItemsClass items={this.state.items1}/> and this.state.items1 in the parent changes.
Is this the expected behavior? How can I update the state in the ItemsClass child from the parent?
I'm I missing something? This behavior seems quite error prone, since it's easy to assume that the child should follow the new state, the way it was passed in when the child was created.
You're copying the props of ItemsClass into the state when the component gets initialized - you don't reset the state when the props change, so your component's updates don't get displayed. To quote the docs:
Beware of this pattern, as state won't be up-to-date with any props update. Instead of syncing props to state, you often want to lift the state up.
If your component has to do something when the props change, you can use the componentWillReceieveProps lifecycle hook to do so (note that it doesn't get run when the component initially mounts, only on subsequent prop updates).
That said, there's zero reason for you to be duplicating the props here (and honestly there's rarely a good reason to do so in general) - just use the props directly, as you're doing with ItemsFunction, and everything will stay in sync:
class ItemsClass extends React.Component {
render() {
var items = this.props.items.map(it => <div key={it.id}>{it.text}</div>);
return(
<div>{items}</div>
);
}
}
Here's a working version of your Codepen: http://codepen.io/anon/pen/JNzBPV