Dispatch not a function - reactjs

Objective
Setup a dynamic form controlled by the user using react-redux and revalidate to run validation checks on my form.
Problem:
Since my form is dynamic I need to make dynamic validations. In order to do this my form data needs to be passed in as props to my component, which can be used as a second argument in the validate function from revalidate
My approach
To do this I am waiting until the component is mounted, building the form, passing it to redux, and then will map state to props. As the user adds more rows I will update state and then the component will render. I will use shouldComponentUpdate() to avoid any render loops.
The Error
My error is regarding the dispatch. When I try to run the dispatch (which will pass the form into redux) I get Dispatch is not a function error.
I am not supper comfortable with the connect() as I have to wrap redux with it as well as firebase. This syntax really confuses me.
Question
I believe the issue with with how I am exporting the component where I am using HOC like withFirebase, Redux, and Connect. Somewhere along the way I am losing scope to the connect. Can someone shed light into what it is I am doing wrong?
Component
import React, { Component } from "react";
import { reduxForm, Field } from "redux-form";
import { Container, Form, Col, Button } from "react-bootstrap";
import MaterialIcon from '../../material-icon/materialIcon';
import { withFirestore } from "react-redux-firebase";
import { connect } from "react-redux";
import TextInput from "../../forms/textInput";
import { combineValidators, isRequired } from "revalidate";
import { setupStudentForm } from '../../../store/actions/students';
const validate = (values, ownprops) => {
// Will be passing in validation rules, once form is apssed in with props via mapStateToProps.
}
export class CreateStudentsForm extends Component {
// Using constrcutor so componentDidMount() can render() cann access variables
constructor(props) {
super(props);
this.state = {
rows: 2,
}
this.formArray = [];
this.form = null;
}
componentDidMount() {
// Once component is rendered, setup form and send to redux
for (let i = 1; i !== this.state.rows + 1; i++) {
let firstNameField = {
fieldName: `firstName${i}`,
label: 'First Name',
required: true,
type: "text",
}
let lastNameField = {
fieldName: `lastName${i}`,
label: 'Last Name',
required: true,
type: "text",
}
this.formArray.push([firstNameField, lastNameField]);
}
this.props.setupStudentFormHandler(this.formArray);
}
// Ensure we do not get stuck in render loop
shouldComponentUpdate(nextProps, nextState){
if(nextProps !== this.props){
return true
} else {
return false
}
}
render() {
// Allows user to add another row
const addRow = () => {
this.setState({
rows: this.state.rows + 1
})
}
// Map through form array and create template
if (this.formArray) {
let form = this.formArray.map((field, index) => {
return (
<Form.Row key={index} className="animated fadeIn">
<Col xs={5}>
<Form.Group className="mb-0 noValidate">
<Field
label={field[0].label}
attempt={this.props.attempt}
name={field[0].fieldName}
type={field[0].type}
component={TextInput}
/>
</Form.Group>
</Col>
<Col xs={5}>
<Form.Group className="mb-0 noValidate">
<Field
label={field[1].label}
attempt={this.props.attempt}
name={field[1].fieldName}
type={field[1].type}
component={TextInput}
/>
</Form.Group>
</Col>
<Col xs={2}>
<MaterialIcon icon="delete" className="mt-4" />
</Col>
</Form.Row>
)
})
}
return (
<Container>
{this.form}
<Button variant="outline-success" onClick={addRow}>Add Another Student</Button>
</Container>
)
}
}
const mapStateToProps = state => {
return {
// Get access to student state which will have form
studentForm: state.students
};
};
const mapDispatchToProps = dispatch => {
return {
//Send formArray to redux
setupStudentFormHandler: (form) => dispatch(setupStudentForm(form))
};
};
export default withFirestore(
reduxForm({
form: "createStudents", validate
})(connect(
mapDispatchToProps,
mapStateToProps
)(CreateStudentsForm)
)
);

