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>;
}
...
...
}
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'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).
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.
I have an index.js which renders all tables in the database.
_renderAllTables() {
const { fetching } = this.props;
let content = false;
if(!fetching) {
content = (
<div className="tables-wrapper">
{::this._renderTables(this.props.allTables)}
</div>
);
}
return (
<section>
<header className="view-header">
<h3>All Tables</h3>
</header>
{content}
</section>
);
}
_renderTables(tables) {
return tables.map((table) => {
return <Table
key={table.id}
dispatch={this.props.dispatch}
{...table} />;
});
}
render() {
return (
<div className="view-container tables index">
{::this._renderAllTables()}
</div>
);
}
}
const mapStateToProps = (state) => (
state.tables);
export default connect(mapStateToProps)(HomeIndexView);
I changed mapStateToProps from above code to below code.
const mapStateToProps = (state) => ({
tables: state.tables,
currentOrder: state.currentOrder,
});
The reason why I changed code is that I want to use one of state of currentOrder. I have a state which shows whether table is busy or not. So in order to use that I added currentOrder in mapStateToProps. However, it triggers Uncaught TypeError: Cannot read property 'map' of undefined..
How can I use states from other components? any suggestions for that??
Thanks in advance..
--reducer.js
const initialState = {
allTables: [],
showForm: false,
fetching: true,
formErrors: null,
};
export default function reducer(state = initialState, action = {}) {
switch(action.type){
case Constants.TABLES_FETCHING:
return {...state, fetching: true};
case Constants.TABLES_RECEIVED:
return {...state, allTables: action.allTables, fetching: false};
case Constants.TABLES_SHOW_FORM:
return {...state, showForm: action.show};
case Constants.TALBES_RESET:
return initialState;
case Constants.ORDERS_CREATE_ERROR:
return { ...state, formErrors: action.errors };
default:
return state;
}
}
Your problem is that before fetching successfully tables, your component is rendered with state.tables is undefined.
First of all, best practice is to use selectors rather than json path like state.tables, to be in a separate selectors.js file using reselect lib as follow:
import { createSelector } from 'reselect';
const tablesSelector = state => state.tables;
export default {
tablesSelector,
}
Second, you need to add reducer for, let's assume, FETCH_ALL_TABLES action using combineReducers from redux lib and most important to initialize tables array with [] before the action is dispatched so as to be defined as follow:
import {combineReducers} from 'redux';
function tables(state = [], {type, payload}) {
switch (type) {
case 'FETCH_ALL_TABLES':
return [
...state,
...payload,
]
}
return state;
}
export default combineReducers({
tables,
});
and in your index.js, may be want update it to:
import selector from './selector';
...
function mapStateToProps(state) {
return {
tables: selector.tablesSelector(state),
}
}
You should always check if variable you want to map is defined.
_renderTables(tables) {
if (tables) {
return tables.map((table) => {
return <Table
key={table.id}
dispatch={this.props.dispatch}
{...table} />;
});
}
}