I am trying to learn react redux and I creating a small todo app, which has backend as REST server
most of the part is implemented, however, I am not able to understand how to pass a value from my input box to rest API call. I am able to successfully store the value of inputbox in redux state.
I am using react-thunk as middleware to handle API calls.
container
import { connect } from 'react-redux'
import {toggleAddTodoDialog, addNewTodo, handleChange} from '../actions'
import AddTodoDialog from '../components/add_todo_dialog';
class AddTodoContainer extends Component {
render() {
return (
<div>
<AddTodoDialog toggleAddTodoDialog={this.props.toggleAddTodoDialog}
addNewTodo = {this.props.addNewTodo}
newTodoList = {this.props.newTodoList}
handleChange = {this.props.handleChange}
is_add_todo_dialog_opened={this.props.is_add_todo_dialog_opened}/>
</div>
)
}
}
const mapStateToProps = (state) => {
return state
}
const bindActionsToDispatch = dispatch =>
(
{
toggleAddTodoDialog : (e) => dispatch(toggleAddTodoDialog(e)),
handleChange: (e) => dispatch(handleChange(e)),
addNewTodo : (e) => addNewTodo(e)
}
)
export default connect(mapStateToProps, bindActionsToDispatch)(AddTodoContainer)
component
export default class AddTodoDialog extends Component {
toggleAddTodoDialog = (e) => {
this.props.toggleAddTodoDialog(!this.props.is_add_todo_dialog_opened)
}
addNewTodo = (e) => {
this.props.addNewTodo()
this.toggleAddTodoDialog(e)
}
handleChange = (e) => {
this.props.handleChange(e)
}
render() {
return (
<div>
<Button color="primary" onClick={this.toggleAddTodoDialog}>Add new Todo</Button>
<Modal isOpen={this.props.is_add_todo_dialog_opened} >
{/* <Modal isOpen={false} > */}
<ModalHeader toggle={this.toggleAddTodoDialog}>Modal title</ModalHeader>
<ModalBody>
<FormGroup >
<Label for="Title">Task </Label>
<Input name="task"
value={this.props.newTodoList.task}
onChange={this.handleChange} />
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.addNewTodo}>OK</Button>{' '}
<Button color="secondary" onClick={this.toggleAddTodoDialog}>Cancel</Button>
</ModalFooter>
</Modal>
</div>
);
}
actions
export function addNewTodo() {
console.log("addNewTodo")
return function (dispatch) {
axios.post("http://localhost:5001/todos", 'task=' + this.props.addNewTodo.task)
.then(response => {
dispatch(_addTodoAction(response.data))
})
}
}
export function _addTodoAction(todos) {
console.log("_addTodoAction")
return {
type: 'ADD_TODO',
todos: todos
}
}
export function handleChange(event){
console.log("handleChange "+event)
return{
type: 'HANDLE_CHANGE',
event: event
}
}
reducer
case 'ADD_TODO':
console.log(action)
return {
...state,
todos: action.todos
}
You are dispatching like this:
addNewTodo : (e) => addNewTodo(e)
But your action doesn't take any arguments:
export function addNewTodo() {
console.log("addNewTodo")
return function (dispatch) {
axios.post("http://localhost:5001/todos", 'task=' + this.props.addNewTodo.task)
.then(response => {
dispatch(_addTodoAction(response.data))
})
}
}
If you want the value of the value of the input:
handleChange = (e) => {
this.props.handleChange(e.target.value)
}
And then dispatch:
handleChange: (value) => dispatch(handleChange(value)),
And then in the action:
export function handleChange(value) {
...
Related
I learn ReactJs and now I must have Firebase signin. I have a design question on how to detect when Firebase linkWithPopup finish. User press Button and my anonymous user Firebase UID will be turned into a Google credential one.
The linkWithPopup pops up and user select one Google account to use.
I must detect when this process finish or aborted.
Here is my code:
This method get's called when user click Button for Google signin:
onSocialLoginLink = provider => {
const { firebase, changeUserRole } = this.props;
firebase.auth.currentUser
.linkWithPopup(firebase[provider])
// .linkWithRedirect(this.props.firebase[provider])
.then(changeUserRole())
.then(this.fetchSignInMethods)
.catch(error => this.setState({ error }));
};
The problem I encounter is that changeUserRole() gets called before linkWithPopup returns since linkWithPopup of course run asynchronous. This means that my user get this new roll from changeUserRole() even if User select to abort signin.
I must detect when this process finish or aborted.
What would be a recommended best way to do this?
My ide is that if I can detect maybe when signin window looses focus and regain focus I could look at if Firebase user have changed provider to Google or is still anonymous user? Is this doable?
This is the Component that handle signin:
/* eslint-disable max-classes-per-file */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { AuthUserContext, withAuthorization } from '../../session';
import { withFirebase } from '../../firebase';
import { SIGN_IN_METHODS } from '../../constants/signinmethods';
import * as ROLES from '../../constants/roles';
import '../../styles/link-account.scss';
import { changeToUserRole } from '../../redux/userData/user.actions';
class LoginManagementBase extends Component {
constructor() {
super();
this.state = {
activeSignInMethods: [],
anonymousSignIn: null,
error: null,
};
}
componentDidMount() {
this.fetchSignInMethods();
}
fetchSignInMethods = () => {
const { firebase, authUser } = this.props;
const email = authUser.email === null ? 'none#guest.ac' : authUser.email;
firebase.auth
.fetchSignInMethodsForEmail(email)
.then(activeSignInMethods =>
this.setState({
activeSignInMethods,
anonymousSignIn: activeSignInMethods.length === 0,
error: null,
}),
)
.catch(error => this.setState({ error }));
};
onSocialLoginLink = provider => {
const { firebase, changeUserRole } = this.props;
firebase.auth.currentUser
.linkWithPopup(firebase[provider])
// .linkWithRedirect(this.props.firebase[provider])
.then(changeUserRole())
.then(this.fetchSignInMethods)
.catch(error => this.setState({ error }));
};
onDefaultLoginLink = password => {
const { firebase, authUser } = this.props;
const credential = firebase.emailAuthProvider.credential(authUser.email, password);
firebase.auth.currentUser
.linkAndRetrieveDataWithCredential(credential)
.then(this.fetchSignInMethods)
.catch(error => this.setState({ error }));
};
onUnlink = providerId => {
const { firebase } = this.props;
firebase.auth.currentUser
.unlink(providerId)
.then(this.fetchSignInMethods)
.catch(error => this.setState({ error }));
};
render() {
const { activeSignInMethods, error } = this.state;
const { saveRolesErr, isSavingRole } = this.props;
// if (isSavingRole) return null;
return (
<div className="provideToggler">
<h1>
You are signed in Anonymously!
<br />
Changes you do is only saved in this browser.
<br /> If you want to access your progress anywhere please sign in below!
</h1>
<ul>
{SIGN_IN_METHODS.map(signInMethod => {
const onlyOneLeft = activeSignInMethods.length === 1;
const isEnabled = activeSignInMethods.includes(signInMethod.id);
return (
<li key={signInMethod.id}>
{signInMethod.id === 'password' ? (
<DefaultLoginToggle
// accountEmail={this.props.authUser.email}
onlyOneLeft={onlyOneLeft}
isEnabled={isEnabled}
signInMethod={signInMethod}
onLink={this.onDefaultLoginLink}
onUnlink={this.onUnlink}
/>
) : (
<SocialLoginToggle
onlyOneLeft={onlyOneLeft}
isEnabled={isEnabled}
signInMethod={signInMethod}
onLink={this.onSocialLoginLink}
onUnlink={this.onUnlink}
/>
)}
</li>
);
})}
</ul>
<h1 style={{ color: 'red' }}>
{error && error.message}
{saveRolesErr && saveRolesErr.message}
</h1>
</div>
);
}
}
const SocialLoginToggle = ({ onlyOneLeft, isEnabled, signInMethod, onLink, onUnlink }) =>
isEnabled ? (
<button type="button" onClick={() => onUnlink(signInMethod.id)} disabled={onlyOneLeft}>
Unlink <i className={signInMethod.icon} aria-hidden="true" /> {signInMethod.name} sign in
</button>
) : (
<button type="button" onClick={() => onLink(signInMethod.provider)}>
Link <i className={signInMethod.icon} aria-hidden="true" /> {signInMethod.name} sign in
</button>
);
// TODO This is not in use but might use it later
class DefaultLoginToggle extends Component {
constructor() {
super();
this.state = { passwordOne: '', passwordTwo: '' };
}
onSubmit = event => {
const { passwordOne } = this.state;
const { onLink } = this.props;
event.preventDefault();
onLink(passwordOne);
this.setState({ passwordOne: '', passwordTwo: '' });
};
onChange = event => {
this.setState({ [event.target.name]: event.target.value });
};
render() {
const { signInMethod } = this.props;
const { passwordOne, passwordTwo } = this.state;
const isInvalid = passwordOne !== passwordTwo || passwordOne === '';
return (
<form onSubmit={this.onSubmit}>
Link <i className={signInMethod.icon} aria-hidden="true" /> {signInMethod.name} sign in
<input
name="passwordOne"
value={passwordOne}
onChange={this.onChange}
type="password"
placeholder="Password for email sign in"
/>
<input
name="passwordTwo"
value={passwordTwo}
onChange={this.onChange}
type="password"
placeholder="Confirm New Password"
/>
<button disabled={isInvalid} type="submit">
Save password for email sign in
</button>
</form>
);
}
}
const mapDispatchToProps = dispatch => ({
changeUserRole: () => dispatch(changeToUserRole()),
});
const mapStateToProps = state => {
return {
isSavingRole: state.user.isSavingRoles,
saveRolesErr: state.user.saveRolesErrMsg,
};
};
const enhance = compose(withFirebase, connect(mapStateToProps, mapDispatchToProps));
const LoginManagement = enhance(LoginManagementBase);
const LinkAccounts = () => (
<AuthUserContext.Consumer>
{authUser => (
<div>
<LoginManagement authUser={authUser} />
</div>
)}
</AuthUserContext.Consumer>
);
const condition = authUser => authUser && authUser.roles.includes(ROLES.ANON);
export default withAuthorization(condition)(LinkAccounts);
if that's the problem you can add a condition to check than change the role
const { firebase, changeUserRole } = this.props;
firebase.auth.currentUser
.linkWithPopup(firebase[provider])
.then(res=>{
if(res.credential){changeUserRole()}
})
.then(this.fetchSignInMethods)
.catch(error => this.setState({ error }));
};```
I imagine this is a basic in react but I'm not sure how to get it to work, basically when I delete, create or edit anything in my components I want the change to happen in realtime without refreshing the page, I've achieved it at some level with the search function but not entirely sure how to do with for the delete function for example:
Here is what I'm working with, how would I get this to work with my axios delete function?
Thanks
import { connect } from 'react-redux';
import { fetchTournaments } from '../actions/tournaments';
import Item from './Item';
import EditTournament from './EditTournament';
import axios from 'axios';
import '../styles/Item.css';
class SearchAndDisplay extends React.PureComponent {
componentDidMount() {
this.props.fetchTournaments();
}
state = {
searchCriteria: '',
isLoading: false
};
handleChange = event => {
this.setState({
searchCriteria: event.target.value
});
};
async handleDelete(id) {
const url = `http://localhost:4000/tournaments/`;
await axios
.delete(url + id)
.then(res => {
console.log(res.data);
})
.catch(err => {
console.log(err);
});
}
formatDate(date) {
let options = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
};
let newDate = new Date(Date.parse(date));
let format = new Intl.DateTimeFormat('default', options).format(newDate);
return format;
}
handleChange = event => {
this.setState({ searchCriteria: event.target.value });
};
renderList() {
let tournmentsArray = this.props.tournaments;
const filterTournaments = tournmentsArray.filter(item =>
item.name.includes(this.state.searchCriteria)
);
if (filterTournaments === undefined || filterTournaments.length === 0) {
return (
<React.Fragment>
<div className="notFound">
Something went wrong.
<br />
<button
className="notFoundButton"
onClick={() => {
this.setState({ searchCriteria: '' });
}}
>
Retry
</button>
</div>
</React.Fragment>
);
} else {
return filterTournaments.map(item => (
<Item
key={item.name}
name={item.name}
organizer={item.organizer}
participants={Object.values(item.participants)}
game={item.game}
start={this.formatDate(item.startDate)}
>
<div className="buttonBar">
<EditTournament id={item.id} />
<button
className="button"
onClick={() => {
if (
window.confirm('Are you sure you want to delete this item?')
) {
this.handleDelete(item.id);
}
}}
>
Delete
</button>
</div>
</Item>
));
}
}
render() {
return (
<div className="container">
<input
onChange={this.handleChange}
className="input"
placeholder="Search..."
id="searchField"
value={this.state.searchCriteria}
/>
<div className="row">{this.renderList()}</div>
</div>
);
}
}
function mapStateToProps({ tournaments }) {
return {
tournaments: Object.values(tournaments).flat()
};
}
export default connect(mapStateToProps, {
fetchTournaments
})(SearchAndDisplay);
unlike delete the create and edit data is handled by redux like so:
Create tournament:
import { reduxForm, Field } from 'redux-form';
import '../styles/promptForms.css';
import '../styles/Header.css';
import { connect } from 'react-redux';
import { createTournaments } from '../actions/tournaments';
class CreateTournamentPromptFrom extends React.Component {
constructor(props) {
super(props);
this.state = {
showHide: false
};
}
createTournamentButton() {
return (
<div>
<button
className="genericButton"
onClick={() => this.setState({ showHide: true })}
>
CREATE TOURNAMENT
</button>
</div>
);
}
renderInput = ({ input, label }) => {
return (
<div>
<label>{label}</label>
<br />
<input className="promptInput" {...input} autoComplete="off" />
</div>
);
};
onSubmit = formValues => {
this.props.createTournaments(formValues);
};
render() {
const { showHide } = this.state;
return (
<React.Fragment>
<div className={`overlay ${showHide ? 'toggle' : ''} `} />
<div className={`promptBox ${showHide ? 'toggle' : ''} `}>
<h3>localhost:3000 says</h3>
<form onSubmit={this.props.handleSubmit(this.onSubmit)}>
<Field
name="name"
component={this.renderInput}
label="Enter Tournament:"
/>
<button className="okayButton">OK</button>
</form>
<button
className="cancelButton"
onClick={() => this.setState({ showHide: false })}
>
Cancel
</button>
</div>
{this.createTournamentButton()}
</React.Fragment>
);
}
}
const formWrapped = reduxForm({
form: 'promptForm'
})(CreateTournamentPromptFrom);
export default connect(null, { createTournaments })(formWrapped);
actions:
import {
FETCH_TOURNAMENTS,
FETCH_TOURNAMENT,
CREATE_TOURNAMENT,
EDIT_TOURNAMENT
} from './types';
import { API_TOURNAMENTS_URL } from '../constants/api';
import axios from 'axios';
export const fetchTournaments = () => async dispatch => {
const response = await axios.get(API_TOURNAMENTS_URL);
dispatch({
type: FETCH_TOURNAMENTS,
payload: response.data.flat()
});
};
export const fetchTournament = id => async dispatch => {
const response = await axios.get(`http://localhost:4000/tournaments/${id}`);
dispatch({ type: FETCH_TOURNAMENT, payload: response.data });
};
export const createTournaments = formValues => async dispatch => {
const response = await axios.post(API_TOURNAMENTS_URL, {
...formValues
});
dispatch({ type: CREATE_TOURNAMENT, payload: response.data });
};
export const editTournaments = (id, formValues) => async dispatch => {
const response = await axios.patch(
`http://localhost:4000/tournaments/${id}`,
formValues
);
dispatch({ type: EDIT_TOURNAMENT, payload: response.data });
};
reducers:
import _ from 'lodash';
import {
FETCH_TOURNAMENT,
CREATE_TOURNAMENT,
FETCH_TOURNAMENTS,
EDIT_TOURNAMENT,
DELETE_TOURNAMENT
} from '../actions/types';
export default (state = {}, action) => {
switch (action.type) {
case FETCH_TOURNAMENT:
return { ...state, [action.payload.id]: action.payload };
case FETCH_TOURNAMENTS:
return { ...state, [action.payload.id]: action.payload };
case CREATE_TOURNAMENT:
return { ...state, [action.payload.id]: action.payload };
case EDIT_TOURNAMENT:
return { ...state, [action.payload.id]: action.payload };
case DELETE_TOURNAMENT:
return _.omit(state, action.payload);
default:
return state;
}
};
To "optimistically" delete an item from state you'll need to immediately delete it from state to reflect the change right away in the UI. BUT you will need to add extra redux state to "hold" a pending delete with your backend. When the delete is successful you clear the held delete, if it fails you clear the held delete and add it back in to your regular data (and perhaps display some error message or toast, etc..).
I see you don't do the delete via redux, so use local component state and you'll have to filter your tournament data when rendering.
class SearchAndDisplay extends PureComponent {
componentDidMount() {
this.props.fetchTournaments();
}
state = {
searchCriteria: "",
isLoading: false,
optimisticTournaments: null // <-- state to hold temp "deleted" data
};
handleChange = event => {
this.setState({
searchCriteria: event.target.value
});
};
async handleDelete(id) {
console.log("delete id", id);
// optimistically remove element
this.setState({
optimisticTournaments: this.props.tournaments.filter(
item => item.id !== id
)
});
await axios
.delete(url + id)
.then(res => {
console.log(res.data);
// Need to create a call back to let parent know element was deleted
this.props.deleteSuccess(id);
})
.catch(err => {
console.log(err);
alert("Failed to delete");
})
.finally(() => {
this.setState({ optimisticTournaments: null });
});
}
formatDate(date) {
let options = {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false
};
let newDate = new Date(Date.parse(date));
let format = new Intl.DateTimeFormat("default", options).format(newDate);
return format;
}
handleChange = event => {
this.setState({ searchCriteria: event.target.value });
};
renderList() {
let tournmentsArray =
this.state.optimisticTournaments || this.props.tournaments;
const filterTournaments = tournmentsArray.filter(item =>
item.name.includes(this.state.searchCriteria)
);
if (filterTournaments === undefined || filterTournaments.length === 0) {
return (
<React.Fragment>
<div className="notFound">
Something went wrong.
<br />
<button
className="notFoundButton"
onClick={() => {
this.setState({ searchCriteria: "" });
}}
>
Retry
</button>
</div>
</React.Fragment>
);
} else {
return filterTournaments.map(item => (
<Item
key={item.name}
name={item.name}
organizer={item.organizer}
participants={Object.values(item.participants)}
game={item.game}
start={this.formatDate(item.startDate)}
>
<div className="buttonBar">
<EditTournament id={item.id} />
<button
className="button"
onClick={() => {
if (
window.confirm("Are you sure you want to delete this item?")
) {
this.handleDelete(item.id);
}
}}
>
Delete
</button>
</div>
</Item>
));
}
}
render() {
return (
<div className="container">
<input
onChange={this.handleChange}
className="input"
placeholder="Search..."
id="searchField"
value={this.state.searchCriteria}
/>
<div className="row">{this.renderList()}</div>
</div>
);
}
}
Here is how you can do using forceUpdate() (Since you don't want to use state):
import React, { Component } from 'react';
import { render } from 'react-dom';
class App extends Component {
constructor() {
super();
this.items = [
{id: 1, name: "item 1"},
{id: 2, name: "item 2"},
];
this.handleDelete = this.handleDelete.bind(this);
}
handleDelete(index) {
const newState = [...this.items];
delete newState[index];
// or
// newState.splice(index, 1);
this.items = newState;
this.forceUpdate();
}
render() {
return (
<div>
{
this.items.map((item, index) => {
return (
<div>
{item.name}
<button onClick={() => this.handleDelete(index)}>Delete item</button>
</div>
)
})
}
</div>
);
}
}
render(<App />, document.getElementById('root'));
Just pass the index in the map method :
...map((item, index) => ...);
Do it in the then() after your axios call.
Note that the documentation highly advice you to avoid using forceUpdate() when you can, so you really should use a state for this, I don't see any good reason for you not to use it here.
Here is a quick repro on Stackblitz.
I am having 2 issues:
Initially I can add clients to the empty array through action creators and my reducer. However, whenever I delete the items from the list and try to add new clients to it, it gives me an error: TypeError: Invalid attempt to spread non-iterable instance.
When I said I am deleting the items, what really happens is I create the clients, and then when I click on the delete button next to one of them, all of the clients delete. There is not error in the console, but I just want to delete the specific client with the corresponding id.
Here is my code!
Clients.js
import React, { Component } from 'react'
import AddClient from './AddClient'
import {connect} from 'react-redux'
import {deleteClient} from '../../store/actions/clientActions'
class Clients extends Component {
handleClick = (id) => {
console.log(id)
this.props.deleteClient(id)
}
render() {
const {clientList} = this.props
return (
<div className="container mt-5">
<h2>Here Are Your List of Clients...</h2>
{clientList && clientList.map(client => {
return(
<div key={client.id}>
<div>
Client Name: {client.name} | Client Price: {client.price}
<button onClick={() => {this.handleClick(client.id)}}>Delete</button>
</div>
</div>
)
})}
<AddClient/>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
clientList : state.clients.clientList,
}
}
const mapDispatchToProps = (dispatch) => {
return{
deleteClient : (id) => dispatch(deleteClient(id))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Clients)
Actions:
export const addClient = (client) => {
return(dispatch, getState) => {
dispatch({type: 'ADD CLIENT', client})
}
}
export const deleteClient = (id) => {
return(dispatch, getState) => {
dispatch({type: 'DELETE CLIENT', id})
}
}
Reducer:
const initState = {
clientList: []
}
const clientReducer = (state = initState, action) => {
switch (action.type) {
case 'ADD CLIENT' :
action.client.id = Math.random();
let clientList = [...state.clientList, action.client];
clientList.sort((a, b) => a.name.localeCompare(b.name));
return {
clientList
};
case 'DELETE CLIENT' :
const id = action.id;
clientList = state.clientList.filter(client =>
{return client.id !== id});
return clientList;
default : return state;
}
}
export default clientReducer
Lastly, this is AddClient.js
import React, { Component } from 'react'
import {connect} from 'react-redux'
import {addClient} from '../../store/actions/clientActions'
class AddClient extends Component {
state = {
id: null,
name: null,
price: null,
}
handleChange = (e) => {
this.setState({
[e.target.id] : e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault();
this.props.addClient(this.state);
e.target.reset();
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit} className="mt-5">
<h3>Add a new client:</h3>
<label htmlFor="name">Client Name: </label>
<input type="text" id="name" onChange={this.handleChange}/><br/>
<label htmlFor="price">Client Price: </label>
<input type="text" id="price" onChange={this.handleChange}/> <br/>
<button className="btn btn-primary">Add Client</button>
</form>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
addClient: (client) => dispatch(addClient(client))
}
}
export default connect(null, mapDispatchToProps)(AddClient)
Thank you for all the help, I am fairly new to React and Redux. Let me know if there is any other code you would like to see.
Here's how you can accomplish the delete:
export const deleteClient = (id) => {
const index = find the index of the client you want to delete from the array
return(dispatch, getState) => {
dispatch({type: 'DELETE CLIENT', index})
}
}
case 'DELETE CLIENT' :
return {
...state,
clientList: [
...state.clientList.slice(0, action.index),
...state.clientList.slice(action.index + 1)
]
}
I figured it out, the problem is within my clientReducer.js
This needs to change:
case 'DELETE CLIENT' :
const id = action.id;
clientList = state.clientList.filter(client =>
{return client.id !== id});
return clientList;
to...
case 'DELETE CLIENT' :
const id = action.id;
let newClientList = state.clientList.filter(client => {
return id !== client.id;
})
return {clientList : newClientList};
case 'DELETE CLIENT' :
const id = action.id;
const clientList = state.clientList.filter(client =>
{return client.id !== id});
return {
...state,
clientList
}
You're currently returning just an array, instead of an object. Since that's probably the only thing you have in your redux store right now, it's not breaking (in the ADD action), but you probably want to apply the previous state first, then add your newly filtered clientlist to the state you're returning.
I'm using Redux and material-ui
I'm trying to run Dialog with <Slide direction="up"/> animation using this attribute: TransitionComponent
email is a state value that came from reducer and changes when I enter value on TextField
When I try to enter some value on, animation plays but, I want to play it only one time.
interface IProps extends WithStyles<typeof styles> {
// ...
setEmail: (email: string) => void;
email: string;
// ...
}
const LoginDialog: React.SFC<IProps> = props => {
const handleClose = () => {
props.setIsOpen(false);
};
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
props.setPassword(event.target.value);
};
const handlePasswordVisibility = () => {
props.setPasswordVisibility(!props.passwordVisibility);
};
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
props.setEmail(event.target.value);
};
return (
<div>
<Dialog
open={props.isOpen}
//dialog plays animation when props.isOpen changes
TransitionComponent={props => <Slide direction="up" {...props} />}
onClose={handleClose}
aria-labelledby="login-dialog-slide-title"
aria-describedby="login-dialog-slide-description"
disableBackdropClick={true}
keepMounted
>
<DialogTitle id="login-dialog-slide-title">
<FormattedMessage id="logindialog_title" />
</DialogTitle>
<DialogContent>
<TextField value={props.email} onChange={handleEmailChange} autoFocus type="email" label={<FormattedMessage id="logindialog_email" />}/>
<TextField type="password" label={<FormattedMessage id="logindialog_password" />} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
<FormattedMessage id="logindialog_cancle" />
</Button>
<Button onClick={handleClose} color="primary">
<FormattedMessage id="logindialog_ok" />
</Button>
</DialogActions>
</Dialog>
</div>
);
};
export default withStyles(styles)(withRouter(LoginDialog));
I updated my container which has mapStateToProps and action, reducer for email
and also you can see my full code here: codesandbox.io/s/nkrmw3wjxj
import { connect } from "react-redux";
import { ICombineReducersState } from "../../../reducers";
import LoginDialog from "./LoginDialog";
import {
setIsOpen,
setPassword,
setPasswordVisibility,
setEmail,
setNickname,
DuplicatedEmail,
setIsEmailDuplicated
} from "../../../actions";
const mapStateToProps = (state: ICombineReducersState) => ({
isOpen: state.open.isOpen,
password: state.password.password,
passwordVisibility: state.password.passwordVisibility,
email: state.email.email,
isPasswordError: state.password.isPasswordError,
isEmailError: state.email.isEmailError,
isEmailDuplicated: state.email.isEmailDuplicated
});
const mapDispatchToProps = (dispatch: any) => ({
setIsOpen: (isOpen: boolean) => dispatch(setIsOpen(isOpen)),
setPassword: (password: string) => dispatch(setPassword(password)),
setPasswordVisibility: (passwordVisibility: boolean) =>
dispatch(setPasswordVisibility(passwordVisibility)),
setEmail: (email: string) => dispatch(setEmail(email)),
setNickname: (nickname: string) => dispatch(setNickname(nickname)),
DuplicatedEmail: () => dispatch(DuplicatedEmail()),
setIsEmailDuplicated: (isEmailDuplicated: boolean) =>
dispatch(setIsEmailDuplicated(isEmailDuplicated))
});
export const LoginDialogContainer = connect(
mapStateToProps,
mapDispatchToProps
)(LoginDialog);
export const SET_EMAIL = "SET_EMAIL";
export const SET_IS_EMAIL_DUPLICATED = "SET_IS_EMAIL_DUPLICATED";
import axios from "axios";
import config from "../config";
export interface IEmailAction {
email: string;
type: string;
isEmailDuplicated: boolean;
}
export const setEmail = (email: string) => {
return {
email,
type: SET_EMAIL,
} as IEmailAction;
};
export const setIsEmailDuplicated = (isEmailDuplicated: boolean) => {
return {
isEmailDuplicated,
type: SET_IS_EMAIL_DUPLICATED,
} as IEmailAction;
}
export const DuplicatedEmail = () => (dispatch: any):boolean => {
axios.get(`${config.REACT_APP_SERVER_URL}/users/email`)
.then(res => {
if (res.data.message.length >= 1) {
return dispatch(setIsEmailDuplicated(true));
}
})
.catch(err => {
console.log(err.response)
})
return dispatch(setIsEmailDuplicated(false));
}
import { IEmailAction, SET_EMAIL, SET_IS_EMAIL_DUPLICATED } from "../actions";
export interface IEmailState {
email: string;
isEmailError: boolean;
isEmailDuplicated: boolean;
}
const createEmpty = () => ({
email: "",
isEmailError: false,
isEmailDuplicated: false,
});
const emailRegex = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*#[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
export const emailReducer = (state = createEmpty(), action: IEmailAction) => {
switch (action.type) {
case SET_EMAIL: {
return {
email: action.email,
isEmailError: !validateEmail(action.email),
isEmailDuplicated: false,
} as IEmailState;
}
case SET_IS_EMAIL_DUPLICATED: {
return {
email: state.email,
isEmailError: true,
isEmailDuplicated: action.isEmailDuplicated,
} as IEmailState;
}
default:
return state;
}
};
const validateEmail = (email: string):boolean => {
if (emailRegex.test(email)) {
return true;
}
return false;
}
Please let me know if you need more info about it.
Thanks.
The following line was the problem:
TransitionComponent={props => <Slide direction="up" {...props} />}
By defining this inline it means that the TransitionComponent will look like a new component type with each re-render which then causes that portion of the Dialog to re-mount and therefore redo the transition.
This is easily fixed by defining this outside of your component function as a stable component type (I've called it TransitionComponent below) and then using this for the TransitionComponent Dialog prop:
const TransitionComponent = props => <Slide direction="up" {...props} />;
const LoginDialog: React.SFC<IProps> = props => {
...
return (
<div>
<Dialog
open={props.isOpen}
TransitionComponent={TransitionComponent}
...
};
I am getting “TypeError: Cannot read property 'props' of undefined”
when i try to fire my function onChange from a deeper component. I can fire the action from the DemoForm component, but then I cannot pass in my value as it becomes undefined so, I am trying to make a function that takes in the event information and then I am firing my action but it says props is undefined, when I do a debugger and check on the console, its all there
// App.js
class App extends Component {
constructor(props) {
super(props)
}
handleThis(e){
this.props.SomeAction
}
render() {
return (
<div className="App">
<DemoForm state={this.props} someFunction={this.handleThis }/>
<AnotherForm/>
</div>
);
}
}
const mapStateToProps = (reduxState) => {
return reduxState;
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(actionCreators, dispatch);
}
export default connect(
mapStateToProps, mapDispatchToProps
)(App)
// Demo.js
let DemoForm = ({ handleSubmit, submitting, state }) =>
<form onSubmit={handleSubmit(showResults)}>
<Field name="value" label="Value" component={RenderInput} onChange={(e) => this.props.someFunction(e.target.value) } />
<button type="submit"> Submit </button>
{console.log("Demo state >>>>> ", {state})}
</form>
DemoForm = reduxForm({
form: 'demo',
destroyOnUnmount: false,
validate
})(DemoForm)
export default DemoForm
// RenderInput
const RenderInput = createRenderer((input, label, onChange) => {
return <input {...input}/>
})
export default RenderInput
// createRenderer
const createRenderer = render => ({ input, meta, label, ...rest }) => {
return (
<div>
{/* <pre> {JSON.stringify(input, null, 2) }</pre> */}
<label> {label}</label>
{render(input, label, rest)}
{
meta.touched &&
<span className="text-danger"> {meta.error} </span>
}
</div>
)
}
export default createRenderer
// REDUCER
const initialState = {
todos: [],
count: 0,
demoPercent: 0,
anotherPercent : 0
}
export default function rootReducer(state = initialState, action) {
if(action.type === "INC"){
console.log("incrementing count")
let newState = {...state}
newState.count++
return {
...newState
}
}
if(action.type === "GET_PERCENT"){
console.log("getting balance percent", action.payload)
let newState = {...state}
newState.demoPercent = action.payload;
newState.anotherPercent = 100 - action.payload;
return {
...newState
}
}
return state;
}
// ACTION
export function increase(){
console.log("i am INC action firing")
return {
type: "INC"
}
}
export function getPercent(value){
console.log(value) //value is undefined
return {
type: "GET_PERCENT",
paypoad : value
}
}
You need to bind the handler in your controller, to do that just change your App component constructor to:
constructor(props) {
super(props);
this.handleThis = this.handleThis.bind(this);
}
You have to bind your function... And the best way to bind function is to use arrow functions
render() {
return (
<div className="App">
<DemoForm state={this.props} someFunction={(e) => this.handleThis(e)}/>
<AnotherForm/>
</div>
);
}
}