What is the purpose of using Object.assign in this redux reducer? - reactjs

I know that state should be immutable and this is a no-no, mutating state with push,
//action = Object {type: "CREATE_COURSE", course: {title: algebra}}
export default function courseReducer(state = [], action) {
switch(action.type) {
case 'CREATE_COURSE':
state.push(action.course)
return state;
default:
return state;
}
}
Pluralsight recommends this:
export default function courseReducer(state = [], action) {
switch(action.type) {
case 'CREATE_COURSE':
return [...state,
Object.assign({}, action.course)
];
default:
return state;
}
}
but what is wrong with not using object.assign? What is wrong with this, seems like the app still works. state is still not being mutated, a new array is being returned.
export default function courseReducer(state = [], action) {
switch(action.type) {
case 'CREATE_COURSE':
return [...state,
action.course
];
default:
return state;
}
}
CourseActions:
export function createCourse(course) {
return { type: 'CREATE_COURSE', course};
}
CoursesPage Component:
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as courseActions from '../../actions/courseActions';
class CoursesPage extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
course: { title: '' }
};
this.onTitleChange = this.onTitleChange.bind(this);
this.onClickSave = this.onClickSave.bind(this);
}
onTitleChange(event) {
const course = this.state.course; // assign this.state.course to course
course.title = event.target.value; // reassign course.title to whatever was in input
this.setState({course: course}); // reassign state course to new course object with updated title
}
onClickSave() {
this.props.actions.createCourse(this.state.course);
}
courseRow(course, index) {
return <div key={index}>{course.title}</div>;
}
render() {
return (
<div>
<h1>Courses</h1>
{this.props.courses.map(this.courseRow)}
<h2>Add Course</h2>
<input
type="text"
onChange={this.onTitleChange}
value={this.state.course.title} />
<input
type="submit"
value="Save"
onClick={this.onClickSave} />
</div>
);
}
}
CoursesPage.propTypes = {
courses: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
function mapStateToProps(state, ownProps) {
return {
courses: state.courses
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(courseActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(CoursesPage);

If you don't use Object.assign, last element of new array will be a reference to action.course and whatever that is a reference to. Passing a reference may work fine, but if something mutates it somewhere down the line and it causes problems - they will be hard to debug. Better safe than sorry.

if you just put the reference to action.course in the array, all of your components that use this.props.courses will see all the course objects in the array share the same reference, which shouldn't be an issue up to the point where any of the component intentionally/unintentionally mutate any of them even temporarily.

FYI: Object.assign is also more performant than the spread operator.

Related

dispatching actions won't trigger render when using combinedReducers

When I don't use combineReducers:
const store = createStore<StoreState,any,any,any>(pointReducer, {
points: 1,
languageName: 'Points',
});
function tick() {
store.dispatch(gameTick());
requestAnimationFrame(tick)
}
tick();
everything works and my component updates. However when I do:
const reducers = combineReducers({pointReducer}) as any;
const store = createStore<StoreState,any,any,any>(reducers, {
points: 1,
languageName: 'Points',
});
The store does update (checked by console logging) however the component doesn't render the change and I have no idea why!
The reducer:
export function pointReducer(state: StoreState, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return { ...state, points: state.points + 1 };
case DECREMENT_ENTHUSIASM:
return { ...state, points: Math.max(1, state.points - 1) };
case GAME_TICK:
return { ...state, points: state.points + 1 };
default:
return state;
}
}
and component:
export interface Props {
name: string;
points: number;
onIncrement: () => void;
onDecrement: () => void;
}
class Points extends React.Component<Props, object> {
constructor(props: Props) {
super(props);
}
render() {
const { name, points, onIncrement, onDecrement } = this.props;
return (
<div className="hello">
<div className="greeting">
Hello {name + points}
</div>
<button onClick={onDecrement}>-</button>
<button onClick={onIncrement}>+</button>
</div>
);
}
}
export default Points;
The container:
export function mapStateToProps({ points, languageName }: StoreState) {
return {
points: points,
name: languageName,
}
}
export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
return {
onIncrement: () => dispatch(actions.incrementEnthusiasm()),
onDecrement: () => dispatch(actions.decrementEnthusiasm())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Points);
Store state:
export interface StoreState {
languageName: string;
points: number;
food: number;
wood: number;
}
When making the suggested changes (changing the reducer and combinereducers I get a new error:
my reducer now looks like:
export function pointReducer(state: 1, action: EnthusiasmAction) {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return state + 1;
case DECREMENT_ENTHUSIASM:
return Math.max(1, state - 1);
case GAME_TICK:
return state + 1;
default:
return state;
}
}
The problem is likely in how you're using combineReducers, vs how you're writing your mapState function.
I'm going to guess that your mapState function looks like:
const mapState = (state) => {
return {
points : state.points
}
}
This works okay when you use your pointsReducer by itself, because your state has state.points.
However, when you use combineReducers the way you are, you're creating two problems for yourself:
You're naming the state field state.pointsReducer, not state.points
Your pointsReducer is further nesting the data as points
So, the actual data you want is at state.pointsReducer.points, when the component is expecting it at state.points.
To fix this, you should change how you're calling combineReducers, and change the pointsReducer definition to just track the data without nesting:
export function pointReducer(state: 1, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return state + 1
case DECREMENT_ENTHUSIASM:
return Math.max(1, state - 1);
case GAME_TICK:
return state.points + 1;
default:
return state;
}
}
// later
const rootReducer = combineReducers({
points : pointsReducer,
languageName : languageNameReducer,
});
See the Redux docs page on "Using combineReducers" for more details

Cannot access nested data from action creator which grabs data from contentful

So i am trying to access state from a component via react-redux's connect. The state is coming from an asynchronous action grabbing data from the Contentful api. I am pretty sure its because i am replacing the article:{} in the state with article: action.article (this action.article has nested data).
Here is the component:
import React, { Component } from 'react';
import ReactMarkDown from 'react-markdown';
import { connect } from 'react-redux';
import { getArticle } from '../../actions/blog.actions';
class ArticlePage extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.getArticle(this.props.match.params.id)
}
render() {
let {article} = this.props;
return (
<div className='article-page-wrapper'>
<div className='navbar-background'></div>
<div className='article-details'>
<img src={`https:${article}`} />
</div>
<ReactMarkDown className={'main-content'} source={article.blogContent} />
{console.log(this.props.article)}
</div>
)
}
}
function mapStateToProps(state) {
return {
article: state.blogReducer.article
}
}
function mapDispatchToProps(dispatch) {
return {
getArticle: (articleId) => {
dispatch(getArticle(articleId));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ArticlePage);
Here is the action:
export function getArticle (articleId) {
return function (dispatch) {
dispatch(getArticleRequest())
client.getEntries({'sys.id':`${articleId}`,include:1})
.then((article) =>{
console.log(article)
dispatch(getArticleSuccess(article.items[0].fields));
})
.catch((error) => {
dispatch(getArticleError(error));
})
}
}
export function getArticleRequest () {
return {
type: types.GET_ARTICLE_REQUEST
}
}
export function getArticleSuccess (article) {
return {
type: types.GET_ARTICLE_SUCCESS,
article: article
}
}
export function getArticleError (error) {
return {
type: types.GET_ARTICLE_ERROR,
error: error
}
}
Here is the reducer:
import * as types from '../types/blog.types';
const initialState = {
articles:[],
article:[],
error: null,
loading: true
}
export default function blogReducer (state=initialState, action) {
switch(action.type) {
case types.GET_ALL_ARTICLES_REQUEST :
return {...state, loading: true}
case types.GET_ALL_ARTICLES_SUCCESS :
return {...state,loading:false, articles:action.articles.items}
case types.GET_ALL_ARTICLES_ERROR :
return {...state, loading:false, error: action.error}
case types.GET_ARTICLE_REQUEST :
return {...state, loading: true}
case types.GET_ARTICLE_SUCCESS :
return {...state,loading:false,article: action.article}
case types.GET_ARTICLE_ERROR :
return {...state, loading:false, error: action.error}
default :
return state
}
}
Here is the structure of the data being retrieved from Contentful:
So it fails in the ArticlePage when i try and do article.authorImage.fields in the src for the authors image on the article. Here is the error message:
I am pretty sure its because when the empty {} in the state is updated by replacing it with the nested data from getArticle, it isn't setting the newState to the entire payload.
I would appreciate any help you can give, if it is indeed due to nested state and mutations can you provide a way of setting the state to equal the payload of the action.
When setting article in your redux store, you are setting
article: article.items[0].fields
whereas you are trying to access fields from this.props.article.fields, instead you have a fields called authorImage under fields which in turn contains fields key, so either you must use
this.props.articles.authorImage.fields
Also check for undefined property before using it since it may not be present initially and will only be populated on an async Request

Change in state do not propagate in props

I have the "classic" issue with the React redux about not propagating the change in state into the props when I try to access it in the component.
Here I have read that
99.9% of the time, this is because you are accidentally mutating data, usually in your reducer
Can you tell me what am I doing wrong? Is this the good way how to do the deep copy of the property of the specified object in array?
note: in the reducer in the return statement the state is clearly changed correctly (debugged it)
reducer:
case 'TOGGLE_SELECTED_TAG':
const toggledTagId = action.payload;
const index = findItemById(state.tags, toggledTagId);
const newTags = state.tags.slice(0);
if(index >= 0)
{
newTags[index] = Object.assign(
state.tags[index],
{selected: !state.tags[index].selected});
state.tags = newTags;
}
return Object.assign({}, state);
component:
import React from 'react';
import { Button, FormControl, Table, Modal } from 'react-bootstrap';
import { connect } from 'react-redux';
import axios from 'axios';
import {selectTagAction} from '../../actions/actions'
#connect((store) => {
return {
tags: store.TagsReducer.tags,
}
})
export default class AssignTag extends React.Component {
constructor(props) {
super(props);
this.handleTagClick = this.handleTagClick.bind(this);
}
handleTagClick(element) {
debugger;
this.props.dispatch(selectTagAction(element));
}
render() {
const tags = this.props.tags;
console.log(tags);
const mappedTags = tags.map(tag => {
return (
<div className="col-sm-12" key={tag.id} onClick={() => this.handleTagClick(tag.id)}
style={{background: this.getBackgroundColor(tag.selected)}}>
<span>{tag.name}</span>
</div>
)
})
// code continues
}
}
You are indeed mutating the state. Try this:
case 'TOGGLE_SELECTED_TAG':
const toggledTagId = action.payload;
const index = findItemById(state.tags, toggledTagId);
let newTags = state;
if( index >= 0 )
{
newTags[index] = Object.assign(
{},
state.tags[index],
{ selected: !state.tags[index].selected }
);
//state.tags = newTags; This line essentially mutates the state
return Object.assign( {}, state, { tags: newTags });
}
return state;
Another workaround to avoiding mutation of state is to use the ES6 shorthand in your reducer:
.... return { ...state, tags : newTags };