mapStateToProps is the first argument of connect, mapDispatchToProps is the second. Try swapping the order:
connect(
mapStateToProps,
mapDispatchToProps
)(CreateStudentsForm)

Related

React Redux - how to derive and then aggregate data in mapStateStateToProps using a selector?

I have a component that returns an object called Progress, inside of which is an array called Results. This array has objects with various properties, one of which is called total
{
Progress: {
count: 100,
results: [
{total: 4, ...},
{total: 10, ...},
...
]
}
}
The component, Dashboard, gets the data from state and maps it the Progress property.
export class Dashboard extends Component {
static propTypes = {
progress: PropTypes.object.isRequired,
getProgress: PropTypes.func.isRequired,
totalResults: PropTypes.number.isRequired
}
componentDidMount() {
this.props.getProgress()
}
...
}
const selectProgress = state => state.progressReducer.progress
const mapStateToProps = state => ({
progress: selectProgress(state),
})
export default connect(mapStateToProps, { getProgress })(Dashboard)
The issue I have now is how can I add a new property which is derived from progress?
I understand I need to use a Selector but I cannot see where/how to do this.
For example, I know I can do something trivial (and pointless) like this:
const mapStateToProps = state => ({
progress: selectProgress(state),
count: selectProgress(state).count
})
which adds another property count to the component (yes it's just duplicated the property inside progress, hence why it is pointless).
What I need to do is something like this:
const mapStateToProps = state => ({
progress: selectProgress(state),
resultsTotal: <loop through the results array and sum the property total>
})
1 - What I have tried
I tried this even though I understand it isn't meant to be this way. This is to illustrate hopefully what I am trying to do - AFTER I've got progress, pass it to some function to calculate the total and return that as a property to the component:
const selectResults = progress => {
progress.results.reduce((acc, result) => {
acc + result.total
}, 0)
}
const mapStateToProps = state => ({
progress: selectProgress(state),
totalResults: selectResults(progress)
})
2 - What I have tried
I thought this would have worked, by basically letting the render view call function at the point needed in the JSX:
export class Dashboard extends Component {
static propTypes = {
progress: PropTypes.object.isRequired,
getProgress: PropTypes.func.isRequired,
}
componentDidMount() {
this.props.getProgress()
}
totalResults() {
if (this.props.progress.results)
return this.props.progress.results.reduce((acc, result) => {
acc + result.total
}, 0)
}
render() {
...
<SummaryCard title='Students' value={this.totalResults()} />
...
}
}
I am now wondering why this didn't work - I had to add this line:
if (this.props.progress.results)
because progress is of course empty when this function executes (ie I guess because it executes when the component first mounts, and the store has not returned the data yet).
mapStateToProps is a function. Currently you are using a short version to return an object immediately, but you can have it as a complex function and return an object in the end:
const mapStateToProps = state => {
const progress = selectProgress(state);
return {
progress,
totalResults: progress !== undefined ? selectResults(progress) : undefined
}
}
One solution that I found to this problem is to use the excellent reselect library. From their github page:
Selectors can compute derived data, allowing Redux to store the
minimal possible state.
Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
Selectors are composable. They can be used as input to other selectors.
So I had to create a second selector and chain it to the first one.
Below you can see that progressSelector will pass its result (the progress data) onto the next selector in the chain (totalResultsSelector):
Here is the full component:
import React, { Component } from 'react';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'
import { getProgress } from '../../actions/progress';
import SummaryCard from '../SummaryCard';
import { People } from 'react-bootstrap-icons';
import { Book } from 'react-bootstrap-icons';
import { Award } from 'react-bootstrap-icons';
import styles from './styles.scss';
export class Dashboard extends Component {
static propTypes = {
progress: PropTypes.object.isRequired,
getProgress: PropTypes.func.isRequired,
totalResults: PropTypes.number.isRequired
}
componentDidMount() {
this.props.getProgress()
}
render() {
return (
<div className={styles.wrapper}>
<Row xs={1} sm={3}>
<Col>
<SummaryCard title='Students' value={this.props.totalResults} icon={<People />} />
</Col>
<Col>
<SummaryCard title='Courses' value={this.props.progress.count} icon={<Book />} />
</Col>
<Col>
<SummaryCard title='Certified' value='0' icon={<Award />} />
</Col>
</Row>
</div>
)
}
}
const progressSelector = state => state.progressReducer.progress
const totalResultsSelector = createSelector (
progressSelector,
progress => {
if (!progress.results) return 0
const total = progress.results.reduce((acc, result) => {
return acc + result.total
}, 0)
return total
}
)
const mapStateToProps = state => ({
progress: progressSelector(state),
totalResults: totalResultsSelector(state)
})
export default connect(mapStateToProps, { getProgress })(Dashboard)

Set parent's state from child

My custom component should pass data to parent which is react-admin <Create>
I have came across some questions already and found that I can't simply set state from child to parent.
The problem is that this component should work like the default react-admin components (ex. ). It means when I submit the form It gets data from that component.
I have already tried addField()
This is my custom component (child):
import React from "react";
import MenuItem from "#material-ui/core/MenuItem";
import Select from "#material-ui/core/Select";
import { fetchUtils } from 'react-admin';
import FormControl from '#material-ui/core/FormControl';
import InputLabel from '#material-ui/core/InputLabel';
import { DataService } from '../routes/api';
import PropTypes from 'prop-types';
import { resources as rsrc } from '../resources';
const divStyle = {
marginTop: '16px',
marginBottom: '8px',
};
const inputStyle = {
width: '256px',
}
export default class MultipleSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
selectOptions: [],
selectedValues: [],
selectedValue: null,
};
}
getRoles() {
// get data from api
}
getAllOptions() {
// get some additional data from API
}
createRelationRecord(id) {
// create relation record (for ex. User's Role)
}
deleteRelationRecord(id) {
// delete relation record
}
componentDidMount() {
this.getRoles();
this.getAllOptions();
}
renderSelectOptions = () => {
return this.state.selectOptions.map((dt, i) => (
<MenuItem key={dt.id} value={dt.id}>
{dt.value}
</MenuItem>
));
};
handleChange = event => {
this.setState({ selectedValue: event.target.value });
// If record doesn't exist
if (this.state.selectedValue != event.nativeEvent.target.dataset.value) {
this.createRelationRecord(event.nativeEvent.target.dataset.value);
}
if (this.state.selectedValues.includes(Number(event.nativeEvent.target.dataset.value))) {
this.deleteRelationRecord(event.nativeEvent.target.dataset.value);
} else {
this.createRelationRecord(event.nativeEvent.target.dataset.value);
}
};
selectboxType() {
if (this.props.multiple) {
return true;
}
return false;
}
getSelected() {
if (this.selectboxType()) {
return this.state.selectedValues;
}
return this.state.selectedValue;
}
render() {
return (
<div style={divStyle}>
<FormControl>
<InputLabel htmlFor={this.props.label}>{this.props.label}</InputLabel>
<Select
multiple={this.selectboxType()}
style={inputStyle}
value={this.getSelected()}
onChange={this.handleChange}
>
{this.renderSelectOptions()}
</Select>
</FormControl>
</div>
);
}
}
Parent (create form):
export const ServerCreate = props => (
<Create {...props}>
<SimpleForm>
<TextInput source="Name" validate={required()} />
<ReferrenceSelectBox label="ServerType" multiple={false} source="ServerTypeId" reference="ServerType"></ReferrenceSelectBox>
<TextInput source="Barcode" validate={required()} />
</SimpleForm>
</Create>
);
It works with handleChange to achieve the data update. Now I need to save the selected data in Create form, but handleChange will not help me, because the object is not created yet and I cannot set attribute of non-existent record.
So my question is how can I pass value/values from my component to Create? How to set parent's state?
Your ServerCreate doesn't have any state, but if it did, you need to pass a function which can update its state to the child component as a prop.
Generally speaking, you should probably lift the state higher up the component tree

