I'm stating to learn react and redux so i think there are many things that i don't know.
I have a problem with missed re-rendering component on store changes.
This is my project structure: https://i.stack.imgur.com/tJJSg.png
And here is my code:
App.js:
class App extends Component {
render() {
return (
<div className="App">
<Nav sortByDate={()=>{this.props.sortBy(SORT_BY_DATE)}} sortByLikes={()=>{this.props.sortBy(SORT_BY_LIKES)}} />
<Items comments={this.props.comments} getList={()=>{this.props.sortBy(GET_LIST)}}/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
comments: state.comments
};
};
const mapDispatchToProps = (dispatch) => {
return {
sortBy: (action) => {
dispatch(sortBy(action));
}
};
};
export default connect (mapStateToProps, mapDispatchToProps) (App);
CommentList.js:
class ListItems extends Component {
constructor(props){
super(props);
this.state = {
comments: props.comments
};
}
componentWillMount() {
this.props.getList();
}
componentWillReceiveProps(nextProps) {
if (this.props.comments !== nextProps.comments) {
this.setState({
comments: nextProps.comments
});
}
}
getComments() {
return (this.state.comments.map(function (object) {
return <Item numLikes={object.num_like} id={object.id} comment={object.comment} date={object.date}
sender={object.sender}/>
}));
}
render() {
return (
<Container>
<Row>
<Col lg={2} md={1} xs={0}/>
<Col lg={8} md={10} xs={12}>
{this.getComments()}
</Col>
<Col lg={2} md={16} xs={0}/>
</Row>
</Container>
);
}
}
export default ListItems;
Reducers.js:
const listReducer = (state = {comments: []}, action) => {
function toDate(dateStr) {
const [day, month, year] = dateStr.split("/")
return new Date(year, month - 1, day)
}
function commentSortedByDate(comments) {
const sorted = comments.sort(function(a, b) {
return toDate(b.date) - toDate(a.date);
})
return sorted;
}
function commentSortedByLikes(comments) {
const sorted = comments.sort(function(a, b) {
return parseInt(b.num_like) - parseInt(a.num_like);
})
return sorted;
}
switch (action.type) {
case SORT_BY_DATE:
console.log("sort by date");
state={
comments: commentSortedByDate(state.comments)
}
break;
case SORT_BY_LIKES:
console.log("sort by likes");
state={
comments: commentSortedByLikes(state.comments)
}
break;
case GET_LIST:
state = {
comments: action.payload
}
break;
}
return state;
};
export default listReducer;
The problem is certainly with this two components.
I have 3 actions:
GET_LIST (in a middleware call a rest service getting the json of comments and update the store).
SORT_BY_DATE (in the reducer sort the array of comments by date and update the store).
SORT_BY_LIKES (same).
The comments in the store are effectively sorted.
First of all the app dispatch automatically the GET_LIST action and it works, pass the props with comments correctly to the CommentList.js component and successfully render the list of CommentItem.
Now the problem:
The click of a button in the Navbar component will dispatch a SORT_BY action that updates the store and finally calls the MapStateToProps function in App, but this time the CommentList stay the same and componentWillReceiveProps is not called.
Why? Can anyone help me?
You are mutating the state (sort function) instead of creating a new Array in your reducer. This prevents the component from re-rendering as it is not notified of a change. To fix it you could make your functions pure:
function commentSortedByDate(comments) {
const copy = [...comments];
copy.sort(function(a, b) {
return toDate(b.date) - toDate(a.date);
})
return copy;
}
function commentSortedByLikes(comments) {
const copy = [...comments];
copy.sort(function(a, b) {
return parseInt(b.num_like) - parseInt(a.num_like);
})
return copy;
}
This way you return a new array instead of old one (with sorted elements).
Related
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 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()
})
}
I'm quite new to React and have searched through so many StackOverflow responses but with no joy. So when an array of employees is passed to a reducer and then a component, the prop is empty the first time and then contains data on subsequent renders. Does anyone know how to prevent react rendering until the employees props contains data?
--------Update
So i added isloading to the reducer initial state and tried to send it to the component but i receive a really horrible error
TypeError: In this environment the sources for assign MUST be an object. This error is a performance optimization and not spec compliant
this is my updated case statement
case EMPLOYEES_FETCH_SUCCESS:
return {
list: action.payload,
isloading: false
}
New error message
Many thanks
the data array looks like this when it is populated
data structure
Reducer code:
import { EMPLOYEES_FETCH_SUCCESS, USER_ADD } from "../actions/types";
const INITIAL_STATE = {};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case EMPLOYEES_FETCH_SUCCESS:
return action.payload;
case USER_ADD:
return state;
default:
return state;
}
};
this is my action dispatch statement
dispatch({ type: EMPLOYEES_FETCH_SUCCESS, payload: returnArray });
Component code:
componentWillMount() {
this.props.employeesFetch();
this.createDataSource(this.props);
}
componentWillReceiveProps(nextProps) {
this.createDataSource(nextProps);
}
createDataSource({ employees }) {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.dataSource = ds.cloneWithRows(employees);
}
onButtonPress() {
Actions.GroupChatContainer(); //Need to send bar's chat record
}
renderRow(employee) {
return <ListItem employee={employee} />;
}
render() {
console.log(this.props.employees);
return (
<View>
<ListView
style={styles.listStyle}
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
/>
<Button onPress={this.onButtonPress.bind(this)}>Group chat</Button>
</View>
);
}
}
const mapStateToProps = state => {
console.log(state.employees);
const employees = _.map(state.employees, (val, uid) => {
return { ...val, uid };
});
return { employees };
};
Does anyone know how to prevent react rendering until the employees
props contain data?
Sure, just do a check and return null:
render() {
if (!this.props.employees) {
return null;
}
return (
<View>
<ListView
style={styles.listStyle}
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
/>
<Button onPress={this.onButtonPress.bind(this)}>Group chat</Button>
</View>
);
}
Or inside JSX:
render() {
return (
<View>
{this.props.employees &&
<ListView
style={styles.listStyle}
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
/>
}
<Button onPress={this.onButtonPress.bind(this)}>Group chat</Button>
</View>
);
}
You can have two properties for your store. One for the list of employees and another for the loading state.
import { EMPLOYEES_FETCH_SUCCESS, USER_ADD } from "../actions/types";
const INITIAL_STATE = {
list: [],
isLoading: true,
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case EMPLOYEES_FETCH_SUCCESS:
return {
list: action.payload,
isLoading: false,
}
default:
return state;
}
};
And in the render if the state is loading return loading view
render() {
if (!this.props.employees.isLoading) {
return <div>Loading...</div>;
}
...
...
}
I want to reuse a react component and share common actions & reducers. My app dashboard has 3 Lists, where each List is fetched with different query param.
All 3 List components have the same props because all 3 of them are being re-rendered once I receive props from reducer.
Is there an dynamic way to display Lists based on query parameter? What I was thinking is to call different reducer in the action file based on the query param. Is there a better way?
Dashboard.js
const Dashboard = () => {
return(
<div>
<List query={QUERY1} />
<List query={QUERY2} />
<List query={QUERY3} />
</div>
)
}
List.js
class List extends Component {
constructor(props) {
super(props);
this.state = {
items: []
};
}
componentWillMount() {
const { query } = this.props;
this.props.onLoad(query);
}
componentWillReceiveProps() {
const { items } = this.props;
this.setState({ items });
}
render() {
return (
<div>
{
this.state.items.map((item, index) =>
<Item data={item} key={index}/>
)
}
</div>
)
}
}
function mapStateToProps(state) {
const { items } = state.item;
return {
items
}
}
function mapDispatchToProps(dispatch) {
return {
onLoad: bindActionCreators(actions.load, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(List);
action.js
export function load(query) {
return function (dispatch) {
fetch(`//api.example.com/list?type=${query}&limit=10`)
.then((response) => response.json())
.then((data) => {
dispatch(setItems(data));
});
};
}
reducer.js
export default function(state = [], action) {
switch (action.type) {
case actionTypes.ITEMS_SET:
return setItems(state, action);
}
return state;
}
function setItems(state, action) {
const { items } = action;
return { ...state, items };
}
Note I am a contributor on redux-subpace
redux-subspace came around to solve this problem of having the same component displayed on the page, without crossing over the store values.
It has a feature called namespacing that will allow you to isolate your load actions and components from each other.
const Dashboard = () => {
return(
<div>
<SubspaceProvider mapState={state => state.list1}, namespace='list1'>
<List query={QUERY1} />
</SubspaceProvider>
<SubspaceProvider mapState={state => state.list2}, namespace='list'>
<List query={QUERY2} />
</SubspaceProvider>
<SubspaceProvider mapState={state => state.list3}, namespace='list3'>
<List query={QUERY3} />
</SubspaceProvider>
</div>
)
}
You'll also need to namespace your reducers, you can see how to do that here.
I would like to ask if how to dispatch or catch the data in mapStateToProps if data that I want to get is in a nested state and the identifier would be the this.props.group that is passed in FilmList via the Parent Component.
// Parent Component
<Row>
<FilmList group="upcoming" groupTitle="Upcoming Movies" />
<FilmList group="top_rated" groupTitle="Top Rated Movies" />
</Row>
// Child Component
class FilmList extends React.Component {
constructor(props){
super(props);
}
componentDidMount(){
this.props.getMovieByGroup(this.props.group);
}
renderFilmItem(){
if(this.props.data){
var film = this.props.data.upcoming.slice(0,6).map((item) => {
return <FilmItem key={item.id} film={item} />
});
return film;
}
}
render(){
console.log('new');
return(
<div className={styles.filmContainer}>
<h1>{ this.props.groupTitle }</h1>
{ this.renderFilmItem() }
</div>
);
}
}
function mapStateToProps(state){
return {
data: state.film.data.upcoming
}
}
This is what my state looks like:
This is my reducer:
const INITIAL_STATE = {
data: {},
error: {},
};
function processData(initialData, data) {
let updated = initialData;
updated[data.group] = data.results;
return updated;
}
export default (state = INITIAL_STATE, action) => {
switch(action.type) {
case GET_FILM_SUCCESS: {
return Object.assign({}, state.data[action.data.group], {
data: processData(state.data,action.data)
});
}
case GET_FILM_FAILURE: {
return { ...state, error: action.data }
}
}
return state;
}
Currently in my mapStateToProps I only access state.film.data.upcoming what I want to achieve is like state.film.data.{this.props.group} somewhere along that code so it will re render the component when "top_rated" or "upcoming" data state change.
So if state.file.data.upcoming is working fine, then you should be able to use state.file.data in mapStateToProps then do state.file.data[this.props.group] in your component.