So I have a simple form wizard taken from an example in redux-form. I want to initialize fields with data received from an asynchronous API call.
The way it is right now, fields are not being populated without the enableReinitialize: true param in each page of the wizard, because initialization already happens once upon starting the server request.
If on the other hand I add enableReinitialize: true to the wizard pages then any user input will be discarded upon moving from page to page and initialValues will be set again.
I have also tried adding the keepDirtyOnReinitialize: true option in conjunction with enableReinitialize: true to each page, but to no avail.
What would be the correct way of creating a form wizard when you need to have a subset of fields be initialized by asynchronously fetched data which has to be overwriteable by user?
WizardForm.jsx:
import React, {Component, PropTypes} from 'react'
import WizardFormFirstPage from './WizardFormFirstPage.jsx'
import WizardFormSecondPage from './WizardFormSecondPage.jsx'
import WizardFormThirdPage from './WizardFormThirdPage.jsx'
import fetchData from "./fetchData.jsx";
import {connect} from "react-redux";
class WizardForm extends Component {
constructor(props) {
super(props);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.state = {
page: 1
}
}
componentWillMount() {
if (!this.props.hasFetched)
this.props.fetchData();
}
nextPage() {
this.setState({ page: this.state.page + 1 })
}
previousPage() {
this.setState({ page: this.state.page - 1 })
}
render() {
const { onSubmit } = this.props;
const { page } = this.state;
return (
<div>
{page === 1 && <WizardFormFirstPage onSubmit={this.nextPage} initialValues={this.props.initialValues}/>}
{page === 2 &&
<WizardFormSecondPage previousPage={this.previousPage} initialValues={this.props.initialValues}
onSubmit={this.nextPage}/>}
{page === 3 &&
<WizardFormThirdPage previousPage={this.previousPage} initialValues={this.props.initialValues}
onSubmit={onSubmit}/>}
<label>{this.props.isFetching ? "Fetching data.." : "Fetched data successfully." }</label>
</div>
)
}
}
function mapStateToProps(state) {
return {
initialValues: state.prefill.data,
isFetching: state.prefill.fetching,
hasFetched: state.prefill.fetched
}
}
WizardForm = connect(
mapStateToProps,
{fetchData}
)(WizardForm);
WizardForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
initialValues: PropTypes.object
};
export default WizardForm;
WizardFormFirstPage.jsx:
class WizardFormFirstPage extends Component {
constructor(props) {
super(props);
}
render() {
const {handleSubmit} = this.props;
return (
<form onSubmit={handleSubmit}>
<Field name="firstName" type="text" component={renderField} label="First Name"/>
<Field name="lastName" type="text" component={renderField} label="Last Name"/>
<Field name="downPayment" type="text" component={renderField}
label="Down Payment" normalize={normalizeDownPayment}/>
<div>
<button type="submit" className="next">Next</button>
</div>
</form>
)
};
}
WizardFormFirstPage = reduxForm({
form: "wizard",
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
//enableReinitialize: true, // <-- !
validate
})(WizardFormFirstPage);
export default WizardFormFirstPage;
fetchData.jsx:
export default function fetchData() {
return (dispatch) => {
dispatch({type: "FETCH_DATA_START"});
axios.get("http://rest.learncode.academy/api/nordischby/customer")
.then(response => {
dispatch({type: "FETCH_DATA_FINISH", data: response.data[0]});
})
.catch(error => {
console.error(error);
});
}
};
prefill.jsx:
const initialState = {
data: {},
fetching: false,
fetched: false,
error: null
};
const prefillReducer = (state = initialState, action) => {
switch (action.type) {
case "FETCH_DATA_START":
return {
...state,
fetching: true
};
case "FETCH_DATA_FINISH":
return {
...state,
data: action.data,
fetching: false,
fetched: true
};
default:
return state
}
};
export default prefillReducer;
I've found what was wrong. For future reference:
I'm now using the following options for reduxForm():
export default reduxForm({
form: "wizard",
destroyOnUnmount: false,
//forceUnregisterOnUnmount: true, // <-- This bad boy was wrong
keepDirtyOnReinitialize: true,
enableReinitialize: true,
validate
})(WizardFormThirdPage)
Related
I am studying react-redux. First of all, the action of addTodo looks like this.
let nextTodoId = 0;
export const addTodo = (content) => ({
type: ADD_TODO,
payload: {
id: ++nextTodoId,
content
}
});
And the component is here
import React, { useState } from "react";
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";
function AddTodoComponent(props) {
const [inputValue, setInput] = useState("");
console.log({ addTodo })
const handleAddTodo = () => {
console.log(inputValue)
props.addTodo(inputValue);
setInput("");
};
return (
<div>
<input onChange={(e) => setInput(e.target.value)} />
<button className="add-todo" onClick={handleAddTodo}>
Add Todo
</button>
</div>
);
}
export default connect(null, { addTodo })(AddTodoComponent);
I want to use mapStateToProps and mapDispatchToProps but It's not working.
I thought It will be work if I use destructing object.
But it's not appear in console too.
Am I wrong?
import React, { useState } from "react";
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";
function AddTodo({
allId,
byId,
newTodo,
}) {
const [inputValue, setInput] = useState("");
const handleAddTodo = () => {
newTodo(inputValue);
setInput("");
};
return (
<div>
<input onChange={(e) => setInput(e.target.value)} />
<button className="add-todo" onClick={handleAddTodo}>
Add Todo
</button>
</div>
);
}
const mapStateToProps = state => {
return {
allId: state.allId,
byId: state.byId,
}
}
const mapDispatchToProps = dispatch => {
return {
newTodo: () => dispatch(addTodo()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);
and this is reducer function.
import { ADD_TODO, TOGGLE_TODO } from "../actionTypes";
const initialState = {
allIds: [],
byIds: {}
};
export default function(state = initialState, action) {
switch (action.type) {
case ADD_TODO: {
const { id, content } = action.payload;
return {
...state,
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false
}
}
};
}
case TOGGLE_TODO: {
const { id } = action.payload;
return {
...state,
byIds: {
...state.byIds,
[id]: {
...state.byIds[id],
completed: !state.byIds[id].completed
}
}
};
}
default:
return state;
}
}
It took me a bit of playing but I found your problem! (3 problems, actually).
Your addTodo function is a function which takes the content of the Todo (a string) and creates the action.
const mapDispatchToProps = (dispatch) => {
return {
newTodo: () => dispatch(addTodo())
};
};
But in your mapDispatchToProps function, you are accepting no arguments and calling addTodo with no arguments. So your Todo gets added, but the content will always be undefined.
Change this to
newTodo: (content) => dispatch(addTodo(content))
In mapStateToProps you've got a misnamed property. You need to change state.byId to state.byIds.
In order to clear the content of the input, you need the value of the input to be controlled by your useState. Add value={inputValue} to the input element.
<input value={inputValue} onChange={(e) => setInput(e.target.value)} />
This is a sidenote, but you might want to think about learning typescript and adding annotations to your projects. There is a bit of a learning curve but the tradeoff is you would be able to catch things like missing arguments and misnamed properties easily. They can be a real head-scratcher otherwise.
I am trying to use Redux in my React application to update the user profile within my Firebase database from my react component.
This is my component:
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import { firestoreConnect } from "react-redux-firebase";
import { compose } from "redux";
import { editProfile } from "../../store/actions/editProfileActions";
class UserProfile extends Component {
state = {
firstName:"",
initials:"",
lastName:""
};
onChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
onSubmit = e => {
e.preventDefault();
console.log(this.state);
this.props.editProfile(this.state);
}
render() {
const { auth, profile } = this.props;
console.log(profile);
if (auth.isEmpty) return <Redirect to="/home" />;
return (
<div className="container">
<form onSubmit={this.onSubmit} className="white">
<h5 className="grey-text text-darken-3">Edit Profile</h5>
<div className="input-field">
<label htmlFor="title">First Name: {profile.firstName}</label>
<input type="text" id="firstName" onChange={this.onChange} />
</div>
<div className="input-field">
<label htmlFor="title">Initials: {profile.initials}</label>
<input type="text" id="initials" onChange={this.onChange} />
</div>
<div className="input-field">
<label htmlFor="title">Last Name: {profile.lastName}</label>
<input type="text" id="lastName" onChange={this.onChange} />
</div>
<div className="input-field">
<button className="btn black z-depth-0">Submit</button>
{ }
</div>
</form>
</div>
)
}
};
const mapStateToProps = state => {
return {
auth: state.firebase.auth,
profile: state.firebase.profile,
};
};
const mapDispatchToProps = dispatch => {
return {
editProfile: edit => dispatch(editProfile(edit))}
}
export default compose(
connect(mapStateToProps, mapDispatchToProps),
firestoreConnect([
{ collection: "profile"}
])
)(UserProfile);
The component correctly displays the current user information.
This is the action I have set up:
return async (dispatch, getState, { getFirestore, getFirebase }) => {
const firebase = getFirebase();
const user = await firebase
.auth()
.currentUser
.updateProfile({
firstName: profile.firstName
});
dispatch({ type: "EDITPROFILE_SUCCESS", user })
console.log("user = " + profile.firstName);
};
}
When I log the entered profile.firstName I get the entered data.
And my reducer:
const editProfileReducer = (state, action) => {
switch (action.type) {
case "EDITPROFILE_ERROR":
return {
...state,
editError: action.error
};
case "EDITPROFILE_SUCCESS":
return {
...state
};
default:
return state;
}
}
export default editProfileReducer;
Any idea what I am missing here?
In your reducer change the like below
case "EDITPROFILE_SUCCESS":
return {
...state,
user:action.user
};
Above is if you want to update the whole user object
If you want to change only name then
Let’s assume that profileName is in user object then
case "EDITPROFILE_SUCCESS":
return {
...state,
user:Object.assign({}, state.user, profileName:action.user.profileName)
};
I am new to Redux and I appear to be having an issue. Once my action has been dispatched it is successful however the parent component does not get the updated state until another state change is made. If I click login then delete a character in the input field the state change is then triggered showing me the Menu. Any help/pointers are much appreciated, thanks.
Main (Parent):
import React, { Component } from 'react'
import { connect } from 'react-redux'
import Login from '../login'
import Menu from '../menu'
type Props = { token: string }
class Main extends Component<Props, {}> {
render() {
const { token } = this.props;
if (!token) {
return (
<Login />
)
}
return (
<Menu />
)
}
}
const mapStateToProps = (state) => ({
token: state.session.token,
})
export default connect(
mapStateToProps,
null,
)(Main)
Login (Child):
import React from 'react'
import { connect } from 'react-redux'
import { login } from '../../redux/session/session.actions'
import { View, StyleSheet } from 'react-native'
import { Button, FormLabel, FormInput, FormValidationMessage } from 'react-native-elements'
import styled from 'styled-components/native'
const Container = styled(View)`
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
`
const Wrapper = styled(View)`
width: 300;
`
type Props = { login: Function, error: string, loading: boolean };
type State = { email: string, password: string };
class Login extends React.PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = {
email: null,
password: null,
}
}
render() {
console.log('props', this.props);
console.log('state', this.state);
const { loading, error } = this.props;
return (
<Container>
<Wrapper>
<FormValidationMessage>{loading ? 'Loading...' : null}</FormValidationMessage>
<FormValidationMessage>{error ? 'Unable to login, please try again.' : null}</FormValidationMessage>
<FormLabel>Email:</FormLabel>
<FormInput onChangeText={text => this.setState({ email: text })} />
<FormLabel>Password:</FormLabel>
<FormInput secureTextEntry onChangeText={password => this.setState({ password })} />
<Button title='Login' onPress={this.login} />
</Wrapper>
</Container>
)
}
login = () => {
this.props.login(this.state.email, this.state.password);
}
}
const mapStateToProps = (state) => {
console.log(state);
return {
error: state.session.error,
loading: state.session.loading
}
}
const mapDispatchToProps = ({
login
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Login);
Reducer:
import {
LOGGING_IN,
LOGIN_SUCCESS,
LOGIN_FAILED
} from './session.types'
const initialState = {
loading: null,
error: null,
token: null,
}
export default (state = initialState, { type, payload }) => {
switch (type) {
case LOGGING_IN:
return {
...state,
loading: true
}
case LOGIN_SUCCESS:
return {
...state,
loading: false,
error: null,
token: payload.token
}
case LOGIN_FAILED:
return {
...state,
loading: false,
error: payload.error
}
default:
return state
}
}
Actions:
import { API_URL } from '../../../app-env'
import axios from 'axios'
import {
LOGGING_IN,
LOGIN_SUCCESS,
LOGIN_FAILED
} from './session.types'
export const login = (email, password) => (
async dispatch => {
console.log('here');
dispatch(loggingIn());
await axios.post(`${API_URL}/login`, {
email,
password
}).then(res => {
dispatch(loginSuccess(res.data.token))
}).catch(err => {
dispatch(loginFailed('Unable to login.'))
})
}
)
export const loggingIn = () => ({
type: LOGGING_IN,
})
export const loginSuccess = (token) => ({
type: LOGIN_SUCCESS,
payload: {
token
}
})
export const loginFailed = (error) => ({
type: LOGIN_FAILED,
payload: {
error
}
})
Since your problem is about Menu not render and Menu is under Main. So, we can ask the question what condition Main component not re-render. Luckily your example Main only depend on solely one props and no state. -I'll say your problem lies on props.token.- Since you initialize your token as null, I'll assume it hold object type. In that case, you need to make sure the token need to be a new object (new reference) else no re-render because react-redux connect by default will check the props changes before trigger the component underneath it.
EDIT: You mentioned the Menu not showing and the token is string, I can think of another reason Main not render is because connect is not trigger. You probably need to check the root of the store and make sure it has the new reference as your code only showing the reducer update state.session but not the state itself.
I'm new to Redux. It's really confusing to understand basic syntax. None of the bugs are found so It's hard to figure out what's wrong with my code.
It worked well last week, I don't remember what I have changed.
//child component
import React, { Component } from 'react';
import { SingleDatePicker } from 'react-dates';
import moment from 'moment';
class InputForms extends Component {
state = {
inputs: ['input-0'],
title: '',
tag: '',
createdAt: moment(),
imageLinks: [''],
calendarFocused: false,
error: '',
}
appendInput(e) {
const newInput = `input-${this.state.inputs.length}`;
this.setState({ inputs: this.state.inputs.concat([newInput]) });
}
onTitleChange = (e) => {
const title = e.target.value;
this.setState(() => ({ title }));
};
onTagChange = (e) => {
const tag = e.target.value;
this.setState(() => ({ tag }));
};
onImageLinkChange = (e) => {
const imageLinks = e.target.value;
this.setState(() => ({ imageLinks: this.state.imageLinks.concat([imageLinks]) }));
};
onDateChange = (createdAt) => {
if (createdAt) {
this.setState(() => ({ createdAt }));
}
};
onFocusChange = ({ focused }) => {
this.setState(() => ({ calendarFocused: focused }));
};
onSubmit = (e) => {
e.preventDefault();
if (!this.state.title || !this.state.imageLinks) {
this.setState(() => ({ error: '제목과 이미지링크를 입력해주세요' }));
} else {
this.setState(() => ({ error: '' }));
this.props.onSubmit({
title: this.state.title,
tag: this.state.tag,
createdAt: this.state.createdAt.valueOf(),
imageLinks: this.state.imageLinks,
});
}
}
render() {
return (
<div>
<form onSubmit={this.onSubmit}>
<input
type="text"
placeholder="제목을 입력하세요"
required
value={this.state.title}
onChange={this.onTitleChange}
/>
<input
type="text"
placeholder="태그를 입력하세요"
value={this.state.tag}
onChange={this.onTagChange}
/>
<SingleDatePicker
date={this.state.createdAt}
onDateChange={this.onDateChange}
focused={this.state.calendarFocused}
onFocusChange={this.onFocusChange}
numberOfMonths={1}
isOutsideRange={() => false}
/>
{this.state.inputs.map((input, key) => {
return <input
key={input}
type="text"
required
value={this.state.imageLinks}
onChange={this.onImageLinkChange}
placeholder={`이미지링크 ${key + 1}`}
/>
})}
<button>저장</button>
</form>
</div>
)
}
}
export default InputForms;
//parent component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import configureStore from '../store/configureStore';
import InputForms from './InputForms';
import { addPost } from '../actions/posts';
const store = configureStore();
class CreatePost extends Component {
onSubmit = (post) => {
this.props.addPost(post);
this.props.history.push('/');
};
render(){
return(
<div>
<InputForms onSubmit={this.onSubmit}/>
</div>
)
}
}
const mapDispatchToProps = (dispatch, props) => ({
addPost: (post) => dispatch(addPost(post))
});
export default connect(undefined, mapDispatchToProps)(CreatePost);
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import registerServiceWorker from './registerServiceWorker';
import AppRouter from './routers/AppRouter';
import 'normalize.css/normalize.css';
import './style/style.css';
import 'react-dates/lib/css/_datepicker.css';
import 'react-dates/initialize';
const store = configureStore();
const jsx = (
<Provider store={store}>
<AppRouter />
</Provider>
);
ReactDOM.render(jsx, document.getElementById('root'));
registerServiceWorker();
//action
import database from '../firebase/firebase';
//Add Posts
export const addPost = (post) => ({
type: 'ADD_POST',
post
});
//reducer
const postReducerDefaultState = [];
export default (state = postReducerDefaultState, action) => {
switch (action.type) {
case 'ADD_POST':
return [
...state,
action.post
];
default:
return state;
}
};
In your reducer, you return as below
return [ ...state, action.post];
Reducer doesnt return array, but instead returning objects. Secondly, action.post is a value, you need to assign this to key, something like below:
return { ...state, post: action.post };
I am trying to make an async react-select component with redux but somehow not able to get search results in the dropdown. Very new to this. Please help :)
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import Select from 'react-select';
import { fetchInstitutionsIfNeeded } from '../../actions/institutions';
class Signup extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null
};
this.getInstitutions = this.getInstitutions.bind(this);
this.onChange = this.onChange.bind(this);
}
onChange(input) {
this.setState({
value: input
});
}
getInstitutions(input) {
const { dispatch } = this.props;
if (!input) {
return Promise.resolve({ options: [] });
}
dispatch(fetchInstitutionsIfNeeded(input));
}
render() {
let options = this.props.options;
return (
<div>
<Select.Async
name="institute"
value={this.state.value}
autoload={false}
onChange={this.onChange}
loadOptions={this.getInstitutions}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
options: state.institutions.options
});
export default connect(mapStateToProps)(Signup);
Also options object is also properly formatted and is getting updated properly in redux store but not reflecting back in select async's dropdown.
Try this, we can also return from action but it breaks the whole idea of reducers.
// actions.js
export const getProducts = (input = '') => async (dispatch) => {
dispatch({
type: GET_PRODUCTS_PENDING,
payload: {},
});
try {
const response = await axios.get(`/api/v1/products/`);
dispatch({
type: GET_PRODUCTS_SUCCESS,
payload: response.data,
});
} catch (error) {
// handle errors
}
};
// components.jsx
class Signup extends PureComponent {
async getProductsForSelect(input) {
await this.props.getProducts(input);
return this.props.product.options;
}
render() {
const { handleSubmit } = this.props;
return (
<form onSubmit={handleSubmit}>
<AsyncSelect
isMulti
cacheOptions
defaultOptions
loadOptions={(e) => this.getProductsForSelect(e)}
/>
</form>
);
}
}