How to load previous values in Redux From Field on screen and get the new values as users enter them?

I am trying to load my previous values in Redux From Fields on screen and hoping to get new values from each fields as a user updates the values. I have tried couple different ways, like onChange or initialValues but so far I still have no luck to get it working:
Here is my code so far:
`import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { reduxForm, Field } from 'redux-form';
import { Link } from 'react-router-dom';
import SurveyField from './SurveyField';
import { fetchOneSurvey } from '../../actions';
import validateEmails from '../../utils/validateEmails';
import formFields from './formFields';
class SurveyEditForm extends Component {
componentDidMount() {
const id = window.location.pathname.split('/')[2];
this.props.fetchOneSurvey(id);
}
renderFields() {
return _.map(formFields, ({ label, name }) => {
return (
<Field
key={name}
component={SurveyField}
type="text"
label={label}
name={name}
/>
);
});
}
render() {
return (
<div>
{/*.handleSubmit is from reduxForm method*/}
<form
onSubmit={this.props.handleSubmit(
this.props.onSurveySubmit
)}
>
{this.renderFields()}
<Link to="/surveys" className="red btn-flat white-text">
Cancel
</Link>
<button
type="submit"
className="teal btn-flat right white-text"
>
Next
<i className="material-icons right">done</i>
</button>
</form>
</div>
);
}
}
function validate(values) {
const errors = {};
errors.recipients = validateEmails(values.recipients || '');
_.each(formFields, ({ name }) => {
if (!values[name]) {
errors[name] = 'You must provide a value';
}
});
return errors;
}
function mapStateToProps(state) {
return {
surveyEdit: state.surveyEdit
};
}
SurveyEditForm = connect(mapStateToProps, { fetchOneSurvey })(SurveyEditForm);
SurveyEditForm = connect(state => ({
initialValues: {
title: this.props.surveyEdit.title
}
}))(SurveyEditForm);
export default reduxForm({
validate,
form: 'surveyEditForm',
destroyOnUnmount: false,
enableReinitialize: true
})(SurveyEditForm);`
This currently gives me error:
TypeError: Cannot read property 'surveyEdit' of undefined
this.props.surveyEdit
this is the object I fetched from the database and I was hoping to use it to provide initial values to the field. I also couldn't see previous value on screen while I was trying.
Anyone have better idea? I am pretty sure there must be a way to do it, I just can't seem to comprehend how to implement it.
Thank you in advance as always!

