Problem: shouldComponentUpdate retrieves previous state with this.state, that doesn't work if you keep reference to array at UserList, and update array entity at UserStore.
PureRenderMixin.js
const deepEqual = require('deep-equal');
module.exports = function pureRenderMixin(Component) {
Component.prototype.shouldComponentUpdate = function(nextProps, nextState) {
return !deepEqual(this.props, nextProps) || !deepEqual(this.state, nextState);
};
return Component;
};
UserList.react.js
class UserList extends React.Component {
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
}
componentWillMount() {
UsersStore.addChangeListener(this._onChange);
}
_onChange() {
this.setState({userList: UsersStore.getState()});
}
}
module.exports = PureRenderMixin(UserList);
UsersStore.js
......
getState() { return _userList; }
switch(action.type) {
case ActionTypes.UPDATE_USER_FLAG:
//!!!!!!!!!!!!!!
//PROBLEM: since UserList.react keep userList reference, there is no way to retrieve previous state inside shouldComponentUpdate
_userList[action.index].flag = action.flag;
UsersStore.emitChange();
break;
}
#taggon solution
thanks to taggon, now I know how to make shouldComponentUpdate keep the reference to previous state:
UsersStore.js
......
getState() { return _userList; }
switch(action.type) {
case ActionTypes.UPDATE_USER_FLAG:
//SOLUTION: copy an array, so there will be two versions of _userList[action.index]
_userList = _.map(_userList, _.clone);
_userList[action.index].flag = action.flag;
UsersStore.emitChange();
break;
}
I think the problem is in the store.
The store would be better to create another array whenever its state is changed.
For example, think the store as an array:
var store = [ ];
export default store;
You may want to write add() function like this:
export function add(newItem) {
store = [ ...store, newItem ];
// or write this in es5
store = store.concat([newItem]);
// trigger change event or dispatch an action here
}
Likewise, remove() function can be:
export remove(index) {
store = [ ...store.slice(0, index), ...store.slice(index+1) ];
// trigger change event or dispatch an action here
}
In this way, the store dereference the component's state whenever the store is changed. This makesshouldComponentUpdate() return true.
I hope this helps you.
if your props are immutable, you can compare the data by reference safely and easily. you can have a look at immutablejs
class ProductStore extends ReduceStore {
getInitialState() {
return Immutable.OrderedMap({1: new Product('react', 'flux'), 2: new Product('angular', 'mvc')});
}
reduce (state, action) {
switch (action.type) {
case 'product/item-selected':
return state.map((product)=> {
return product.set('selected', product.id === action.id);
});
case 'product/search':
let alldata = this.getInitialState();
return alldata.filter((product)=> {
return product.name.indexOf(action.value) !== -1;
});
default:
return state;
}
}
}
export default class ProductDetail extends Component {
shouldComponentUpdate(nextProps) {
return this.props.product !== nextProps.product;
}
render() {
const {product} = this.props;
return (
<div className="product-detail">
<div className="product-name">
{product.name}
</div>
<div className="product-type">
{product.type}
</div>
</div>
);
}
}
https://facebook.github.io/immutable-js/
Related
Im trying to make an api request from redux then take that data and put it in my react state (arrdata). The api call works but i cant seem to get the state on my app.js to update based on the redux api call. Am i missing something?
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
arrdata: []
};
}
componentDidMount() {
this.props.loadData();
console.log(this.props.data);
}
render() {
const {arrdata} = this.state
return ( ......)}}
const mapStateToProps = state => {
return {
data: state.data
};
};
export default connect(mapStateToProps, dataAction)(App);
Action
export function loadData() {
return dispatch => {
return axios.get("https://api.coincap.io/v2/assets").then(response => {
dispatch(getData(response.data.data.slice(0, 10)));
});
};
}
export function getData(data) {
return {
type: "GET_DATA",
data: data
};
}
Reducer
let initialState = {
data: []
};
const mainReducer = (state = initialState, action) => {
if (action.type === "GET_DATA") {
return {
...state,
data: action.data
};
} else {
return {
...state
};
}
};
export default mainReducer;
I think you are misleading store with state. Your arrdata is empty since it's stored inside state, but your data comes from props.
Anyways, arrdata in state remains empty, since you are not setting the state anywhere. To do that, you would have to use e.g. getDerivedStateFromProps lifecycle hook, however I wouldn't recommend that.
render() {
const { data } = this.props;
console.log(this.props.data);
return (
// do something with your data
);
}
It should log your data properly.
Note: You don't need state, actually. It's a better approach to manipulate over props, instead of saving data from props into state (in most cases).
I'm implementing a Task App where I have two views to render Tasks and Items and one where I render multiple lists based on the Task Status as kanban board.
My reducer:
export const rootReducer = Redux.combineReducers({
Tasks: TasksReducer,
itemsAreLoading: itemsAreLoadingReducer
});
const TasksReducer = (state , action ) => {
if (typeof state == "undefined") {
console.log('state undefined');
return null;
}
switch (action.type) {
case TasksTypes.Tasks_GET:
return action.Tasks;
default:
console.log(state);
return state;
}
}
export class TasksApp extends React.Component {
constructor(props) {
super(props);
}
render() {
const {tasks} = this.props;
return (<div>
<ItemsView Tasks={tasks}/>
<BoardView Lanes=[/* tasks tranfromed into mutliple list based on their status*/]/>
</div>);
}
}
const mapStateToProps = (state) => {
return {
tasks: state.Tasks
};
};
My Question is where to transform the data for the second view to have a different representation of the data.
The main problem here is that you dont fire any actions in your class, and I dont see any actions here neither. So first, you have to fire an action, and dispatch it with the type and payload, second, as David Tyron wrote, the syntax was a bit off in this line:
const { tasks } = this.props;
And for the end a small remark, you can do some destruction in the mapStateToProps function:
const mapStateToProps = ({ Tasks }) => {
return { Tasks };
};
And then get it like const { Tasks } = this.props;
I think, that the best practice to change your tasks props is to fire another action that creates a new props from your tasks props, something like:
export const transformData = tasks => {
return dispatch => {
//Do the transformations here
dispatch {
type: TRANSFORM_DATA,
payload: transformed_tasks
}
}
}
And then catch it with a reducer.
And IMHO, the best place to call this action is the componentDidMount()
The component is not rendering on state update, even the mapStateToProps is executing successfully.
Component:
import React from 'react';
import AddItem from './addItemComponent';
import { connect } from 'react-redux';
import RenderItem from './RenderItem';
class HomeComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div className="container">
<AddItem />
</div>
<div className="container">
{this.props.Items.length}
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
Items: state.auth.items
}
}
export default connect(mapStateToProps)(HomeComponent);
Reducers:
const ItemsReducers = (state = {}, Action) => {
if (!state.items)
state.items = [];
console.log("inside the reducers", state);
var newState = Object.assign({}, state);
switch (Action.type) {
case 'AddItem':
newState.items.push(Action.payload);
return newState;
break;
default:
return newState;
break;
}
return newState;
}
export default ItemsReducers;
To avoid mutation, I just clone the object and doing push operation. But still facing issue. It would be more helpful if I got any help.
Object.assign() make a shallow copy of your object, not a deep copy. So, you have a mutation !
In your example, mapStateToProps is executing because state has changed, but items keep the same reference. So your old items is equal to the new items, even if the length of array has changed. And mapStateToProps do not rerender the component if the output hasn't changed.
You need to copy your state and items like this:
var newState = Object.assign({}, state);
newStats.items = newState.items.slice();
But it's not a good way to use redux.
I recommend to use immutability-helper package with update(). It create a copy of your object without change the original source
import update from 'immutability-helper';
const reducer = (state = {}, action) => {
switch (action.type) {
case: 'addItem':
return !state.items
? update(state, {items: {$set: [action.payload]}})
: update(state, {items: {$push: [action.payload]}})
default:
return state;
}
}
I have a component, that doesn't change data after updated store
class RenderComments extends Component {
commentsParse() {
return this.props.comments.map((comment) => {
if (comment.hasComments === true) {
return (
<div key={comment.id}>
<CommentItem data={comment} />
</div>
) // return
}
}); // this.props.comments.map()
} // commentParse
render() {
return (
<div>
{ this.commentsParse() }
</div>
) // return
} // render
} // RenderComments
function mapStateToProps(state) {
return {
comments: state
}
}
export default connect(mapStateToProps)(RenderComments);
dispatcher
class AddComment extends Component {
constructor(props) {
super(props);
this.state = {
comment: ''
}
}
addNewComment() {
this.props.addNewComment(this.state);
}
render() {
return (
<form>
<div className="comment-entry">
<div className="form-group">
<textarea
onChange={event => this.setState({comment: event.target.value})}
>
</textarea>
</div>
<div className="comment-entry-footer">
<button
onClick={() => this.addNewComment()}
>
Submit
</button>
</div>
</div>
</form>
) // return
} // render
} // AddComment
function mapStateToProps(state) {
return {
comment: state
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({addNewComment}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(AddComment);
this is my reducer
import { ADD_COMMENT } from './../constants';
let allUserComments = [/* Array with 4 objects */]
const createNewComment = (action) => {
let d = new Date();
let n = d.toLocaleTimeString();
let newComment = {
id: Math.random(),
rating: 0,
name: 'MyNick',
thumbnail: 'icon.50x50.png',
time: n,
comment: action.text.comment,
replyTo: null,
replyToUser: null,
hasComments: false
}
allUserComments.push(newComment);
return allUserComments;
}
export default (state = allUserComments, action) => {
switch (action.type) {
case ADD_COMMENT:
allUserComments = createNewComment(action);
return allUserComments;
default:
return state;
}
}
In first launch application, my component shows 4 objects i.e state that return reducer, but after action, component doesn't re-render, it's continuing shows 4 object instead 5. Why component doesn't update?
Your reducer is very wrong, on several levels. It's not following the requirement of updating data immutably.
First, you shouldn't be maintaining a separate allUserComments array outside the reducer function.
Second, you shouldn't be using .push() to mutate an array - you would want to use .concat() to return a new array that also contains the added item.
Third, you're also generating a new ID value in a reducer.
None of those follow the principle of "pure functions" and immutable updates in reducers. Since you're directly mutating state and keeping the same array reference, that will almost definitely keep your components from updating properly.
Please see the Redux FAQ entry on mutations keeping components from rendering for more info, as well as the Structuring Reducers - Immutable Update Patterns section.
mapStatToProps is callings it's self twice and loads data on the second call but then doesn't assign it to components as a prop and returns an empty array.
The Reducer:
export default function postsReducer(state = initialState.posts, action) {
switch (action.type) {
case types.LOAD_POSTS_SUCCESS:
return action.posts;
default:
return state;
}
}
Here is my mpStateToProps func:
function mapStateToProps(state, ownProps) {
const singlePost = state.posts.filter(post => post.code == ownProps.params.id);
return {
post: singlePost
};
}
Here is my Component's state:
this.state = {
post: [...props.post]
};
Your code is probably right, but it seems like you forgot to update your state.post value when your component receives new props.
componentWillReceiveProps(nextProps) {
this.setState({
post: nextProps.post
});
}