I have a simple React/Redux app that displays a list of cars based on my Rails API.
I'm trying to add a sort feature that alphabetizes the cars by their name.
While my variable orgArray is in fact alphabetized when I console.log it, my Redux dev tool says states are equal after clicking the sort button - therefore my UI isn't updated.
Here's my code:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import CarCard from '../components/CarCard';
import CarForm from './CarForm';
import './Cars.css';
import { getCars } from '../actions/cars';
import { sortCar } from '../actions/cars';
Component.defaultProps = {
cars: { cars: [] }
}
class Cars extends Component {
constructor(props) {
super(props)
this.state = {
cars: [],
sortedCars: []
};
}
sortAlphabetically = () => {
console.log("sort button clicked")
const newArray = [].concat(this.props.cars.cars)
const orgArray = newArray.sort(function (a,b) {
var nameA = a.name.toUpperCase();
var nameB = b.name.toUpperCase();
if (nameA < nameB) {
return -1;
} else if (nameA > nameB) {
return 1;
}
return 0;
}, () => this.setState({ cars: orgArray }))
console.log(orgArray)
this.props.sortCar(orgArray);
}
componentDidMount() {
this.props.getCars()
this.setState({cars: this.props.cars})
}
render() {
return (
<div className="CarsContainer">
<h3>Cars Container</h3>
<button onClick={this.sortAlphabetically}>Sort</button>
{this.props.cars.cars && this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
{/* {this.state.cars.cars && this.state.cars.cars.map(car => <CarCard key={car.id} car={car} />)} */}
<CarForm />
</div>
);
}
}
const mapStateToProps = (state) => {
return ({
cars: state.cars
})
}
const mapDispatchToProps = (dispatch) => {
return {
sortCar: (cars) => dispatch(sortCar(cars)),
getCars: (cars) => dispatch(getCars(cars))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cars);
I would have guessed that mapStateToProps or adding the sortedCars: [] in my initial setState would have worked.
Essentially, my props are getting updated, but I need my state to be updated as well - though I'm not sure what I'm missing.
UPDATED:
Here's my action creator and Async action if it helps:
const sortCars = cars => {
return {
type: 'SORT_CARS',
cars
}
}
// Async Actions
export const sortCar = (cars) => {
console.log(cars, 'cars object');
return dispatch => {
dispatch(sortCars(cars))
}
}
UPDATE:
Here's the Reducer as well:
export default (state = {cars: []}, action) => {
switch(action.type) {
case 'GET_CARS_SUCCESS':
return Object.assign({}, state, {cars: action.payload})
case 'CREATE_CAR_SUCCESS':
return Object.assign({}, state, {cars: action.payload})
case 'REMOVE_CAR;':
return state.filter(car => car.id !== action.id)
case 'SORT_CARS;':
return Object.assign({}, state, { cars: action.payload})
default:
return state;
}
}
First, I think you don't actually need any state here, you can just update the store and show cars from props.
Also, I think the problem here is that you pass orgArray to this.props.sortCar(orgArray) which is an array instead of object with "cars" key and values inside.
Apart from that setState call is being declared as anonymous function instead of being actually executed after the sorting.
Make sure to indent your code.
Sort only have one argument (the sort function). And in your sortAlphabetically method setState (placed as a second argument of sort) is not being called.
The method should be something like that (I didn't tested it)
sortAlphabetically = () => {
console.log("sort button clicked")
const newArray = this.props.cars.cars.slice();
const orgArray = newArray.sort(function (a, b) {
var nameA = a.name.toUpperCase();
var nameB = b.name.toUpperCase();
if (nameA < nameB) {
return -1;
} else if (nameA > nameB) {
return 1;
}
return 0;
});
this.setState({ cars: orgArray }, () => {
console.log(orgArray);
this.props.sortCar(orgArray);
});
}
Got a solution that just uses the local state instead of the Redux store (not ideal, but fulfills this feature).
Adding async to my componentDidMount along with the await waits to update the data until the local state has changed, which then reflects in my UI.
Thanks everyone for your help.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import CarCard from '../components/CarCard';
import CarForm from './CarForm';
import './Cars.css';
import { getCars } from '../actions/cars';
import { sortCar } from '../actions/cars';
Component.defaultProps = {
cars: { cars: [] }
}
class Cars extends Component {
constructor(props) {
super(props)
this.state = {
cars: [],
sortedCars: []
};
}
sortAlphabetically = () => {
console.log("sort button clicked")
const newArray = [].concat(this.props.cars.cars)
const orgArray = newArray.sort(function (a,b) {
var nameA = a.name.toUpperCase();
var nameB = b.name.toUpperCase();
if (nameA < nameB) {
return -1;
} else if (nameA > nameB) {
return 1;
}
return 0;
})
console.log(orgArray)
this.props.sortCar(orgArray);
this.setState({ cars: {cars: orgArray} })
}
async componentDidMount() {
await this.props.getCars()
this.setState({cars: this.props.cars})
}
render() {
return (
<div className="CarsContainer">
<h3>Cars Container</h3>
<button onClick={this.sortAlphabetically}>Sort</button>
{this.state.cars.cars && this.state.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
<CarForm />
</div>
);
}
}
const mapStateToProps = (state) => {
return ({
cars: state.cars
})
}
const mapDispatchToProps = (dispatch) => {
return {
sortCar: (cars) => dispatch(sortCar(cars)),
getCars: (cars) => dispatch(getCars(cars))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cars);
You should not use setState(). setState() does not immediately mutate this.state but creates a pending state transition. this.state after calling this method can potentially return the existing value. so here is how to handle sorting.
NOTE that by default, the sort() method sorts the values as strings in alphabetical and ascending order. so u dont need to define any compare function.
1-u can define another action and have reducer handle the sorting. the thing is here u have to make sure u do not mutate the state so use slice()
case 'SORT_CARS':
return state.slice().sort()
2- u can define a function and pass the state as arguments and return the sorted state.
const mapStateToProps = (state) => {
return ({
cars: state.cars.sort()
})
}
Related
I have a problem in my redux based todolist , where adding to the todo and deleting works , but i am not able to update the todo. i am using immunity helpers , it is updating the next item in the list and sometimes returns object is undefined!
This is my EditTodo
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { editTodo } from '../../actions/editTodoActions'
class EditTodo extends Component {
constructor(props){
super(props)
this.state = {
id:this.props.match.params.id,
todo:this.props.todo[0].todo
}
}
onInputChange = (e) => {
const todo = e.target.value;
this.setState({
todo : todo
})
}
editTodo = (e) => {
e.preventDefault()
const todo = {
id : this.state.id,
todo: this.state.todo
}
this.props.editTodo(todo);
}
render() {
const id = this.props.match.params.id
console.log(this.props.todo)
console.log(this.props.todo[0].todo)
return (
<div>
<input className="todo" name="todo" onChange={this.onInputChange} value={this.state.todo} type="text" placeholder="enter todo" /> <br />
<button onClick={this.editTodo}>Submit </button>
</div>
)
}
}
const MapStateToProps = (state,ownProps) =>({
todo : state.todolist.filter((item) => item.id == ownProps.match.params.id)
})
const MapDispatchToProps = {
editTodo : editTodo
}
export default connect(MapStateToProps,MapDispatchToProps)(EditTodo)
This is Edit Todo Action
import { EDIT_TODO } from './types'
export function editTodo(newTodo){
return {
type: EDIT_TODO,
payload: {
todo: newTodo
}
}
}
This is my Reducer :
import { ADD_TODO,EDIT_TODO, DELETE_TODO } from '../actions/types'
import update from 'immutability-helper'
const todolist = [
{ id :1, todo:"hello team"}
]
function todolistReducer(state=todolist,{ type, payload }){
switch(type){
case ADD_TODO:
let newTodo = {
id: payload.todo.id,
todo:payload.todo.todo
};
return state.concat([newTodo]);
case EDIT_TODO:
console.log('edit todo called')
console.log(payload.todo.id)
return update(state, {
[payload.todo.id]: {
todo: {$set: payload.todo.todo}
}
});
// Now we know that foo is defined, we are good to go.
case DELETE_TODO:
console.log('delete reducer called')
return state.filter((item) => item.id != payload.id)
default:
return state
}
}
export default todolistReducer
The update method provided by immutability-helper accepts second parameter key as index of the object that need to be updated in the array.
In your case it's not working or updating the next object because the id's of the objects are defined starting from 1. Ideally before updating you should find the index of the object the need to be updated and then use the same index to update the object in the array of todos.
So I have created a utility to find out the index of the object need to be updated and then calling the update method
export const todolist = [
{ id :1, todo:"hello team"},
{ id :2, todo:"hello team 2"}
]
const updateObj = (list, id, value) => {
let index = list.findIndex(todo => todo.id === id)
if(index === -1) return list;
let updatedList = update(list, {
[index]: {
todo: {$set: value}
}
})
return updatedList;
}
console.log(updateObj(todolist, 2, "updated value"))
console.log(updateObj(todolist, 3, "updated value"))
In your example replace this below with EDIT_TODO case and use the above updateObj utility
case EDIT_TODO:
console.log('edit todo called')
console.log(payload.todo.id)
return updateObj(state, payload.todo.id, payload.todo.todo);
Here is a link to the sample project I have created. Though it doesn't have reducers implementation but then updating the object works as per your requirement using immutability-helpers update method.
Hope this helps.
I am trying to create a sort button which when clicked will sort me menu cards alphabetically. My question is how should I have the sort function coded in the Reducer and Actions? I added pseudo-code for sorting in the Reducer as well. When I click the button I am getting "(TypeError): state.slice is not a function".
Edit:
Added my button component and main Container.
Actions:
export const sortMenus = () => {
return dispatch => {
dispatch({ type: "LOADING_MENUS" });
fetch(`/api/menus`)
.then(res => res.json())
.then(responseJSON => {
dispatch({ type: "SORT_MENUS", cards: responseJSON });
});
};
};
Reducer:
export default function MenusReducer(
state = {
cards: [],
loading: false
},
action
) {
switch (action.type) {
case "LOADING_MENUS":
return {
...state
};
case "ADD_MENUS":
return {
...state,
cards: action.cards
};
case "SORT_MENUS":
return state.slice().sort(function(menu1, menu2) {
if (menu1.name < menu2.name) return -1;
if (menu1.name < menu2.name) return 1;
return 0;
});
default:
return state;
}
}
Button Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { sortMenus } from ".././actions/dataActions";
import Row from "react-bootstrap/Row";
import Container from "react-bootstrap/Container";
class SortButton extends Component {
constructor() {
super();
this.state = { menus: [] };
}
handleMenuSort = e => {
this.props.sortMenus()
};
render() {
return (
<Container>
<Row>
<div>
<button id="sort-button" title="Sort Menus" onClick= {this.handleMenuSort}>Sort Menus</button>
</div>
</Row>
</Container>
)
}
}
const mapStateToProps = state => {
return {
menus: state.menus
}
};
const mapDispatchToProps = dispatch => {
return {
sortMenus: params => dispatch(sortMenus(params)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(SortButton)
Container:
class MainContainer extends Component {
displayCards = () => {
switch(this.props.path) {
case "menus":
return (this.props.menus.cards.map(card => (
<NavLink style={{ color: "black" }} to={`/menus/${card.id}`} key={card.id}><MenuCard view={this.props.displayObject} info={card} /></NavLink>
)));
default:
return (<div>Empty</div>)
}
};
render() {
return (
<CardColumns>
{this.displayCards()}
</CardColumns>
)
}
}
const mapStateToProps = state => {
return {
menus: state.menus
}
};
const mapDispatchToProps = dispatch => {
return {
displayObject: (id, category, type) => dispatch(displayObject(id, category, type)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(MainContainer)
Your state is an object, not an array. You likely mean to sort the stored cards array.
state.cards.slice(... instead of state.slice(...
case "SORT_MENUS":
return state.cards.slice().sort(function(menu1, menu2) {
if (menu1.name < menu2.name) return -1;
if (menu1.name < menu2.name) return 1;
return 0;
});
Side note: You may also want to clear/set your loading state upon successful data fetching. ;)
EDIT
You are mapping undefined state within mapStateToProps, then mapping over it in the component. Change mapStateToProps to access the correct defined property.
const mapStateToProps = state => ({
cards: state.cards,
});
Then you can iterate over the new cards prop.
case "menus":
return (this.props.cards.map(card => (
<NavLink
style={{ color: "black" }}
to={`/menus/${card.id}`}
key={card.id}
>
<MenuCard view={this.props.displayObject} info={card} />
</NavLink>
)));
You can simply store the fetched menu in application state.
You can have standalone action say SORT_MENU_BY_ALPHABET.
You can simply dispatch this action on button handler as well as on Ajax success. this dispatch may not have any payload associated.
hope it helps.
in reducer you defined state as object and you're trying to do array operation on it. state.slice().
slice is a function available for arrays. so its throwing error.
you should be doing
state.cards.slice().sort((a,b)=> a-b)
I have a React app that displays a grid of cars that I'd like to be able to sort on the click of a button.
While I have this.setState implemented towards the end of the sortAlphabetically function, it isn't being reflected on the web page.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import CarCard from '../components/CarCard';
import CarForm from './CarForm';
import './Cars.css';
import { getCars } from '../actions/cars';
import { sortCar } from '../actions/cars';
Component.defaultProps = {
cars: { cars: [] }
}
class Cars extends Component {
constructor(props) {
super(props)
this.state = {
cars: [],
sortedCars: []
};
}
sortAlphabetically = () => {
console.log("sort button clicked")
const newArray = [].concat(this.props.cars.cars)
const orgArray = newArray.sort(function (a,b) {
var nameA = a.name.toUpperCase();
var nameB = b.name.toUpperCase();
if (nameA < nameB) {
return -1;
} else if (nameA > nameB) {
return 1;
}
return 0;
}, () => this.setState({ cars: orgArray }))
console.log(orgArray)
this.props.sortCar(orgArray);
}
componentDidMount() {
this.props.getCars()
this.setState({cars: this.props.cars})
}
render() {
return (
<div className="CarsContainer">
<h3>Cars Container</h3>
<button onClick={this.sortAlphabetically}>Sort</button>
{this.props.cars.cars && this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
{/* {this.state.cars.cars && this.state.cars.cars.map(car => <CarCard key={car.id} car={car} />)} */}
<CarForm />
</div>
);
}
}
const mapStateToProps = (state) => {
return ({
cars: state.cars
})
}
const mapDispatchToProps = (dispatch) => {
return {
sortCar: (cars) => dispatch(sortCar(cars)),
getCars: (cars) => dispatch(getCars(cars))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cars);
Looking in the console, the orgArray does in fact alphabetize the array. The problem is, the UI isn't updating because of some disconnect between props and state.
Why is mapStateToProps not bridging the two together so that a change in the array re-runs render?
If you want to see updated changes, you have to read "cars" from the state instead of reading it from the props.
{this.state.cars && this.state.cars.map(car => <CarCard key={car.id} car={car} />)}
I'm trying to set Loader when data is not fetched yet. This scenario would be easy if the data would be uploaded only ones (logic here: set a flag is isFetching to true, when receiving from redux set it to false). But my scenario is a bit different. I'd like to get my data multiple times to update my Calendar component. All is done thru redux with axios package.
It looks like that:
My reducer adds isFetching flag when my axios request is done(the store is updated):
import { ACTIVE_MONTH } from "../actions/types";
export default function(state = null, action){
switch(action.type){
case ACTIVE_MONTH:
return Object.assign({}, state, {
isFetching: false,
fullyBooked: action.payload
})
default:
return state;
}
}
And the component looks like that:
import React, { Component } from 'react';
import Calendar from 'react-calendar';
import ChooseHour from './ChooseHour';
import { connect } from 'react-redux';
import * as actions from '../actions';
class Calendario extends Component {
state = { showHours: false, disabledDates: null}
componentDidMount() {
const { chosenRoom } = this.props;
const date = new Date();
const reqMonth = date.getMonth() + 1;
const reqYear = date.getFullYear();
this.props.activeMonthYearToPass({reqMonth, reqYear, chosenRoom});
}
onChange = date => this.setState({ date }, () => {
const { chosenRoom, isBirthday } = this.props;
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const fullDate = `${year}/${month}/${day}`;
const roomAndDayObj = {fullDate, chosenRoom, isBirthday};
this.props.sendRoomAndDay(roomAndDayObj);
}
);
onClickDay(e) {
const { chosenRoom } = this.props;
!chosenRoom ? this.setState({ errorMsg: "Wybierz pokój", showHours: false}) :
this.setState({ showHours: true, errorMsg:'' });
}
passActiveDate(activeDate) {
const { chosenRoom } = this.props;
const reqMonth = activeDate.getMonth() + 1;
const reqYear = activeDate.getFullYear();
this.setState({ pending: true},
() => this.props.activeMonthYearToPass({reqMonth, reqYear, chosenRoom})
);
this.props.passDateDetails({reqMonth, reqYear});
}
render() {
const { fullyBookedDays, isBirthday } = this.props;
const { errorMsg, pending } = this.state;
return (
<div>
<div className="calendarsCont">
<Calendar
onChange={this.onChange}
onClickDay={(e) => this.onClickDay(e)}
onActiveDateChange={({ activeStartDate }) => this.passActiveDate(activeStartDate)}
value={this.state.date}
locale="pl-PL"
tileDisabled={({date, view}) =>
(view === 'month') &&
fullyBookedDays && fullyBookedDays.fullyBooked.some(item =>
date.getFullYear() === new Date(item).getFullYear() &&
date.getMonth() === new Date(item).getMonth() -1 &&
date.getDate() === new Date(item).getDate()
)}
/>
}
</div>
<p style={{color: 'red'}}>{errorMsg}</p>
<div>
{this.state.showHours ?
<ChooseHour chosenDay={this.state.date} chosenRoom={this.props.chosenRoom} isBirthday={isBirthday}/> :
null}
</div>
</div>
)
}
}
function mapStateToProps({fullyBookedDays}){
return {
fullyBookedDays,
}
}
export default connect (mapStateToProps, actions)(Calendario);
So the new values will come many times from axios request.
What kind of strategy do you use in that case?
THANK YOU!
Whenever there is multiple fetching requests, or even multiple actions that indicates something async is happening and needs to be stored in a part of the state, I use a counter :
export default function(state = {fetchCount: 0}, action){
switch(action.type){
case FETCHING_THING:
return Object.assign({}, state, {
fetchCount: state.fetchCount + 1
})
case FETCHING_THING_DONE:
return Object.assign({}, state, {
fetchCount: state.fetchCount - 1,
fullyBooked: action.payload
}
default:
return state;
}
}
Then you can just check fetchCount > 0 in your mapstatetoprops.
function mapStateToProps({fullyBookedDays, fetchCount}){
return {
fullyBookedDays,
isLoading: fetchCount > 0
}
}
Please take below as an example , Redux-thunk style action is used for wrap multiple axios requests and dispatch them all.
//axios call2
function getData1() {
return axios.get('/data1');
}
//axios call2
function getData2() {
return axios.get('/data2');
}
//redux-thunk action creator
function getFullData() {
return (dispatch, getState) => {
axios.all([getData1(), getData2()])
.then(axios.spread(function (acct, perms) {
//call normal action creator
dispatch(fetchData1())
dispatch(fetchData2())
}));
};
}
//normal actioncreator
function fetchData1(data)
{
return {type: "FETCH_DATA1", payload: data}
}
//normal actioncreator
function fetchData2(data)
{
return {type: "FETCH_DATA2", payload: data}
}
//reducer1:
function reducer1 (state = defaultedState ,action){
return Object.assign({},{...state, data: action.payload, isFetching: false} )
}
//reducer2:
function reducer2 (state = defaultedState ,action){
return Object.assign({},{...state, data: action.payload, isFetching: false} )
}
//component:
mapStateToProps = function(state){
return {
data1: state.data1.data,
data2: state.data2.data,
isFetching1: state.data1.isFetching,
isFetching2: state.data2.isFetching
}
}
import React, { Component } from "react";
class MyComponent extends Component{
render(){
return (!data1 && isFetching1) || (!data2 && isFetching2) ? <Loading> : <DataComponent>
}
}
connect(mapStateToProps)(MyComponent)
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 };