How to create bootstrap toggle buttons with React and Redux Form?

I am trying to create a toggable button group using React, Redux Form and Bootstrap (reactstrap).
What i have done is already correctly updating the redux form data.
The problem is with the button color attribute wich should be toggled between "success" and "secondary". Right now it does set the color on the first toggle, but does not update when i click another button afterwards.
This is my render component:
import React from 'react';
import classNames from 'classnames';
import { Label, FormGroup, ButtonGroup, Button } from 'reactstrap';
import FontAwesome from 'react-fontawesome';
export default class buttonOptions extends React.PureComponent {
static propTypes = {
input: React.PropTypes.object,
buttons: React.PropTypes.any,
label: React.PropTypes.string,
meta: React.PropTypes.shape({
touched: React.PropTypes.bool,
error: React.PropTypes.any,
})
};
constructor(props) {
super(props);
this.toggleOption = this.toggleOption.bind(this);
}
toggleOption(val) {
if (!this.props.input.value.length) this.props.input.value = [];
// option per buttongroud is always limited to 1
// remove previously selected options
for (let b of this.props.buttons) {
if (b.value !== val && this.props.input.value.indexOf(b.value) > -1) {
this.props.input.value.splice(this.props.input.value.indexOf(b.value), 1)
}
}
// push the new option and update state
this.props.input.value.push(val);
this.props.input.onChange(this.props.input.value)
}
render() {
const { input, buttons, label, meta: { touched, error }} = this.props;
const labelStyles = {width: '100%', marginBottom: '0'};
return (
<FormGroup>
<Label style={labelStyles}>{label}</Label>
<ButtonGroup>
{
buttons.map((b) => {
return (
<Button
key={b.title}
color={classNames({
success: input.value.indexOf(b.value) > -1,
secondary: input.value.indexOf(b.value) === -1,
})}
role="button"
onClick={() => { this.toggleOption(b.value) }}
>
{b.title}
</Button>
)
})
}
</ButtonGroup>
</FormGroup>
);
}
}
And this is how its implemented:
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AdWizard.css';
import cx from 'classnames';
import FontAwesome from 'react-fontawesome';
import { Field, reduxForm } from 'redux-form'
import { Row, Col, FormGroup, Label, Button } from 'reactstrap';
import buttonOptions from '../FormComponents/buttonOptions';
class Step2 extends React.Component {
constructor(props) {
super(props);
this.workingtimes = [
{
title: "Vollzeit",
value: "Vollzeit",
selected: true
},
{
title: "Teilzeit",
value: "Teilzeit",
selected: false
}
]
}
render() {
const { handleSubmit, previousPage } = this.props;
return (
<form onSubmit={handleSubmit}>
<Row className="justify-content-center">
<Col xs="12" sm="6">
<Field
label="Arbeitszeit"
name="arbeitszeit"
buttons={this.workingtimes}
component={buttonOptions}
/>
</Col>
</Row>
</form>
)
}
}
Step2 = reduxForm({
form: 'posting',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true
})(Step2);
export default withStyles(s)(Step2);
Would be great if someone could help out!
Cheers
Stefan
The problem you have is that your toggleOption function is impure*.
This means it is mutating this.props.input.value, instead of creating a new array whose value is based on it - putting it simple, always create a new reference!
Since most React code out there is very sensitive to pure function calls,
you must convert that function to a pure one, so that redux-form actually sees you changed the array:
toggleOption (b) {
let newValue;
const currValue = this.props.input.value || [];
if (currValue.includes(b.value)) {
// value already exists in array, let's remove it from there
newValue = currValue.filter(val => val !== b.value);
} else {
// value doesn't exist in array, let's add it there
newValue = currValue.concat([b.value]);
}
this.props.input.onChange(newValue);
}
Array methods such as .filter() and .concat() are your frinds here: they return new array instances, instead of mutating the existing array.
Your code was using .push() and .splice(), methods that are bad, because they mutate the existing array.
You can see a small demo here.
* You can read more about this subject here.

