React-Redux's mapStateToProps working in unexpected way - reactjs

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

Related

Array inside redux state is undefined

here I'm facing a little problem with react - redux .....
I'm getting state attribute (array) undefined.... What might be the reason?
Here is my Reducers.js code ..
const initialState = {
cList: [], //I'm getting this array undefined...
selectedCatagory: "",
}
export const reducerForCatagoryList = (state = initialState , action) => {
switch(action.type){
case "CATAGORY_LIST":
let lis = [...state.cList,action.payload];
return Object.assign({},state,{ cList: lis });
default:
return state;
}
}
Actions.js code is:
export const catagoryList = (list) => {
return {
type: "CATAGORY_LIST",
payload: list
}
}
And my Component's code is:
//This array is just for testing purpose....
const list = [{
cName: 1,
heading: "Heading",
link: ""
},
{
cName: 2,
heading: "Heading2",
link: ""
}]
const mapStateToProps = (state) => {
console.log("state is: ",state);
return {
cList: state.cList
}
}
const mapDispatchToProps = (dispatch) => {
return {
catagoryList: (list) => {
dispatch(catagoryList(list));
}
}
}
class MainPage extends Component{
render(){
console.log("Props are: ",this.props)
return(
<div id = "Cards-div">
{this.props.catagoryList(list)}
{
createCards(this.props.cList) //I want to send Array inside redux store (cList) to this......
}
</div>
)
}
}
export default connect(mapStateToProps,mapDispatchToProps)(MainPage);
So, Plz can someone explain me what's going wrong here?

React dispatch from reducer firing twice

Whenever I call a function from the reducer, it gets called once the first time, and then twice every other time.
Here's the code:
reducer.js:
import data from './data'
export const initialState = {
notes: data,
filter: '',
};
export const setFilter = filter => ({ type: 'setFilter', filter });
export const createNote = id => ({ type: 'createNote', id })
export const deleteNote = note => ({ type: 'deleteNote', note })
export const reducer = (state, action) => {
switch (action.type) {
case 'setFilter':
return { ...state, filter: action.filter };
case 'createNote':
console.count('Create note fired')
state.notes.push({
id: action.id,
tags: [],
content: ""
})
return { ...state }
case 'deleteNote':
return {
...state,
notes: state.notes.filter((note) => note.id !== action.note.id)
}
default: return state;
}
};
The component that calls the delete method:
import React from 'react'
import PropTypes from 'prop-types';
import { deleteNote } from "../../state/reducer";
import { useStateValue } from "../../state/StateContext";
import './Body.css'
import { Card, Badge } from 'react-bootstrap'
const Body = ({ notes }) => {
let [state, dispatch] = useStateValue();
return (
<div className="Body">
{
notes.map(note =>
<Card key={note.id} className="Card">
<Card.Body className="CardText HideScrollbar">
<Card.Text>{note.content}</Card.Text>
</Card.Body>
<Card.Footer>
{note.tags.map(tag =>
<Badge variant="primary">
{tag} </Badge>)}
</Card.Footer>
<div className="DeleteButton" onClick={() => dispatch(deleteNote(note))}>
<svg className="svg-icon" viewBox="0 0 20 20">
<path d="M10.185,1.417c-4.741,0-8.583,3.842-8.583,8.583c0,4.74,3.842,8.582,8.583,8.582S18.768,14.74,18.768,10C18.768,5.259,14.926,1.417,10.185,1.417 M10.185,17.68c-4.235,0-7.679-3.445-7.679-7.68c0-4.235,3.444-7.679,7.679-7.679S17.864,5.765,17.864,10C17.864,14.234,14.42,17.68,10.185,17.68 M10.824,10l2.842-2.844c0.178-0.176,0.178-0.46,0-0.637c-0.177-0.178-0.461-0.178-0.637,0l-2.844,2.841L7.341,6.52c-0.176-0.178-0.46-0.178-0.637,0c-0.178,0.176-0.178,0.461,0,0.637L9.546,10l-2.841,2.844c-0.178,0.176-0.178,0.461,0,0.637c0.178,0.178,0.459,0.178,0.637,0l2.844-2.841l2.844,2.841c0.178,0.178,0.459,0.178,0.637,0c0.178-0.176,0.178-0.461,0-0.637L10.824,10z"></path>
</svg>
</div>
</Card>
)
}
</div>
)
}
Body.propTypes = {
notes: PropTypes.arrayOf(PropTypes.object),
}
export default Body
Any kind of help would be really helpful, please tell me if there's any file missing or if I implemented the reducer in the wrong way, what I did was mostly following notes from a friend's University professor
make seperate action file. And get that action from redux through mapDispatchToProps in your component , where you want to dispatch that action.
const mapDispatchToProps = {
setProfileDialog: ProfileAction.setProfileDialog,
}
The issue is that reducers must be pure. When react is in 'strict-mode' it will fire reducers twice to ensure that the result is the same both times. Mutating the original state will cause unwanted side effects.
Changing:
case 'createNote':
console.count('Create note fired')
state.notes.push({
id: action.id,
tags: [],
content: ""
})
return { ...state }
To:
case 'createNote':
const notes = [
...state.notes,
{
id: action.id,
tags: [],
content: "",
}
]
return {...state, notes}
Should fix your example.

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

