Calling A State Property in React/Redux - reactjs

I have a redux store with a state called "Pets" which stores an array of the following:
[{"id"2,"name":"Cat"},
{"id":3,"name":"Dog"},
{"id":4,"name":"Fish"}
]
In my component, I map the states like this: (I'm using redux forms and react-bootstrap-table)
class PetsPage extends Component {
constructor(props) {
super(props);
this.state =({
selected:[]
});
this.showRow = this.showRow.bind(this);
}
componentWillMount() {
this.props.fetchPets();
}
render() {
const {
handleSubmit,
previousPage,
pets
} = this.props
if (!pets){
return (
<div className="container">
<div> Loading...</div>
</div>
);
}
return (
<div className="container">
<BootstrapTable
data={pets}
search={true}
striped={true}
hover={true}
condensed={true}
selectRow={selectRowProp}>
<TableHeaderColumn dataField="id" isKey={true}>Pet #</TableHeaderColumn>
<TableHeaderColumn dataField="name">Company</TableHeaderColumn>
</BootstrapTable>
<p> Currently Chosen: <span> id: {this.state.selected}</span> </p>
<div className="btn-group">
<button type="button" onClick={previousPage} className="btn btn-success btn-lg"><i/> Previous</button>
<button type="submit" className="btn btn-success btn-lg">Next</button>
</div>
</div>
</form>
)
}
}
function mapStateToProps(state) {
return { pets: state.pets.all };
}
export default reduxForm({
form: 'wizard',
fields,
destroyOnUnmount: false,
validate
}, mapStateToProps, actions)(PetsPage)
So far, everything WORKS.
My problem arises when I call the pets prop like this:
<p> Currently Chosen: {pets[2].name}</p>
I keep getting this error:
TypeError: pets[2] is undefined
Ideally I want to be able to call the name of the pet by the ID which is provided by the "selected" state.
EDIT ---
I noticed the code actually WORKS if I were to go to another page and come back to this page. This is kind of confusing because I thought having the ...loading container would prevent this from happening.

How do you obtain the initial state of pets? were those retrieved asynchronously? (which i suspect it is)
When your BootstrapTable initially load, the async call was not complete, pets was still undefined, hence the error. Then when you go to another page, and come back to this page, your async call was finished, pets is now an array, and it works.
If I am right so far, add something like this in your BootstrapTable:
if(this.props.pets) {
<p> Currently Chosen: {pets[2].name}</p>
... // render your pets props within the
} else {
// render some loading indicator
}
Edit: if your pets was initialized as an empty array, check the length of the pets instead of the truthy value

Shouldn't you be doing this.props.pets[2] ?
Or this.state.pets[2] ? Unsure exactly what's happening but I don't think pets[2] on it's own would ever be anything. If that doesn't help can you post some more of your code?

Ok, I just figured it out.
Following xiaofan2406's suggestion, I literally have to put
if(this.props.pets[0])
Because
if(this.props.pets)
will always be true.

Related

React Redux - How to store form state so that it can be populated again when user returns to page

I have a search form that loads results. Everything gets updated in the Redux state except for the values selected in the form fields.
Lets say the search below shows 3 results. User clicks on result #2 and does not like it. The click the back button but the form state is cleared. How can I avoid this and make sure the form state stays ?
I am storing the results in Redux and able to load that but not sure how to handle state. I dont want to use packages such as redux-form if possible because this is the only place I need to store form state.
The state does not update when I change the selections. This is probably a slight change in code for an expert but I am beginner and this is my first React-Redux app.
Also, when I navigate away from the page, the state that was last stored is not retained. This is probably because of the local state formValues: {} that I am initializing but without this, there are errors. I tried to delete local state and just use props but it is giving me errors on the select box at this line - value={this.state.formValues['gender']}
Here is the overall code of the component:
import React, { Component } from 'react'
import { get } from 'axios'
import { connect } from 'react-redux'
import { FadeIn } from 'animate-components'
import {
getSearchResults,
setSearchedOnce,
setPageNum,
setFormValues,
} from '../../actions/searchresults'
import SearchResult from './Result/searchResult'
import SearchNoResult from './Result/searchNoResult'
class SearchAdvanced extends Component {
constructor(props) {
super(props)
this.state = { searchedOnce: false, users: [] }
}
handleChange(event) {
event.preventDefault()
this.props.setFormValues(this.props.formValues)
}
handleSubmit(event) {
event.preventDefault()
this.props.setSearchedOnce(true)
const apiSearchURL = `/api/search/religion/${this.props.formValues.religion}/gender/${this.props.formValues.gender}`
get(apiSearchURL, { maxContentLength: 400 })
.then((searchResults) => {
this.props.getSearchResults(searchResults)
})
}
loadMoreClick() {
this.props.setPageNum(this.props.pageNo + 1)
}
render() {
const { users } = this.props
let mapPageNo = this.props.pageNo
let map_usersList = users.data && users.data.slice(0, mapPageNo * 2).map((userlist => (
<SearchResult key={userlist.id} {...userlist} />
)))
let mapSearchedOnce = this.props.searchedOnce
return (
<div>
<FadeIn duration="300ms">
<div className="mid_rig_inner">
<div className="mid_inner">
<ul>
{ mapSearchedOnce
? map_usersList
: <SearchNoResult/>
}
{
mapSearchedOnce ?
(mapPageNo * 2 >= 3)
?
<div className="text-center my3 text-danger">
No more profiles. Try to modify search criteria.
</div> :
<div className="text-center my3">
<button type="button" className="btn btn-primary" onClick={this.loadMoreClick.bind(this)}>
Load More
</button>
</div>
: ''
}
</ul>
</div>
<div className="rig_inner">
<div className="my-4">
<div className="recomm">
<div className="recomm_top">
<span>Search</span>
</div>
</div>
<div className="search_advan_box">
<form onSubmit={this.handleSubmit.bind(this)}>
<select
name="religion"
className="mb-2"
value={this.props.formValues.religion}
onChange={this.handleChange.bind(this)}
>
<option value="" disabled="">
Select Religion
</option>
<option value="Any">Any Religion</option>
<option>Christian</option>
<option>Hindu</option>
<option>Muslim</option>
<option>Jain</option>
<option>Buddhist</option>
<option>Sikh</option>
<option>Parsi</option>
<option>Jewish</option>
<option>Spiritual</option>
<option>No Religion</option>
<option>Other</option>
</select>
<select
name="gender"
className="mb-2"
value={this.props.formValues.gender}
onChange={this.handleChange.bind(this)}
>
<option value="" disabled="">
Select Gender
</option>
<option>Male</option>
<option>Female</option>
<option>Other</option>
</select>
<input
type="submit"
className="my-4 btn btn-primary p2"
value={mapSearchedOnce ? "Refine Results":"Search Profiles"}
/>
</form>
</div>
</div>
</div>
</div>
</FadeIn>
</div>
)
}
}
const mapStateToProps = state => ({
users: state.Result.users,
searchedOnce: state.Result.searchedOnce,
pageNo: state.Result.pageNo,
formValues: state.Result.formValues
})
const mapDispatchToProps = {
getSearchResults,
setSearchedOnce,
setPageNum,
setFormValues
}
export default connect(mapStateToProps, mapDispatchToProps)(SearchAdvanced)
The state does not update when I change the selections
This is because you are not dispatching a Redux action in your handleChange method, only in your handleSubmit method. Simply dispatching the appropriate action in handleChange will resolve this issue.
when I navigate away from the page, the state that was last stored is not retained
This is because the values from before (before you navigate away) will only be kept in the Redux store, while the form fields are populated from the local state of the SearchAdvanced component.
To solve this one well, you should get rid of your local state entirely. Instead only use the Redux store. Keeping both intact is unnecessary and breaks the 'Single Source of Truth' that Redux is meant for. I recommend you do away with the local state and only update the Redux state and then pass values to the form inputs from the Redux store (props for the component).
Regarding your note that you tried this, but get errors: you need to change anything like:
<input value={ this.state.formValues.someValue }/>
to
<input value={ this.props.formValues.someValue }/>
where formValues comes from the Redux store.
Update
Problem now is the line
this.props.setFormValues(this.props.formValues)
in handleChange. You're using the old formValues to update, so the store never actually updates. I think you want something like:
handleChange(event) {
event.preventDefault();
const { name, value, type, checked } = event.target;
this.props.setFormValues({
...this.props.formValues,
[name]: type === 'checkbox' ? checked : value
});
}
So that you are updating the store's formValues with the input from the user. The ternary operator is necessary for checkbox inputs since the value of a checked checkbox is 'on' rather than true, but the checked attribute is true if checked.
Extra
It seems that you pass the dispatch method to the SearchAdvanced via props from the parent. You can (and should) do this more cleanly by using the second argument of connect, which is mapDispatchToProps. Here's an example:
const mapDispatchToProps = {
getSearchResults,
setSearchedOnce,
setPageNum,
setFormValues
}
export default connect(mapStateToProps, mapDispatchToProps)(SearchAdvanced);
Then you can just call any of these as methods that already have a dispatch bound to them. So
this.props.dispatch(setSearchedOnce(true))
becomes
this.props.setSearchedOnce(true)
and you can remove the passing of the dispatch from the parent component.
Docs on connect: https://github.com/reduxjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options

Is it okay to call setState on a child component in React?

I have some text. When you click on that element a modal pops up that lets you edit that text. The easiest way to make this work is to call setState on the child to initialise the text.
The other way, although more awkward, is to create an initial text property and make the child set it's text based on this.
Is there anything wrong with directly calling setState on the child or should I use the second method?
Although it is recommended to keep the data of your react application "up" in the react dom (see more here https://reactjs.org/docs/lifting-state-up.html), I don't see anything wrong with the first aproach you mentioned.
If you have to store data that is very specific of a child I don't see anything wrong in keep that information in the child's state.
It seems that your modal doesn't need to have its own state, in which case you should use a stateless React component.
This is one way of passing the data around your app in the React way.
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
initialText: "hello",
}
this.saveChildState = this.saveChildState.bind(this);
}
saveChildState(input) {
console.log(input);
// handle the input returned from child
}
render() {
return (
<div>
<ChildComponent
initialText={this.state.initialText}
save={this.saveChildState}
/>
</div>
);
}
}
function ChildComponent(props) {
return (
<div>
<input id="textInput" type="text" defaultValue={props.initialText}>
</input>
<button onClick={() => props.save(document.getElementById('textInput').value)}>
Save
</button>
</div>
)
}
Maybe I am misinterpreting your question, but I think it would make the most sense to keep the modal text always ready in your state. When you decide to show your modal, the text can just be passed into the modal.
class Test extends Component {
constructor() {
this.state = {
modalText: 'default text',
showModal: false
}
}
//Include some method to change the modal text
showModal() {
this.setState({showModal: true})
}
render(
return (
<div>
<button onClick={() => this.showModal()}>
Show Modal
</button>
{ this.state.showModal ? <Modal text={this.state.modalText}/> : null }
</div>
)
)
}

