How do I update the horse listing after the add horse action is fully done?
I think that the reloadHorseList in CreateHorse is running before createHorse actions is completely done so sometimes I see new horse in list and sometimes not. A full reload shows an update always.
Horses Component
...
import { getHorses } from '../../actions';
import ListHorses from './ListHorses';
import CreateHorse from './forms/createHorseForm';
class Horses extends React.Component {
constructor(props) {
super(props);
this.state = {
...
};
this.reloadHorseList = this.reloadHorseList.bind(this);
}
componentDidMount() {
this.reloadHorseList();
}
reloadHorseList() {
this.props.getHorses(this.props.current_user.selected_stable);
}
render() {
return (
<div className="content page-content-wrapper1">
<CreateHorse
current_user={this.props.current_user}
reloadHorseList={this.reloadHorseList}
/>
<ListHorses
current_user={this.props.current_user}
horses={this.props.horses}
/>
</div>
);
}
}
function mapStateToProps(state) {
return {
horses: state.horses
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
getHorses: getHorses
},
dispatch
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Horses);
Create Horse Form
...
import { Field, reduxForm, getFormValues } from 'redux-form';
import {
createHorse,
getHorseSelect,
updateHorseCount
} from '../../../actions';
import { connect } from 'react-redux';
const renderField = (...
);
class CreateHorse extends Component {
constructor(props) {
super(props);
this.state = {
...
};
this.setMessage = this.setMessage.bind(this);
}
onSubmit(props) {
//let p = this.props.reloadHorseList;
try {
this.props.createHorse(props, this.setMessage);
//running before I have finished creating my horse
this.props.reloadHorseList();
} catch (err) {
...
}
}
render() {
const { handleSubmit } = this.props;
return (
<div>
...
{this.state.displayHorseCreateForm && (
<div>
<h4 className="header-content">Add Horse</h4>
<p> * required field</p>
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
// fields here
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
</div>
)}
</div>
);
}
}
function validate(values) {
...
}
function mapStateToProps(state) {
---
}
export default connect(mapStateToProps, {
createHorse,
getHorseSelect,
updateHorseCount
})(
reduxForm({
form: 'HorseCreatetForm',
initialValues: {
...
},
validate
})(CreateHorse)
);
//create horse action
export const createHorse = (props, setMessage) => async dispatch => {
try {
const request = await axios.post(`/api/horse/create`, props);
return {
type: CREATED_HORSE,
payload: request.data
};
} catch (err) {
...
}
};
ListHorses
...
import { deleteHorse } from '../../actions';
class HorsesList extends React.Component {
render() {
let horses = this.props.horses;
let horseCount = this.props.horse_count;
return (
<div className="content">
horse count: {horseCount}
<ul className="list-inline box-body">
{horseCount > 0 &&
horses.map((horse, key) => (
<li key={key}>
...//listing here
</li>
))}
</ul>
</div>
);
}
}
function mapStateToProps(state) {
return {
horse_count: state.horse_count
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
...
},
dispatch
);
}
export default connect(mapStateToProps, mapDispatchToProps)(HorsesList);
The solution that worked for me is to send a callback to the CreateHorse component to send to the createHorse action which runs Horse components action to getHorses.
class Horses extends React.Component {
constructor(props) {
super(props);
this.state = {
horses: this.props.horses,
};
this.reloadHorses = this.reloadHorses.bind(this);
}
componentDidMount(prevProps) {
this.props.getHorses(this.props.current_user.selected_stable);
}
reloadHorses = () => {
this.props.getHorses(this.props.current_user.selected_stable);
};
...
<CreateHorse
current_user={this.props.current_user}
reloadHorses={this.reloadHorses}
/>
<ListHorses
horses={this.props.horses}
/>
...
function mapStateToProps(state) {
return {
horses: state.horses
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
getHorses: getHorses
},
dispatch
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Horses);
then in CreateHorse component
onSubmit(props) {
this.props.createHorse(props, this.setMessage, this.props.reloadHorses);
}
}
Then in the createHorse action
export const createHorse = (
props,
setMessage,
reloadHorses
) => async dispatch => {
try {
const request = await axios.post(`/api/horse/create`, props);
reloadHorses();
return {
type: CREATED_HORSE,
payload: request.data
};
} catch (err) {
...
}
};
You should be posting real code at this point. To trigger a component re render you need to be changing it's state. I would recommend setting your props from redux into local state and render your list from that. You also will need to be using componentWillRecieveProps();
componentDidMount() {
this.reloadHorseList();
this.setState=({list: this.props.horseList});
}
componentWillRecieveProps(nextProps){
this.setState=({list: nextProps.horseList})
}
You are correct in your assumption that the component finishes loading first. So you need to utilize the componentWillRecieveProps lifecycle hook .
Alternatively, if you're using mapStateToProps() with redux your component should be rerendering when anything within mapStateToProps() changes.
Related
I am trying to learn Redux by simply add/delete users. I have an action 'ADD_PROFILE', with payload : name,account-number. On clicking add button, I wanted to update the store, hide the 'add user' form and show a message 'User added successfully'. If it is in React, I can have a boolean state variable, update/reset variable and switch the views. If I wanted to do the same using Redux bit not sure how.
This is what I tried :
Action
export const addProfile = (name, account_number) => {
console.log(name, account_number)
return{
type :'ADD_PROFILE',
payload : {
name : name,
account_number : account_number
}
};
}
Reducer:
export default (profilesList=[],action) => {
switch(action.type){
case 'ADD_PROFILE':
return [...profilesList, action.payload]
case 'DELETE_PROFILE':
return profilesList.filter(name => name!== action.payload.name)
default:
return profilesList;
}
}
AddView.js
import React from 'react';
import { connect } from 'react-redux';
import { addProfile } from '../actions';
class AddView extends React.Component{
constructor(props) {
super(props);
this.state={
isProfileAdded: false
};
}
addValuesView(){
return(
<div>
Name : <input type="text" value={this.props.profiles.name} ref={el => (this.nameInputRef = el)}/>
Account Number : <input type="text" value={this.props.profiles.account_number} ref={el => (this.accountInputRef = el)}/>
<button onClick={() => {
this.setState(isProfileAdded=true),
this.props.addProfile(this.nameInputRef.value,this.accountInputRef.value)
}
}>Add</button>
</div>
);
}
profileAddedView(){
return(
<div>Profile added succesfully</div>
)
}
view(){
return !this.props.profiles.isProfileAdded ? this.addValuesView() : this.profileAddedView()
}
render(){
console.log(this.state)
return this.view()
}
}
const mapStateToProps = (state) => {
return { profiles : state.profiles }
}
const mapDispatchToProps = dispatch => ({
onAddProfile: dispatch(addProfile())
});
export default connect(mapStateToProps, {addProfile}) (AddView);
App.js
import React from 'react';
import AddView from './AddView';
const App = () =>{
return (
<div className="ui container">
<AddView />
</div>
);
}
export default App;
Method this.setState should receive an object:
() => {
this.setState({ isProfileAdded: true});
this.props.addProfile(this.nameInputRef.value, this.accountInputRef.value);
}
I'm managing Todo lists in my app. The main view is a page with all the lists displayed as cards. If you click on one of them, you can modify, update, delete stuff through a modal that appears.
I have a TodoLists reducer that store all the TodoLists. I don't know how to handle the modal. Should I use redux or just local state?
import _ from "lodash";
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { listsActions } from "../duck";
import NewList from "./NewList";
import Card from "./Card";
import Modal from "./Modal";
class Lists extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false,
list: {}
};
this.hideModal = this.hideModal.bind(this);
this.renderModal = this.renderModal.bind(this);
}
componentDidMount() {
const { fetchByUserId, user } = this.props;
if (user !== undefined) {
fetchByUserId(user.id);
}
}
hideModal() {
this.setState({
modal: false
});
}
renderModal() {
this.setState({
modal: true
});
}
render() {
const { items } = this.props;
const { modal, list } = this.state;
return (
<div>
<NewProject />
<div className="columns">
{_.map(items, (l) => (
<div
key={l.id}
className="column"
>
<Card
list={l}
onClick={() => this.renderModal(l)}
/>
</div>
))}
</div>
<Modal
className={modal ? "is-active" : ""}
list={list}
onClose={this.hideModal}
/>
</div>
);
}
}
const mapStateToProps = (state) => {
const { user } = state.authentication;
const { items, loading, error } = state.lists;
return {
user,
items,
loading,
error
};
};
export default connect(
mapStateToProps,
{ fetchByUserId: listsActions.fetchByUserId }
)(Projects);
I am using react-redux.
I have the following JSX (only relevant snippets included):
getQuestionElement(question) {
if (question) {
return <MultiChoice questionContent={this.props.question.question} buttonClicked={this.choiceClicked} />
}
else {
return (
<div className="center-loader">
<Preloader size='big' />
</div>
)
}
}
render() {
return (
<div>
<Header />
{
this.getQuestionElement(this.props.question)
}
</div>
)
}
function mapStateToProps({ question }) {
return { question };
}
export default connect(mapStateToProps, questionAction)(App);
When the action fires, and the reducer updates the question prop
this.props.question
I expect
{this.getQuestionElement(this.props.question)}
to be reloaded and the new question rendered.
However this is not happening. Am I not able to put a function in this way to get it live reloaded?
My MultiChoice component:
import React, { Component } from 'react';
import ReactHtmlParser from 'react-html-parser';
import './questions.css';
class MultiChoice extends Component {
constructor(props) {
super(props);
this.state = {
question: this.props.questionContent.question,
answerArray : this.props.questionContent.answers,
information: null
}
this.buttonClick = this.buttonClick.bind(this);
}
createButtons(answerArray) {
var buttons = answerArray.map((element) =>
<span key={element._id} onClick={() => { this.buttonClick(element._id) }}
className={"span-button-wrapper-25 " + (element.active ? "active" : "")}>
<label>
<span>{element.answer}</span>
</label>
</span>
);
return buttons;
}
buttonClick(id) {
var informationElement;
this.props.buttonClicked(id);
var buttonArray = this.state.answerArray.map((element) => {
if (element._id === id ){
element.active = true;
informationElement = element.information;
return element;
}
else{
element.active = false;
return element;
}
});
this.setState({
answerArray: buttonArray,
information: informationElement
})
}
render() {
return (
<div className="question-container">
<div className="question-view">
<div className="icon-row">
<i className="fa fa-code" />
</div>
<div className="title-row">
{this.state.question}
</div>
<div className="button-row">
{this.createButtons(this.state.answerArray)}
</div>
<div className="information-row">
{ReactHtmlParser(this.state.information)}
</div>
</div>
</div>
);
}
}
export default MultiChoice;
QuestionAction.js
import axios from "axios";
import { FETCH_QUESTION } from "./types";
export const fetchQuestion = (questionId, answerId) => async dispatch => {
let question = null;
if (questionId){
question = await axios.get("/api/question/next?questionId=" + questionId + "&answerId=" + answerId);
}
else{
question = await axios.get("/api/question/next");
}
console.log("question", question);
dispatch({ type: FETCH_QUESTION, payload: question });
};
questionReducer.js
import {FETCH_QUESTION } from "../actions/types";
export default function(state = null, action) {
switch (action.type) {
case FETCH_QUESTION:
console.log("payload", action.payload.data);
return { question: action.payload.data, selected: false };
default:
return state;
}
}
index.js (Combined Reducer)
import { combineReducers } from 'redux';
import questionReducer from './questionReducer';
export default combineReducers({
question: questionReducer
});
and my entry point:
index.js
const store = createStore(reducers, {}, applyMiddleware(reduxThunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
registerServiceWorker();
requested console.log response:
render() {
console.log("Stackoverflow:", this.props.question)
.....
and after clicking the button (and the reducer updating, the console.log is updated, but the
this.getQuestionElement(this.props.question)
does not get re-rendered
MultiChoice Component shouldn't store his props in his state in the constructor, you have 2 options here :
Handle props changes in componentWillReceiveProps to update the state :
class MultiChoice extends Component {
constructor(props) {
super(props);
this.state = {
question: this.props.questionContent.question,
answerArray : this.props.questionContent.answers,
information: null
}
this.buttonClick = this.buttonClick.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState({
question: nextProps.questionContent.question,
answerArray : nextProps.questionContent.answers,
information: null
});
}
We have to keep using the constructor to set an initial state as from docs :
React doesn’t call componentWillReceiveProps() with initial props
during mounting.
2nd Option : Make it as a "dumb component" by having no state and only using his props to render something (some more deep changes in your component to do, especially to handle the "active" element, it will have to be handled by the parent component).
The this.props does not have the loading, error key.
Why?
My code on :
https://github.com/jiexishede/react-redux-demo01
You can fork it and pull request.
Because you don't pass them in your mapStateToProps function
https://github.com/jiexishede/react-redux-demo01/blob/0c1407935cd6c461705d6ca37f3e33484afac327/src/views/Home.js#L8-L10
This should be something like:
#connect(state => {
return {
articleList: state.home.list.articleList,
loading: state.home.list.loading,
error: state.home.list.error,
};
You didn't set up your component to receive updates from your store. Your component won't know that the reducer has updated the state. Check out the code below:
import React, { Component } from 'react';
import * as Redux from 'react-redux'; // Import redux
import Preview from './Preview'
class PreviewList extends Component {
static propTypes = {
loading:React.PropTypes.bool, // 注意 bushi PropTypes.bool, 前面要价 React
error:React.PropTypes.bool,
articleList: React.PropTypes.arrayOf(React.PropTypes.object),
loadArticles: React.PropTypes.func
};
componentDidMount(){
this.props.loadArticles();
}
render(){
const { loading, error, articleList } = this.props;
if(error){
return <p className="message">)0ops, something is wrong. </p>
}
if(loading){
return <p className="message">Loading....</p>
}
// return this.props.articleList.map(item => (
// <Preview {...item} key={item.id}/>
// ))
return (
<div>
{articleList.map(item => {
return <Preview {...item} key={item.id} push={this.props.push} />
})}
</div>
);
}
}
// Connect your component to your store and
// receive updates from your previewList reducer:
export default Redux.connect(state => {
return {
loading: state.previewList.loading,
error: state.previewList.error,
articleList: state.previewList.articleList
};
})(PreviewList);
The problem is that you're not connecting your component to the redux store. You need to install the react-redux package then use it's connect function to connect your component to the store like the following:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Preview from './Preview';
import { loadArticles } from './PreviewListRedux';
class PreviewList extends Component {
static propTypes = {
loading:React.PropTypes.bool,
error:React.PropTypes.bool,
articleList: React.PropTypes.arrayOf(React.PropTypes.object),
loadArticles: React.PropTypes.func
};
componentDidMount(){
this.props.loadArticles();
}
render(){
if (!this.props.loading) {
return <div>Loading...</div>
}
const { loading, error, articleList } = this.props;
if(error){
return <p className="message">)0ops, something is wrong. </p>
}
if(loading){
return <p className="message">Loading....</p>
}
return (
<div>
{articleList.map(item => {
return <Preview {...item} key={item.id} push={this.props.push} />
})}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
loading: state.list.loading,
error: state.list.error,
articleList: state.list.articleList
}
};
export default connect(mapStateToProps, { loadArticles })(PreviewList);
Also, your code needs some major restructuring, it's really difficult to read through it and see how the different pieces are connected together.
I have a simple Cart component and I want to show either a "Your cart is empty" message when there are no items in it.
import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as CartActions from '../actions/cart'
import Shelf from './Shelf'
import EmptyCart from './EmptyCart'
/*
This is a container component
*/
class Cart extends Component {
constructor(props) {
super(props)
this.state = {
itemQuantity: props.cart.length
}
}
render() {
const CartItems = this.props.cart.map(
(item, idx) =><li key={idx}>{item.name} - ${item.price}</li>
)
const isCartEmpty = () => this.state.itemQuantity === 0
console.log("is cart empty? ", isCartEmpty(), "cart item quantity ", this.state.itemQuantity)
return(
<div className="Cart">
<Shelf addItem={this.props.action.addToCart} />
<h2>Cart Items</h2>
<ol>
{ isCartEmpty() ? <EmptyCart/> : {CartItems} }
</ol>
</div>
)
}
}
function mapStateToProps(state, prop) {
return {
cart: state.cart
}
}
function mapDispatchToProps(dispatch) {
return {
action: bindActionCreators(CartActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cart)
My Shelf component looks like this:
import React, { Component } from 'react';
class Shelf extends Component {
constructor(props) {
super(props)
this.addItemToCart = this.addItemToCart.bind(this)
this.state = {
shelfItems: [
{ "name": 'shampoo', "price": 23 },
{ "name": 'chocolate', "price": 15 },
{ "name": 'yogurt', "price": 10 }
]
}
}
addItemToCart(item){
this.props.addItem(item)
}
render() {
const shelfItems = this.state.shelfItems.map((item, idx) => {
return <li key={idx}><button onClick={()=>this.addItemToCart(item)}>[+]</button>{item.name} - ${item.price}</li>
})
return(
<div>
<h2>Shelf</h2>
<ul>
{shelfItems}
</ul>
</div>
)
}
}
export default Shelf
Cart Reducer:
export default(state = [], payload) => {
switch (payload.type) {
case 'add':
return [...state, payload.item]
default:
return state
}
}
addToCart action:
export const addToCart = (item) => {
return {
type: 'add',
item
}
}
The empty message shows up but the list does not update when I add items. What am I doing wrong? The code works just fine if I remove the conditionals and just render CartItems
It's because you set only initial state. When you add item you don't set a new state. If you use redux there is no local state needed.
Try this:
class Cart extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
const CartItems = this.props.cart.map(
(item, idx) =><li key={idx}>{item.name} - ${item.price}</li>
)
const isCartEmpty = CartItems.length === 0
return(
<div className="Cart">
<Shelf addItem={this.props.action.addToCart} />
<h2>Cart Items</h2>
<ol>
{isCartEmpty ? <li>Your Cart is Empty</li> : CartItems}
</ol>
</div>
)
}
}