React-redux - can't extract part of state - reactjs

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;
}

Related

React-Redux's mapStateToProps working in unexpected way

I have a component which shows a list of names fetched from the Redux store. The component looks like this:
class Details extends React.Component {
constructor (props) {
super(props);
}
render () {
const listName = [...this.props.listName];
return (
<div className="container detailsBox">
<p>Details Grid:</p>
{listName ? (
listName.map((el, index) => {
return (
<li key={index}>
{el}
</li>
)
})
): null}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
listName: state.listName
}
}
export default connect(mapStateToProps)(Details);
Notice here I only map the listName from the store. But the code does not display the <li> with the elements of listName even when logging in console shows listName is populated with data
But data shows as expected when the name is also fetched from the store, with this modification in mapStateToProps:
const mapStateToProps = (state) => {
return {
listName: state.listName,
name: state.name
}
}
I am befuddled as well as curious to know why the code behaves in this unexpected way? What am I missing here?
The reducer code looks like this:
import { UPDATE_NAME, ADD_NAME } from '../actions/addName.action';
const initialState = {
name: '',
listName: new Set()
}
function rootReducer (state = initialState, action) {
switch (action.type) {
case ADD_NAME:
return {
name: '',
listName: state.listName.add(action.payload)
}
case UPDATE_NAME:
return {
...state,
name: action.payload
}
default:
return state;
}
}
export default rootReducer;
and the actions like this:
export const ADD_NAME = 'ADD_NAME';
export const UPDATE_NAME = 'UPDATE_NAME';
export function addName (data) {
return {
type: ADD_NAME,
payload: data
}
}
export function updateName (name) {
return {
type: UPDATE_NAME,
payload: name
}
}
This is happens because you mutate the state. When you mutate listNames it still points to the same object in memory, and react thinks that nothing is changed, while the contents of listNames is changed. You should return a new object.
case ADD_NAME:
return {
name: '',
listName: new Set([...state.listName, action.payload]),
}
But it is not recommended to use Sets in reducers. Instead you can store your listNames as an Array and use lodash union function to have only unique values.
import { union } from 'lodash';
const initialState = {
name: '',
listName: []
}
// some code
case ADD_NAME:
return {
name: '',
listName: union(state.listNames, action.payload)
}
You can instead use an object to hold the listName and then recreate the structure of your state into a new object and then return.
const initialState = {
name: '',
listName: {}
}
case ADD_NAME:
return {
name: '',
listName: {...state.listName, action.payload}
}

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

Failed prop type: The prop todos[0].id is marked as required in TodoList, but its value is undefined

I'm trying to do the redux basic usage tutorial which uses react for the UI.
I'm getting this warning (in red though - perhaps it is an error?) in the console logs when I click a button labelled "Add Todo":
warning.js:36 Warning: Failed prop type: The prop todos[0].id is
marked as required in TodoList, but its value is undefined.
in TodoList (created by Connect(TodoList))
in Connect(TodoList) (at App.js:9)
in div (at App.js:7)
in App (at index.js:12)
in Provider (at index.js:11)
So the todo that is getting added, has no id field - I need to figure out how to add an id.
in actions.js
/*
* action creators
*/
export function addTodo(text) {
return { type: ADD_TODO, text }
}
actions/index.js
let nextTodoId = 0
export const addTodo = (text) => {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text
}
}
export const setVisibilityFilter = (filter) => {
return {
type: 'SET_VISIBILITY_FILTER',
filter
}
}
export const toggleTodo = (id) => {
return {
type: 'TOGGLE_TODO',
id
}
}
containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}>
<input ref={node => {
input = node
}} />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
It looks like actions/index.js does add the todo id?
It's hard to debug because for some reason the chrome dev tools sources are missing the actions and reducers folders of my app:
How do I get the todos[0] to have a unique id?
note that when I add id: 1 here it does get the id added but it is not unique:
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false,
id: 1
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
Maybe:
/*
* action creators
*/
export function addTodo(text) {
return { type: ADD_TODO, text }
}
needs id added?
I briefly looked over your code, and I believe you're not getting that id because you're not importing the action creator function you think you are.
in containers/AddTodo.js:
import { addTodo } from '../actions'
In your project, you have
./src/actions.js
./src/actions/index.js
When you import or require anything (without using file extensions like the above '../actions'), the JS interpreter will look to see if there's a file called actions.js in the src folder. If there is none, it will then see if there's an actions folder with an index.js file within it.
Since you have both, your AddTodo component is importing using the action creator in ./src/actions.js, which does not have an id property as you had originally guessed.
Remove that file, and it should work as you intended.
You have to add an 'id' variable to the actions file then increase the value every time you call the action creator.
action creator:
let nextTodoId = 0;
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
});
reducer:
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id, // unique id
text: action.text,
completed: false
}
]
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
)
default:
return state
}
}

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

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.

Resources