React-Redux. Redux state changes but data not being refreshed on page - reactjs

I am new at Redux and quite incompetent with React in general. Sorry if this is a stupid question.
I have a simple table with data. I clicked on header and data at state sorted but this changes dont displayed on page. Data displayed only one time and not re-renders.
body
let OurBody = ({ filteredArr }) => {
return (
<tbody>
{filteredArr.map(element =>
<tr key={element._id}>
<td>{element.company}</td>
<td>{element.balance}</td>
<td>{element.registered}</td>
<td>{element.address.country}</td>
<td>{element.employers.length}</td>
<td>--</td>
</tr>
)}
</tbody>
)
}
const mapStateToProps = (state) => {
return {
filteredArr: getSortedCompanies(state.json, state.sortCompany)
};
}
export default connect(mapStateToProps)(OurBody);
header
let Headersort = ({ dispatch }) => (
<thead>
<tr>
<td onClick={() => {dispatch(sortCompany());}}>Company</td>
<td>Balance</td>
<td>Registered</td>
<td>Country</td>
<td>Number of employers</td>
<td>Show employers</td>
</tr>
</thead>
);
const mapStateToProps = (state) => ({
Arr: state.json,
})
export default connect(mapStateToProps)(Headersort);
App component
class App extends React.Component {
componentDidMount() {
this.props.dispatch(fetchPosts());
}
render() {
const { loading, error } = this.props;
if (error) {
return <div>Error! {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<table>
<Headersort />
<OurBody />
</table>
</div>
)
}
}
const mapStateToProps = state => ({
loading: state.loading,
error: state.error
})
App = connect(mapStateToProps)(App)
export default App;
Reducer works correctly. Every time I clicked data at store changes.
No errors displayed.
Code at github https://github.com/DrGreenNow/React-Redux-Table

Since you are using sort to sort the data, it sorts the data at original reference and when it updates, the data returned by mapStateToProps has the same reference. Now when connect accepts this data from mapStateToProps, it doesn't trigger a re-render because according to it the data hasn't changed.
Making a clone of data while sorting works.
//getSortedCompanies
export default (json, sortCompany) => {
if (sortCompany === null) {
console.log(sortCompany);
return json;
} else if (sortCompany) {
console.log(sortCompany);
return [...json].sort((a, b) => {
if (a.company.toLowerCase() < b.company.toLowerCase()) {
return 1;
} else {
return -1;
}
});
} else {
return [...json].sort((a, b) => {
if (a.company.toLowerCase() > b.company.toLowerCase()) {
return 1;
} else {
return -1;
}
});
}
};
WOrking demo

Related

React and state

I would like your take on a specific implementation. I have a react app (no redux), the app has a shopping cart. The shopping cart is defined in the state in the App component and it is passed and used further down the tree in several components. E.g. I have a component called ShoppingCart, it displays the shopping cart, plus it has actions to add/remove/clear the cart.
My problem is updating the shopping cart state after performing an action on the shopping cart. E.g. when I call a function to clear the shopping cart, the state should be updated in the App component thus updating my component which is further down the tree. How would one implement these action functions (without redux)?
Code:
const App = () => {
const [cart, setCart] = useState({ lines: [], total: 0 });
return <ShoppingCart cart={cart} />;
}
const ShoppingCart = ({ cart }) => {
const onAddOne = l => {
// not sure how to update cart and update state
}
const onRemoveOne = l => {
// not sure how to update cart and update state
}
return (
<table>
{
cart.lines.map(l => <tr><td>{l.name}</td><td><button onClick={() => onAddOne(l)}>+</button><button onClick={() => onRemoveOne(l)}>-</button></td></tr>)
}
</table>
);
}
Thanks in advance for any tip.
Here you can use the useContext hook.
The idea is similar to redux.
So, what you can do is, first create a StateProvider, like in the example
import React, { createContext, useReducer, useContext } from "react";
export const StateContext = createContext();
export const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext);
Similarly, create a Reducer for that, you can add more reducers, the example shown is to ADD ITEMS IN BASKET and REMOVE ITEMs FROM BASKET
export const initialState = {
basket: [],
user: null,
};
export const getBasketTotal = (basket) =>
basket?.reduce((amount, item) => item.price + amount, 0);
function reducer(state, action) {
switch (action.type) {
case "ADD_TO_BASKET":
return { ...state, basket: [...state.basket, action.item] };
case "REMOVE_ITEM":
let newBasket = [...state.basket];
const index = state.basket.findIndex(
(basketItem) => basketItem.id === action.id
);
if (index >= 0) {
newBasket.splice(index, 1);
} else {
console.warn("Cant do this");
}
return { ...state, basket: newBasket };
default:
return state;
}
}
export default reducer;
Go to your index.js file and wrap your file like this
<StateProvider initialState={initialState} reducer={reducer}>
<App />
</StateProvider>
And voila, while adding items to the basket use following code
const addtobasket = () => {
dispatch({
type: "ADD_TO_BASKET",
item: {
id: id,
title: title,
price: price,
rating: rating,
color: color,
},
});
};
I found a solution, however, I am not sure it is the correct way to do things:
const App = () => {
const onUpdateCart = (cart) => {
setCart({ ...cart });
}
const [cart, setCart] = useState({ lines: [], total: 0, onUpdateCart });
return <ShoppingCart cart={cart} />;
}
const ShoppingCart = ({ cart }) => {
const onRemoveLine = l => {
cart.lines = cart.lines.filter(l2 => l2 !== l);
cart.onUpdateCart(cart);
}
const onAddOne = l => {
l.amount++;
cart.onUpdateCart(cart);
}
const onRemoveOne = l => {
l.amount--;
cart.onUpdateCart(cart);
}
return (
<table>
{
cart.lines.map(l => (
<tr>
<td>{l.name}</td>
<td>
<button onClick={() => onAddOne(l)}>+</button>
<button onClick={() => onRemoveOne(l)}>-</button>
<button onClick={() => onRemoveLine(l)}>x</button>
</td>
</tr>)
)
}
</table>
);
};
The straight forward way to implement this is to pass down props to the child component that when called update the state.
Notice how all state business logic is in a central place .e.g in App component. This allows ShoppingCart to be a much simpler.
const App = () => {
const [cart, setCart] = useState({ lines: [], total: 0 });
const updateLineAmount = (lineIdx, amount) => {
// update the amount on a specific line index
setCart((state) => ({
...state,
lines: state.lines.map((line, idx) => {
if (idx !== lineIdx) {
return line;
}
return {
...line,
amount: line.amount + amount,
};
}),
}));
};
const onAddOne = (lineIdx) => {
updateLineAmount(lineIdx, 1);
};
const onRemoveOne = (lineIdx) => {
updateLineAmount(lineIdx, -1);
};
return (
<ShoppingCart cart={cart} onAddOne={onAddOne} onRemoveOne={onRemoveOne} />
);
};
const ShoppingCart = ({ cart, onAddOne, onRemoveOne }) => {
return (
<table>
{cart.lines.map((line, idx) => (
<tr key={idx}>
<td>{line.name}</td>
<td>
<button onClick={() => onAddOne(idx)}>+</button>
<button onClick={() => onRemoveOne(idx)}>-</button>
</td>
</tr>
))}
</table>
);
};