React-redux - can't extract part of state

There is this action creator:
export function selectBook(book) {
// selectBook is an ActionCreator, it needs to return an action,
// an object with a type property.
return {
type: 'BOOK_SELECTED',
payload: book,
};
}
There's this simple reducer:
export default function() {
return [
{ title: 'Javascript: The Good Parts', pages: 101 },
{ title: 'Harry Potter', pages: 39 },
{ title: 'The Dark Tower', pages: 85 },
{ title: 'Eloquent Ruby', pages: 1 }
];
}
The rest of the code is here:
https://github.com/StephenGrider/ReduxCasts/tree/master/book_list/src
Now, I am playing with it and wanted to return eg. number of characters in the title but first I just want to extract the title in the action creator and display it. I've modified the action creator as follows:
export function selectBook(book) {
// selectBook is an ActionCreator, it needs to return an action,
// an object with a type property.
let title = book.title;
console.log(title);
return {
type: 'BOOK_SELECTED',
payload: book,
count: title
};
}
Afterwards, I'll add methods to 'title' - I just want to access it for now from a component. The console.log(title) above outputs the title correctly. As you can see from the link, it then goes through mapStateToProps and then
<div>Title: {this.props.book.title}</div>
<div>Pages: {this.props.book.pages}</div>
<div>Count: {this.props.book.count}</div>
The first two output fine, as per original code. The third one, Count, does not. The {this.props.book.count} outputs blank.
Sorry for the confusion with multiple titles. Under 'Count' for now, I want to output the title. Once I've done it, I'll change the action creator to count the title's characters.
EDIT:
// State argument is not application state, only the state
// this reducer is responsible for
export default function (state = null, action) {
switch (action.type) {
case 'BOOK_SELECTED':
return { action.payload, action.count };
}
return state;
}
I tried the above but the syntax is wrong.
Some more code (reducers index):
import { combineReducers } from 'redux';
import BooksReducer from './reducer_books';
import ActiveBook from './reducer_active_book';
const rootReducer = combineReducers({
books: BooksReducer,
activeBook: ActiveBook
});
export default rootReducer;
Container with mapStateToProps:
import React, { Component } from 'react';
import { connect } from 'react-redux';
class BookDetail extends Component {
render() {
if (!this.props.book) {
return <div>Select a book to get started.</div>;
}
return (
<div>
<h3>Details for:</h3>
<div>Title: {this.props.book.title}</div>
<div>Pages: {this.props.book.pages}</div>
<div>Count: {this.props.book.count}</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
book: state.activeBook
};
}
export default connect(mapStateToProps)(BookDetail);
In a reducer you need to augment the state object to reflect the new payload you have sent. Not sure what your requirement is, but I guess you just want to return the an object with title, pages and count in the reducer.
You are getting the error in your reducer as indeed there is a syntax error.
The correct implementation if I understand the requirement right would be:-
export default function (state = null, action) {
switch (action.type) {
case 'BOOK_SELECTED':
return { title: action.payload.title,
pages: action.payload.pages,
count: action.count };
}
return state;
}
Another thing I would like to say is that you are anyways passing book in the payload then why do you need to pass the count separately, as it can easily be inferred from the book object.
Your action could be:-
export function selectBook(book) {
return {
type: 'BOOK_SELECTED',
payload: book
};
Reducer will be:-
export default function (state = null, action) {
switch (action.type) {
case 'BOOK_SELECTED':
return book;
}
return state;
}
As per advice given in the comments, what it needed is object augmentation:
export default function (state = null, action) {
switch (action.type) {
case 'BOOK_SELECTED':
return { payload: action.payload, count: action.count };
}
return state;
}

react js mapStateToProps triggers Uncaught TypeError: Cannot read property 'map' of undefined

I have an index.js which renders all tables in the database.
_renderAllTables() {
const { fetching } = this.props;
let content = false;
if(!fetching) {
content = (
<div className="tables-wrapper">
{::this._renderTables(this.props.allTables)}
</div>
);
}
return (
<section>
<header className="view-header">
<h3>All Tables</h3>
</header>
{content}
</section>
);
}
_renderTables(tables) {
return tables.map((table) => {
return <Table
key={table.id}
dispatch={this.props.dispatch}
{...table} />;
});
}
render() {
return (
<div className="view-container tables index">
{::this._renderAllTables()}
</div>
);
}
}
const mapStateToProps = (state) => (
state.tables);
export default connect(mapStateToProps)(HomeIndexView);
I changed mapStateToProps from above code to below code.
const mapStateToProps = (state) => ({
tables: state.tables,
currentOrder: state.currentOrder,
});
The reason why I changed code is that I want to use one of state of currentOrder. I have a state which shows whether table is busy or not. So in order to use that I added currentOrder in mapStateToProps. However, it triggers Uncaught TypeError: Cannot read property 'map' of undefined..
How can I use states from other components? any suggestions for that??
Thanks in advance..
--reducer.js
const initialState = {
allTables: [],
showForm: false,
fetching: true,
formErrors: null,
};
export default function reducer(state = initialState, action = {}) {
switch(action.type){
case Constants.TABLES_FETCHING:
return {...state, fetching: true};
case Constants.TABLES_RECEIVED:
return {...state, allTables: action.allTables, fetching: false};
case Constants.TABLES_SHOW_FORM:
return {...state, showForm: action.show};
case Constants.TALBES_RESET:
return initialState;
case Constants.ORDERS_CREATE_ERROR:
return { ...state, formErrors: action.errors };
default:
return state;
}
}
Your problem is that before fetching successfully tables, your component is rendered with state.tables is undefined.
First of all, best practice is to use selectors rather than json path like state.tables, to be in a separate selectors.js file using reselect lib as follow:
import { createSelector } from 'reselect';
const tablesSelector = state => state.tables;
export default {
tablesSelector,
}
Second, you need to add reducer for, let's assume, FETCH_ALL_TABLES action using combineReducers from redux lib and most important to initialize tables array with [] before the action is dispatched so as to be defined as follow:
import {combineReducers} from 'redux';
function tables(state = [], {type, payload}) {
switch (type) {
case 'FETCH_ALL_TABLES':
return [
...state,
...payload,
]
}
return state;
}
export default combineReducers({
tables,
});
and in your index.js, may be want update it to:
import selector from './selector';
...
function mapStateToProps(state) {
return {
tables: selector.tablesSelector(state),
}
}
You should always check if variable you want to map is defined.
_renderTables(tables) {
if (tables) {
return tables.map((table) => {
return <Table
key={table.id}
dispatch={this.props.dispatch}
{...table} />;
});
}
}

Resources