shall we use state or props for handling inputs in redux react?

I am confused with props or state to use here. If I use state in #connect I get error and does not work. when I use props it does not work with onchange handler to set new props. Please help how should I make input working with state or props. I am retrieving initial data from api.
import React, {PropTypes} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import { asyncConnect } from 'redux-async-connect';
import {load, isLoaded} from 'redux/modules/overview';
#asyncConnect([{
promise: ({ store: { dispatch, getState }, params: { id }, }) => {
const promises = [];
if (!isLoaded(getState())) {
promises.push(dispatch(load(id)));
}
return Promise.all(promises);
}
}])
#connect(
state => ({
overview: state.overview.data
}),
dispatch => bindActionCreators({load}, dispatch))
export default class Overview extends React.Component {
changeinput1(e) {
this.props.overview.title = e.target.value;
// changing value does not work here
}
constructor (props) {
super();
this.state = {
overview: null,
}
}
render() {
return (
<div>
<label>Input 1</label>
<input type="text" className="form-control" id="title" name="title" maxlength="35" value={this.props.overview.title} onChange={this.changeinput1.bind(this)}/>
</div>
)
}
}
I also want to do validation and want to save input value on onBlur so I dont want to use form.
if you want change reducer's(here suppose to be 'overview') value, you should define an action then dispatch it, not change it directly, the state get from store is readonly in the component

Resources