Filter deletes all state inside the Redux store

I have a little problem with filter of the Redux iteams store with reducers case DELETE_TODO.
So the problem is happen when I try to delete some of the elements inside the todo list. Deleting is work, but per click on some separate element to delete it, the whole todo list is gone. I cannt understand why.
I make the several tests to understand where is the problem located. What I got:
The action work good, because the value come from it into reducers normally. Filter work.
When the list in app had been deleted and I try to add some other todos, the app start crash with error - TypeError: Cannot read property 'todos' of undefined.
Plese, help someone if you can, because I'm already empty... :(
/* REDUCERS */
import { combineReducers } from 'redux'
import { ADD_TODO, ADDED_BUTTON, TOGGLE_BUTTON, EDIT_TODO, DELETE_TODO, FILTER_TODO_UP, FILTER_TODO_DOWN } from '../Variables/Variables'
const initialState = {
iteams: [{
todos:[],
buttons:[]
}]
}
function TodoApp(state, action) {
if (typeof state === 'undefined') {
return initialState;
}
switch (action.type) {
case ADD_TODO:
console.log('Reduce',action.text);
return Object.assign({}, state, {
iteams: [{
todos: [
...state.iteams[0].todos,
{
id: action.id,
text: action.text,
}
],
buttons: [
...state.iteams[0].buttons,
{
id: action.id,
text: action.text,
done: false
}
]
}]
});
case DELETE_TODO:
return Object.assign({}, {
iteams: state.iteams.filter(iteam => {
iteam.todos.id !== parseInt(action.id)
})
});
default:
return state;
}
}
export default TodoApp
/* ACTIONS */
import { ADD_TODO } from '../Variables/Variables'
let nextTodoId = 0;
function AddTodo(text) {
console.log('Action', text);
return {
type: ADD_TODO,
id: nextTodoId++,
text,
done: false
}
};
function DeleteTodo(id) {
return {
type: DELETE_TODO,
id
}
};
export { AddTodo }
/* CONTAINER */
import { connect } from 'react-redux';
import TodoList from '../Components/TodoList/TodoList';
import { DeleteTodo } from '../Actions/AddTodo'
const mapStateToProps = state => ({
iteams: state.iteams
});
const mapDispatchToProps = dispatch => ({
todoFormDelete: todo => dispatch(DeleteTodo(todo))
});
export default connect(
mapStateToProps,
mapDispatchToProps)(TodoList)
/* COMPONENT */
import React, {Fragment} from 'react';
import TodoIteam from '../TodoIteam/TodoIteam'
import ButtonToggle from '../ButtonToggle/ButtonToggle'
class TodoList extends React.Component {
handleDelete = (e) => {
let target = e.target;
let closestDelete = target.closest('span');
let closestEdit = target.closest('button');
if (closestDelete) {
let index = closestDelete.parentNode.getAttribute('index');
console.log('index', index);
this.props.todoFormDelete(index);
} else {
return
}
}
render(props) {
return (
<Fragment>
{console.log('Hi', this.props.store.getState().iteams)}
<div onClick={this.handleDelete}>
{this.props.iteams.map((iteam, index) => {
return <TodoIteam key={index} {...iteam} />
})}
</div>
</Fragment>
);
}
}
export default TodoList;
im so confused with your initialState. I think it must be this
const initialState = {
iteams: {
todos:[],
buttons:[]
}
}
and the ADD_TODO
case ADD_TODO:
return Object.assign({}, state, {
iteams: {
todos: [
...state.iteams.todos, //Change here
{
id: action.id,
text: action.text,
}
],
buttons: [
...state.iteams.buttons, //Change here
{
id: action.id,
text: action.text,
done: false
}
]
}
})
and in the DELETE_TODO
case DELETE_TODO:
return {
iteams: {
todos: state.iteams.todos.filter(iteam => iteam.id !== parseInt(action.id)),
buttons: state.iteams.buttons.filter(button => button.id !== parseInt(action.id))
}
}
Why is iteams an array? (BTW, didn't you mean 'items'?)
When you add a 'todo', you add it to iteams[0].todos.
However, when you filter in order to delete a 'todo', you go through the iteams array, and not through the todos array...
Seems to me (unless I don't understand what you intend to do) is that that the state should be:
iteams: {
todos:[],
buttons:[]
}
and when you delete an item, you should filter the iteams.todos array, not the iteams array.

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

Resources