Can't get button component value onClick

I'm sure this is something trivial but I can't seem to figure out how to access the value of my button when the user clicks the button. When the page loads my list of buttons renders correctly with the unique values. When I click one of the buttons the function fires, however, the value returns undefined. Can someone show me what I'm doing wrong here?
Path: TestPage.jsx
import MyList from '../../components/MyList';
export default class TestPage extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleButtonClick(event) {
event.preventDefault();
console.log("button click", event.target.value);
}
render() {
return (
<div>
{this.props.lists.map((list) => (
<div key={list._id}>
<MyList
listCollection={list}
handleButtonClick={this.handleButtonClick}
/>
</div>
))}
</div>
);
}
}
Path: MyListComponent
const MyList = (props) => (
<div>
<Button onClick={props.handleButtonClick} value={props.listCollection._id}>{props.listCollection.title}</Button>
</div>
);
event.target.value is for getting values of HTML elements (like the content of an input box), not getting a React component's props. If would be easier if you just passed that value straight in:
handleButtonClick(value) {
console.log(value);
}
<Button onClick={() => props.handleButtonClick(props.listCollection._id)}>
{props.listCollection.title}
</Button>
It seems that you are not using the default button but instead some sort of customized component from another libray named Button.. if its a customezied component it wont work the same as the internatls might contain a button to render but when you are referencing the event you are doing it throug the Button component

