The action dispatch is not working, The function works and I get the console.log but the store isn't changing. Any ideas?
import React from 'react';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import RemoveTodo from './RemoveTodo';
import { remove } from '../actions/Todo';
import { store } from '../app';
class TodosSummary extends React.Component {
constructor(props) {
super(props);
}
onDelete = ({id}) => {
store.dispatch(remove({id}))
console.log(store.getState());
};
render () {
return (
<ul>
{this.props.target.map(({todo, significance, id}) => {
return (
<li
key={id}>{todo} - impact is {significance}
<button onClick={this.onDelete}>Remove</button>
</li>
);
})}
</ul>
</div>
);
}
};
const mapStateToProps = (state) => {
return {
target: state.target
}; };
export default connect(mapStateToProps)(TodosSummary)
This is the action, taking the todo id
export const remove = ({id}) => ({
type: 'REMOVE_TODO',
id
});
And that's the reducer, filtering the state and bringing back the filtered array
const todosReducer = (state = todosReducerDefaultState, action) => {
switch(action.type) {
case 'ADD_TODO':
return [
...state,
action.target
];
case 'REMOVE_TODO':
return (
state.filter(({ id }) => id !== action.id)
);
I see you want to access target property inside state. So, the reducer should be like this:
case 'ADD_TODO':
return {
...state,
target: [...state.target, action.target]
};
case 'REMOVE_TODO':
return {
...state,
target: state.target.filter(({ id }) => id !== action.id)
};
See if this works.
Return updated state like this.
case 'REMOVE_TODO':
return [...state.filter(({ id }) => id !== action.id)];
It's not suggested to change the state directly, please change the addToDo to the following.
Object.assign({}, state, {
todoList: state.target
});
Can you please provide the code snippet for the default state as per your code 'todosReducerDefaultState'?
Related
This is my component:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Divider } from "antd";
import MovieList from "../components/MovieList";
import IncreaseCountButton from "../components/IncreaseCountButton";
import DeleteButton from "../components/DeleteButton";
import { deleteMovie, increaseCount } from "../actions/movies";
import { getIsDeleting, getIsIncreasing } from "../reducers/actions";
export class MovieListContainer extends Component {
constructor(props) {
super(props);
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
static propTypes = {
isIncreasing: PropTypes.func.isRequired,
isDeleting: PropTypes.func.isRequired,
};
async handleIncrease(movie) {
await this.props.increaseCount(movie, this.props.token);
}
async handleDelete(movie) {
await this.props.deleteMovie(movie.id, this.props.token);
}
render() {
return (
<MovieList movies={this.props.movies}>
{(text, movie) => (
<div>
<IncreaseCountButton
onIncrease={() => this.handleIncrease(movie)}
loading={this.props.isIncreasing(movie.id)}
/>
<Divider type="vertical" />
<DeleteButton
onDelete={() => this.handleDelete(movie)}
loading={this.props.isDeleting(movie.id)}
/>
</div>
)}
</MovieList>
);
}
}
export const mapStateToProps = state => ({
isIncreasing: id => getIsIncreasing(state, id),
isDeleting: id => getIsDeleting(state, id),
});
export default connect(
mapStateToProps,
{ deleteMovie, increaseCount }
)(MovieListContainer);
I feel like this might be bad for performance/reconciliation reasons, but not sure how else to retrieve the state in a way that hides implementation details.
Gist link: https://gist.github.com/vitalicwow/140c06a52dd9e2e062b2917f5c741727
Any help is appreciated.
Here is how you can handle these asynchronous actions with redux. You can use thunk to perform 2 actions and can store a flag to determine what is being done to an object (Deleting, Changing, etc):
action
export const deleteMovieAction = id => {
return dispatch => {
dispatch({ type: "MOVIE_DELETING", id });
setTimeout(() => {
dispatch({ type: "MOVIE_DELETED", id });
}, 2000);
};
};
reducer
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "MOVIE_DELETING": {
const movies = [...state.movies];
movies.find(x => x.id === action.id).isDeleting = true;
return { ...state, movies };
}
case "MOVIE_DELETED": {
const movies = state.movies.filter(x => x.id !== action.id);
return { ...state, movies };
}
default:
return state;
}
};
https://codesandbox.io/s/k3jnv01ymv
An alternative is to separate out the ids into a new array that are being deleted
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case "MOVIE_DELETING": {
const movieDeletingIds = [...state.movieDeletingIds, action.id];
return { ...state, movieDeletingIds };
}
case "MOVIE_DELETED": {
const movieDeletingIds = state.movieDeletingIds.filter(
x => x.id !== action.id
);
const movies = state.movies.filter(x => x.id !== action.id);
return { ...state, movieDeletingIds, movies };
}
default:
return state;
}
};
https://codesandbox.io/s/mj52w4y3zj
(This code should be cleaned up, but is just to demo using thunk)
I have a parent react component containing 3 children:
<ChildComponent category="foo" />
<ChildComponent category="bar" />
<ChildComponent category="baz" />
The child component calls an api depending on the prop category value:
http://example.com/listings.json?category=foo
In my action the data is returned as expected. However, when the child component renders the data. The last child baz is overwriting its value in foo and bar as well.
A solution to this problem seems to be given here. But I would like this to be dynamic and only depend on the category prop. Is this not possible to do in Redux?
My child component looks like this:
class TweetColumn extends Component {
componentDidMount() {
this.props.fetchTweets(this.props.column)
}
render() {
const { tweets, column } = this.props
if (tweets.length === 0) { return null }
const tweetItems = tweets[column].map(tweet => (
<div key={ tweet.id }>
{ tweetItems.name }
</div>
)
return (
<div className="box-content">
{ tweetItems }
</div>
)
}
}
TweetColumn.propTypes = {
fetchTweets: PropTypes.func.isRequired,
tweets: PropTypes.array.isRequired
}
const mapStateToProps = state => ({
tweets: state.tweets.items
})
export default connect(mapStateToProps, { fetchTweets })( TweetColumn )
reducers:
export default function tweetReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TWEETS_SUCCESS:
return {
...state,
[action.data[0].user.screen_name]: action.data
}
default:
return state;
}
}
export default combineReducers({
tweets: tweetReducer,
})
action:
export const fetchTweets = (column) => dispatch => {
dispatch({ type: FETCH_TWEETS_START })
const url = `${ TWITTER_API }/statuses/user_timeline.json?count=30&screen_name=${ column }`
return axios.get(url)
.then(response => dispatch({
type: FETCH_TWEETS_SUCCESS,
data: response.data
}))
.then(response => console.log(response.data))
.catch(e => dispatch({type: FETCH_TWEETS_FAIL}))
}
You are making an api call every time TweetColumn is mounted. If you have multiple TweetColumn components and each one makes an api call, then whichever one's response is last to arrive is going to set the value of state.tweets.items. That's because you are dispatching the same action FETCH_TWEETS_SUCCESS every time (the last one overrides the previous one). To solve that issue, assuming the response has a category (foo, bar, baz), I would write the reducer in the following way:
export default function tweetReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TWEETS_SUCCESS:
return {
...state,
[action.data.category]: action.data
}
default:
return state;
}
}
You can then do the following in your TweetColumn component:
class TweetColumn extends Component {
componentDidMount() {
this.props.fetchTweets(this.props.column)
}
render() {
const { column } = this.props;
const tweetItems = this.props.tweets[column].map(tweet => (
<div key={ tweet.id }>
{ tweet.name }
</div>
)
return (
<div className="box-content">
{ tweetItems }
</div>
)
}
}
const mapStateToProps = state => ({
tweets: state.tweets
})
const mapDispatchToProps = dispatch => ({
fetchTweets: column => dispatch(fetchTweets(column))
})
export default connect(
mapStateToProps,
mapDispatchToProps,
)( TweetColumn )
You will have to do some validation to make sure tweets[column] exists, but you get the idea.
I'm trying to edit an object and replace it in array using React and Redux like this:
case EDIT_LANGUAGE:
let languages = [...state.languageSkills];
languages[languages.findIndex(el => el.id === action.payload.id)] = action.payload;
return {
...state,
languageSkills: languages
};
'languages' array looks find before return statement, but state is not re-rendered. I guess I'm mutating state somehow. Other actions (delete, get, set) are working fine. Any tips?
EDIT. This is relevant part of the component that should render
import { setLanguages, getLanguages } from '../../actions';
import {connect} from 'react-redux';
import {bindActionCreators} from "redux"
import React, { Component } from 'react';
class UserProfile extends Component {
constructor(props) {
super(props);
}
render() {
const languageSkillItems = this.props.languageSkills.map((languageSkill) => {
return (
<LanguageSkillItem key={languageSkill.id} item={languageSkill} />
)
});
return (
<div className="profile">
<Language languageSkillItems={languageSkillItems} />
</div>
)
}
}
const mapStateToProps = (state) => {
return {
languageSkills: state.languageSkills
};
};
const mapDispatchToProps = dispatch => {
return {
...bindActionCreators({ setLanguages, getLanguages }, dispatch)
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserProfile
);
You need to create a new array reference, easiest way is just to use map, like so:
case EDIT_LANGUAGE:
const languageSkills = state.languageSkills.map(el => {
if(el.id === action.payload.id) {
return action.payload;
}
return el;
});
return {
...state,
languageSkills
};
After combining two reducers together (EditButton and TodoApp), my app everytime start crash. Before it, when I just use only one reducer TodoApp I did not have any problem with reducers. But now I cannot figure out what is wrong, because every time I get the error in map function of component below . Error "TypeError: Cannot read property 'map' of undefined".
So, what is I forgot? Also I cannot get the state in nested components or containers of App. It's strange too, but in App I can do that by console.log() for example.
/* REDUCERS */
import { combineReducers } from 'redux'
import { ADD_TODO, EDIT_TODO, DELETE_TODO, FILTER_TODO_UP, FILTER_TODO_DOWN } from '../Variables/Variables'
const initialState = {
todos: []
}
function EditButton(state, action) {
if (typeof state === 'undefined') {
return 'Edit';
}
switch (action.type) {
case EDIT_TODO:
return state = "Edit" ? "Done" : "Edit"
default:
return state
}
}
function TodoApp(state, action) {
if (typeof state === 'undefined') {
return initialState;
}
switch (action.type) {
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
id: action.id,
text: action.text,
done: action.done
}
]
});
case EDIT_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
id: action.id,
text: action.text,
done: action.done
}
]
});
case DELETE_TODO:
return Object.assign({}, {
todos: state.todos.filter(todos => todos.id !== parseInt(action.id))
});
case FILTER_TODO_UP:
return Object.assign({}, {
todos: [
...state.todos.sort((a, b) => b.id - a.id)
]
});
case FILTER_TODO_DOWN:
return Object.assign({}, {
todos: [
...state.todos.sort((a, b) => a.id - b.id)
]
});
default:
return state;
}
}
export default combineReducers({TodoApp, EditButton})
/* APP */
import React, { Fragment } from 'react';
import TodoFormAdd from '../Containers/TodoFormAdd';
import TodoListAdd from '../Containers/TodoListAdd';
import TodoFormFilterAdd from '../Containers/TodoFormFilterAdd';
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return(
<Fragment>
// console.log(this.props.state.getState()) - work!
<TodoFormAdd />
<TodoListAdd store={this.props.store} />
<TodoFormFilterAdd />
</Fragment>
);
}
}
export default App;
/* CONTAINER */
import { connect } from 'react-redux';
import TodoList from '../Components/TodoList/TodoList';
import { DeleteTodo } from '../Actions/AddTodo'
// console.log(this.props.state.getState()) - does not work!
const mapStateToProps = state => ({
todos: state.todos
});
const mapDispatchToProps = dispatch => ({
todoFormDelete: todo => dispatch(DeleteTodo(todo))
});
export default connect(
mapStateToProps,
mapDispatchToProps)(TodoList)
/* COMPONENT */
import React from 'react';
import TodoIteam from '../TodoIteam/TodoIteam'
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');
this.props.todoFormDelete(index);
} else {
return
}
}
render(props) {
// console.log(this.props.state.getState()) - does not work!
return (
<ul onClick={this.handleDelete}>{this.props.todos.map((iteam, index) =>
// this where I get an error
<TodoIteam key={index} index={iteam.id} {...iteam} />
)}
</ul>
);
}
}
export default TodoList;
As you are using ES6 property shorthand notation in combineReducers :
combineReducers({TodoApp, EditButton})
This is equivalent to writing combineReducers({ TodoApp: TodoApp, EditButton: EditButton })
But inside your CONTAINER you are accessing state.todos there is nothing called todos coming from state instead its TodoApp and Hence you get error in your .map():
this.props.todos.map((iteam, index) {}
EDIT :
As you are returning an object containing an array from your reducers called todos so to access correct state you need to use reducer Name followed by an array name you are returning which would be TodoApp.todos
So inside your Container you need to access correct reducer
const mapStateToProps = state => ({
todos: state.TodoApp.todos // Notice TodoApp is used instead of todos
});
You can read more about combineReducers on Redux Documentation
First of all my code is working (everything is exported correctly etc ) but it's not waiting for the async return of data.
I'm using redux-thunk for my async middleware
I have an action named async.js
export function itemsHasErrored(bool) {
return {
type: 'ITEMS_HAS_ERRORED',
hasErrored: bool
};
}
export function itemsIsLoading(bool) {
return {
type: 'ITEMS_IS_LOADING',
isLoading: bool
};
}
export function itemsFetchDataSuccess(items) {
return {
type: 'ITEMS_FETCH_DATA_SUCCESS',
items
};
}
export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsIsLoading(true));
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(itemsIsLoading(false));
return response;
})
.then((response) => response.json())
.then((items) => dispatch(itemsFetchDataSuccess(items)))
.catch(() => dispatch(itemsHasErrored(true)));
};
}
My reducer
export function itemsHasErrored(state = false, action) {
switch (action.type) {
case 'ITEMS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
}
export function itemsIsLoading(state = false, action) {
switch (action.type) {
case 'ITEMS_IS_LOADING':
return action.isLoading;
default:
return state;
}
}
export function items(state = [], action) {
switch (action.type) {
case 'ITEMS_FETCH_DATA_SUCCESS':
return action.items;
default:
return state;
}
}
I have a container component, asyncContainer.js
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
import {itemsFetchData} from '../../../actions/async';
import AsyncUI from './asyncUI'
class AsyncContainer extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
//This fails to wait
return (
<AsyncUI />
);
}
}
AsyncContainer.propTypes = {
fetchData: PropTypes.func.isRequired,
items: PropTypes.array.isRequired,
hasErrored: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AsyncContainer);
And finally I have a simple UI component named asyncUI.js written in a functional way
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
const AsyncUI = (items) => {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
const mapStateToProps = (state) => {
return {
items: state.items
};
};
export default connect(mapStateToProps)(AsyncUI);
In asyncContainer.js you can see the call to my simple UI component
return (
<AsyncUI />
);
But on calling the property of the redux store items in asyncUI.js an empty array, therefore the items.map fails
However, if I remove the code from asyncUI.js and place it in asyncContainer.js it works
This is the code that works in asyncContainer.js
class AsyncContainer extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
//THIS IS WHERE I HAD <Async />
return (
<ul>
{this.props.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
AsyncContainer.propTypes = {
fetchData: PropTypes.func.isRequired,
items: PropTypes.array.isRequired,
hasErrored: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AsyncContainer);
I think the problem is that the component is rendering before the items data is ready. This is normal react behavior. So how do I "hold off" the rendering. As you can see I'm trying to use a Container /Component style of architecture. I can always use the solution that works as mentioned above, but I'd like to stick with this Container /Component.
Am I going to have to delve deeper into Promises etc ?
Should I not use the functional way of writing for asyncUI.js ?
I'm a little confused.
Try:
const AsyncUI = ({items}) => {
/* ^ see ^ */
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
); }
This pulls the items value off the props you reacted in the mapStateToProps function, which is an object, not an array (hence no map function).
NOTE: This should fix your issue, but it is still technically trying to render the items before they are ready in 2 instances:
The first time the component renders. The initial state for itemsIsLoading is false, so the first render will fail all the safety checks. The default value for items is [] so it should just render <ul></ul> for a very brief moment until the itemsIsLoading(true) action is dispatched. You can set the initial state to true for stop this, or change the loading check to be
if (this.props.isLoading || this.props.items.length != 0) {
return <p>Loading…</p>;
}
An argument can be made for how necessary either of those solutions actually are.
After the fetch returns the order the actions is dispatched in will result in another brief render of <ul></ul> as the loading state is set to false before the items are set. See dgrijuela's answer for one way to fix this. Another way would be to not dispatch seperate actions and have the ITEMS_FETCH_DATA_SUCCESS and ITEMS_HAS_ERRORED actions also change the itemsIsLoading value back to false (multiple reducers can act on the same action type).
You call dispatch(itemsIsLoading(false)) before dispatch(itemsFetchDataSuccess(items))
Try like this:
// async.js
...
export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsIsLoading(true));
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json())
.then((items) => {
dispatch(itemsFetchDataSuccess(items));
dispatch(itemsIsLoading(false));
})
.catch(() => dispatch(itemsHasErrored(true)));
};
}
see Michael Peyper for a good answer
It turns out that the problem was with the functional style of coding of my asyncUI component. I converted it back to the 'standard' stateful component and bingo it worked.
asyncContainer.js
class AsyncContainer extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
return (
<AsyncUI />
)
}
}
asyncUI.js
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
class AsyncUI extends Component {
render() {
return (
<ul>
{this.props.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
)
}
}
const mapStateToProps = (state) => {
return {
items: state.items,
};
};
export default connect(mapStateToProps)(AsyncUI);
Hope this helps anyone :-)