how can I set the reducer's return value to state object in component in react js

I have one component as below. I am calling on api on its componentDidMount() event. I am not getting why am I not getting its prop value first time when component renders. Also I am not sure why component is rendering 2 times. I have below code.
import React, { Component } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import AgmtTable from "./AgmtTable";
import * as AgmtAction from "../redux/actions/AgmtAction";
class AgmtContainer extends Component {
constructor(props) {
super(props);
this.state = {};
}
fetch Agmt details.
componentDidMount() {
this.props.dispatch(
AgmtAction.getAgmtsForCustomer(
this.props.match.params.custID,
this.props.match.params.source,
this.props.token
)
);
console.log("componentDidMount", this.props.Agmts);
}
getHeaader = () => {
var tableHeadings = [
"Agmt ID",
"Start Date",
"End Date",
];
return tableHeadings.map((key) => {
return <th key={key}> {key.toUpperCase()}</th>;
});
};
getRowsData = () => {
console.log("in row data", this.props.Agmts);//here I cant see a data though its present in mapStateToProps() function. I am getting error as this.props.agreements.map is not a function.
if (this.props.Agmts) {
return this.props.Agmts.map((value) => {
const {
Agmt_ID,
Agmt_START_DATE,
End_DATE,
} = value;
return (
<tr key={Agmt_ID} className="clickable-row active">
<td> {Agmt_ID} </td>
<td> {Agmt_START_DATE} </td>
<td> {End_DATE} </td>
</tr>
);
});
}
};
render() {
return (
<React.Fragment>
<div>
<table
id="display-table"
className="table table-bordered table-hover table-responsive table-condensed table-striped table-sm"
>
<tbody>
<tr>{this.getHeaader()}</tr>
{this.getRowsData()}
</tbody>
</table>
</div>
</React.Fragment>
);
}
}
function mapStateToProps(state) {
return {
Agmts: state.AgmtsDetails.AgmtsData,//here I have a data
token: state.login.userDetails.token,
};
}
export default connect(mapStateToProps)(AgmtContainer);
Also how can I use the mapStateToProps values to set in state object. When I am running above code I am getting error as this.props.agmts.map is not a function
The dispatch is asynchronous, so you either need to watch for result to be updated in your Redux store with componentDidUpdate or directly return the result from the reducer.
When you get the result, you can manipulate it and store it in local state to reference in your render. Note that unless you need to reference the result in another component somewhere, then you don't need to store it in Redux, you can handle it all in within the component.
Subscribing to the store with componentDidUpdate:
componentDidMount() {
this.props.dispatch(
AgmtAction.getAgmtsForCustomer(
this.props.match.params.custID,
this.props.match.params.source,
this.props.token
)
);
}
componentDidUpdate(prevProps) {
if (JSON.stringify(prevProps.Agmts) !== JSON.stringify(this.props.Agmts)) {
// this is the result of the dispatch
console.log(this.props.Agmts);
}
}
Returning the result directly back:
// in your AgmtAction.getAgmtsForCustomer action
export const getAgmtsForCustomer = () => (dispatch, getState) => {
return axios
.get(..........
.then((res) => {
dispatch(..........
return res.data;
})
.catch((err) => {
...
});
};
// in your `AgmtContainer` component
...
componentDidMount() {
this.props.dispatch(
AgmtAction.getAgmtsForCustomer(
this.props.match.params.custID,
this.props.match.params.source,
this.props.token
)
).then((res) => {
// this is the result of the dispatch
console.log(res);
});
}
In getRowsData function where you are getting error "map is not a function" is due to the data you are getting in this.props.Agmts must be an object type. (Object encloses in curly brackets {}).
You can apply map function only on array not on an object. (Array encloses in square brackets [])

Sorting Data in Reducer Actions

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)

Reusable React component with same actions & reducers

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.

React Drag n Drop PUT request after props

I am building an application that requires a table of items to be sorted and change the orderNumber of them depending on their sorting. I installed and utilized a library called react-dnd to handle the functionality of sorting/ordering, and its working great so far. The issue im having is the update. When a user moves one of the items, I need to send a PUT request to the api and update its orderNumber. It was working last night great, here is my code.
The ListItem (Item that is being sorted and updated):
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import {DragSource, DropTarget} from 'react-dnd';
import sdk from '../../js/sdk';
import ItemTypes from './ItemTypes';
const itemSource = {
beginDrag(props) {
return {id: props.id};
}
};
const itemTarget = {
hover(props, monitor) {
const draggedId = monitor.getItem().id;
if (draggedId !== props.id) {
props.swapItems(draggedId, props.id);
}
}
};
const DragSourceDecorator = DragSource(ItemTypes.ITEM, itemSource, (connect, monitor) => {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
};
});
const DropTargetDecorator = DropTarget(ItemTypes.ITEM, itemTarget, (connect) => {
return {connectDropTarget: connect.dropTarget()};
});
class SwagBagItem extends React.Component {
constructor(props) {
super(props);
this._handleDelete = this._handleDelete.bind(this);
}
componentWillReceiveProps(nextProps) {
const swagbagItemCpy = Object.assign({}, nextProps.swagbagItem);
delete swagbagItemCpy.id;
if (nextProps) {
sdk.put(`swagbags/${nextProps.swagbag.id}/items/${nextProps.swagbagItem.id}`, swagbagItemCpy)
.done((result) => {
console.log(result);
}).fail((error) => {
console.log(error);
})
;
}
}
_handleDelete(event) {
event.preventDefault();
event.stopPropagation();
if (confirm('Are you sure you want to delete this Swagbag Item?')) {
sdk.delete(`swagbags/${this.props.swagbag.id}/items/${this.props.swagbagItem.id}`)
.done(() => {
console.log('Swagbag Item remove!');
}).then(() => {
this.props.loadSwagBags();
});
}
}
render() {
const {swagbagItem} = this.props;
return this.props.connectDragSource(this.props.connectDropTarget(
<tr className="swagbag-item">
<td>{swagbagItem.id}</td>
<td><Link to={`${this.props.swagbag.id}/items/${swagbagItem.id}`}>{swagbagItem.name}</Link></td>
<td>{swagbagItem.uri}</td>
<td>
<div className="btn-group btn-group-xs pull-right" role="group">
<Link to={`${this.props.swagbag.id}/items/${swagbagItem.id}/edit`} className="btn btn-info">Edit</Link>
<Link to={`${this.props.swagbag.id}/items/${swagbagItem.id}`} className="btn btn-info">View</Link>
<button className="btn btn-danger btn-xs" onClick={this._handleDelete}>Remove</button>
</div>
</td>
</tr>
));
}
}
SwagBagItem.propTypes = {
loadSwagBags: PropTypes.func,
params: PropTypes.object,
swagbag: PropTypes.object,
swagbagItem: PropTypes.object,
};
export default DropTargetDecorator(DragSourceDecorator(SwagBagItem));
The container or list that holds these items:
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import {DragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import sdk from '../../js/sdk';
import Nav from '../Nav';
import SwagBagItem from '../SwagBagItem';
class SwagBagItemsList extends React.Component {
constructor(props) {
super(props);
this.state = {
swagbag: null,
swagbagItems: [],
};
this._loadSwagBags = this._loadSwagBags.bind(this);
this._compareItems = this._compareItems.bind(this);
this._swapItems = this._swapItems.bind(this);
}
componentWillMount() {
this._loadSwagBags();
}
_compareItems(item1, item2) {
return item1.orderNumber - item2.orderNumber;
}
_swapItems(itemNo1, itemNo2) {
const items = this.state.swagbagItems;
let item1 = items.filter(item => item.id === itemNo1)[0];
let item2 = items.filter(item => item.id === itemNo2)[0];
let item1Order = item1.orderNumber;
item1.orderNumber = item2.orderNumber;
item2.orderNumber = item1Order;
items.sort(this._compareItems);
this.setState({swagbagItems: items});
}
_loadSwagBags() {
sdk.getJSON(`swagbags/${this.props.params.id}`)
.done((result) => {
this.setState({swagbag: result});
})
.then(() => {
sdk.getJSON(`swagbags/${this.props.params.id}/items?fields=id,name,summary,uri,itemImageFile,orderNumber`).done((results) => {
this.setState({swagbagItems: results});
});
});
}
render() {
let swagbagItems = null;
if (this.state.swagbagItems) {
swagbagItems = this.state.swagbagItems.map((item) => {
return <SwagBagItem
loadSwagBags={this._loadSwagBags}
swagbag={this.state.swagbag}
swagbagItem={item}
key={item.id}
id={item.id}
swapItems={this._swapItems}
/>;
});
}
if (!this.state.swagbag) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Swagbag Items</h1>
<Nav swagbag={this.state.swagbag} />
<table className="table">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>uri</th>
<th></th>
</tr>
</thead>
<tbody>
{swagbagItems}
</tbody>
</table>
<Link to={`swagbags/createItem/swagbagid/${this.state.swagbag.id}`} className="btn btn-success">Add Item</Link>
</div>
);
}
}
SwagBagItemsList.propTypes = {
params: PropTypes.object,
};
export default DragDropContext(HTML5Backend)(SwagBagItemsList);
It is making the PUT request, but its making hundreds of them in a row from just moving one object. I cant for the life of me figure out why. This puts a severe lag on the application and makes it unresponsive. Am I going about this the right way, and if so, what is the solution to this?
EDIT #1: Woke up today and the application is working fine. Unfortunately this is going in production, so before that I have to recreate the bug of 800+ PUT requests and figure it out. Might put a bounty on this.
If you want to get it so that it sends the update once it's finished dragging, there's an endDrag function you can add to your DragSource (http://gaearon.github.io/react-dnd/docs-drag-source.html) that will only be fired once and will only be fired upon finishing the drag. So if you remove your api call from componentWillReceiveProps and move it to the source like this:
const itemSource = {
beginDrag(props) {
return {
id: props.id,
swagbagId: props.swagbag.id,
swagbagItem: props.swagbagItem,
};
},
endDrag(props, monitor) {
const item = monitor.getItem();
sdk.put(`swagbags/${item.swagbagId}/items/${item.swagbagItem.id}`, item.swagbagItem)
.done((result) => {
console.log(result);
}).fail((error) => {
console.log(error);
})
;
},
};
It should only make the call one time (I can't perfectly predict that without knowing what's in swagbag and swagbagItem but I think it should). Note that I'm using the getItem() function from the DragSource monitor (http://gaearon.github.io/react-dnd/docs-drag-source-monitor.html) to retrieve what was passed in upon beginDrag.

Resources