i am not able retrieve array elements one at a time if i call them in my component all the elements are retrieved at a time

I want to load my array element when an event is occurred by referencing the key i tried different variables for the key but it would not accept all the elements of the array are being displayed if i give index as the key.
I am new to Reactjs and not very familiar with all the syntax and concept can somebody help me with the logic to solve this.
The event I am triggering is onClick or onChange.
`var Qstn =['A','B','C','D','E','F','G','H','I','J'];
<div>
{Qstn.map(function(Q,index){
return <span className="col-md-4 txt" key={index}>{Q}</span>
})}
</div>`
Ok I made a codepen with an example
http://codepen.io/lucaskatayama/pen/QGGwKR
It's using ES6 classes components, but it's easy to translate.
You need to set initial state to an empty array like [].
On click button, it call onClick() method which uses this.setState({}) to change component state.
When React notice state changes, it re-render the component.
class Hello extends React.Component {
constructor(){
super();
//Initial State
this.state = {
Qstn : []
}
}
onClick(){
//Called on click button
// Set state to whatever you want
this.setState({Qstn : ['A','B','C','D','E','F','G','H','I','J']})
}
render(){
let Qstn = this.state.Qstn; // load state and render
return (
<div>
<button onClick={() => this.onClick()}>Click</button>
<div>
{Qstn.map(function(Q,index){
return <span className="col-md-4 txt" key={index}>{Q}</span>
})}
</div>
</div>
)
}
}
ReactDOM.render(<Hello />, document.getElementById('container'))

How to fix error Uncaught Invariant Violation: findComponentRoot(...?

I’ve created InitializePhoneNumbersPanel:
class InitializePhoneNumbersPanel extends Component {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit(phoneNumbers) {
const { dispatch, operatorId } = this.props;
dispatch(updateOperatorData(operatorId, phoneNumbers, {include: 'phone_numbers'}));
}
render() {
const {
handleSubmit,
submitting,
fields: { phone_numbers }
} = this.props;
console.log('\n... Render ...');
console.log('phone_numbers <<<<< ', phone_numbers);
if (_.isEmpty(phone_numbers)) {
return (
<div className={"fade in"}>
Hello
</div>
)
}
return (
<form onSubmit={handleSubmit(this.onSubmit)}>
<div className="row">
<div className="col-md-12">
<ul className="list-unstyled m-b-0 clearfix">
{phone_numbers && phone_numbers.map((phone, index) =>
<PhoneNumbersPanelItem key={index} phone={phone} phone_numbers={phone_numbers}
index={index}/>
)}
</ul>
</div>
</div>
<div className="row">
<div className="col-md-12">
<button type="button" className="btn btn-sm btn-success" onClick={event => {
event.preventDefault();
phone_numbers.addField();
}}><i className="fa fa-plus"></i>
</button>
</div>
</div>
<hr/>
<div className="row">
<div className="col-md-12">
<button type="submit" disabled={ submitting } className="btn btn-sm btn-success pull-right">
Save
</button>
</div>
</div>
</form>
)
}
}
Then this component is wrapped by Redux-form:
InitializePhoneNumbersPanel = reduxForm({
form: 'phone-numbers-panel',
fields
})(InitializePhoneNumbersPanel);
Then everything is wrapped by connect method to make data from Store accessible in Redux-form as fields:
function select(state) {
return {
initialValues: {
phone_numbers: _.map(state.operators.items[state.operators.selectedOperator] && state.operators.items[state.operators.selectedOperator].phone_numbers, phoneId => {
return state.phoneNumbers.items[phoneId];
})
},
operatorId: state.operators.selectedOperator
};
}
InitializePhoneNumbersPanel = connect(select)(InitializePhoneNumbersPanel);
The error is…
The code above works normally however in PhoneNumbersPanelItem component phone numbers which come from “phone_numbers” variable are repeated.
When the operators page(whose phone numbers are shown using PhoneNumbersPanelItem) is loaded the first time no errors occur, however if I choose other operator, Route will change which means operatorId param in store will change which means operators object will change and the phone numbers will be different… changed data are sent to component here:
function select(state) {
return {
initialValues: {
phone_numbers: _.map(state.operators.items[state.operators.selectedOperator] && state.operators.items[state.operators.selectedOperator].phone_numbers, phoneId => {
return state.phoneNumbers.items[phoneId];
})
},
operatorId: state.operators.selectedOperator
};
}Operator
};
}
InitializePhoneNumbersPanel = connect(select)(InitializePhoneNumbersPanel);
So if the number of phone numbers of chosen operator is less than the previous one had , the error is thrown
Uncaught Invariant Violation: findComponentRoot(...,
.0.0.0.1.2.0.0.2.1.0.1.1.0.0.0.$1.0.0.0.0.1.1.0): Unable to find
element. This probably means the DOM was unexpectedly mutated (e.g.,
by the browser), usually due to forgetting a when using
tables, nesting tags like , , or , or using non-SVG
elements in an parent. Try inspecting the child nodes of the
element with React ID ``.
As I understood, the error is thrown because at the beginning there were 3 phone numbers for example, and when I choose a new operator the number of phones is 2 and React seemingly fails to find html code for the third number as in the new rendering this element was not created
Even though there is an error, everything works ok. Probably with another rendering react understands that the state has updated and rerenders virtual DOM
If all operators have the same number of phone numbers, NO error occur AT ALL
How can I fix this error? Has anybody encountered anything like that? So strange that React doesn’t understand that the virtual DOM has changed when we switch to a new Route.
I’ll appreciate any help/solution to this problem
I've tried multiple things to make it work on mine. I had a similar problem.
Apparently the problem was with the type of the button. ReactDOM gets lost if you use a type="button"
I removed the type="button" and added a event.preventDefault() on my onClick handler and it worked for me.
I had a very similar scenario. I tried a bunch of things and the only thing that worked for me was updating react and react-dom to version 15.3.2 (from 0.14.2).

Resources