Unit test redux form handleSubmit - reactjs

I try to use redux-form in my app, how to test handleSubmit by redux-form? I use enzyme, and previously I just found some component, simulate click, and check the call args, and how much click was trigger. When I switch to redux-form,it is not quite clear for me how to write right unit test and check handleSubmit.
export const validate = device => {
const errors = {}
if (device.name && device.name.length > constants.defaultMaxTextFieldLength) {
errors.name = <FormattedMessage id={tooLongErrorMessage} />
}
if (!device.name)
errors.name = <FormattedMessage id={emptyErrorMessage} />
return errors
}
export class EditDevice extends Component {
static propTypes = {...}
update = device => {
device.miConfiguration.isMiEnabled = device.miConfiguration.miConfigurationType !== MiConfigurationTypes.AccessPointOnly
this.props.update(device).then(({success, ...error}) => {
if (!success)
throw new SubmissionError(error)
this.returnToList()
})}
returnToList = () => this.props.history.push({pathname: '/devices', state: {initialSkip: this.props.skip}})
render = () => {
let {isLoadingInProgress, handleSubmit, initialValues: {deviceType} = {}, change} = this.props
const actions = [
<Button
name='cancel'
onClick={this.returnToList}
>
<FormattedMessage id='common.cancel' />
</Button>,
<Button
name='save'
onClick={handleSubmit(this.update)}
color='primary'
style={{marginLeft: 20}}
>
<FormattedMessage id='common.save' />
</Button>]
return (
<Page title={<FormattedMessage id='devices.deviceInfo' />} actions={actions} footer={actions}>
<form onSubmit={handleSubmit(this.update)}>
{isLoadingInProgress && <LinearProgress mode='indeterminate'/>}
<Grid container>
<Grid item xs={12} sm={6} md={4} >
<Field
component={renderTextField}
name='name'
label={<FormattedMessage id='name' />}
/>
</Grid>
....
</Grid>
</form>
</Page>
)
}
}
export const mapStateToProps = (state, {match: {params: {deviceId}}}) => {
let initialValues = deviceId && state.entities.devices[deviceId]
return {
initialValues,
deviceId,
isLoadingInProgress: state.devices.isLoadingInProgress,
skip: state.devices.skip,
form: `device-${deviceId}`,
}
}
export const mapDispatchToProps = (dispatch, {match: {params: {deviceId}}}) => ({
init: () => dispatch(DeviceActions.get(deviceId)),
update: device => dispatch(DeviceActions.createOrUpdate(device)),
})
export default compose(
connect(mapStateToProps, mapDispatchToProps),
reduxForm({validate, enableReinitialize: true, keepDirtyOnReinitialize: true}),
)(EditDevice)
Previous unit test.
describe('EditDevice', () => {
let init, handleSubmit, page, push
beforeEach(() => page = shallow(<EditDevice
handleSubmit={handleSubmit = sinon.spy()}
deviceId={deviceId}
history={{push: push = sinon.spy()}}
skip={skip}
/>))
......
it('should call push back to list on successful response', async () => {
let update = sinon.stub().resolves({success: true})
page.setProps({update})
page.find(Field).findWhere(x => x.props().name === 'name').simulate('change', {}, 'good name')
await page.find(Page).props().footer.find(x => x.props.name === saveButtonName).props.onClick()
push.calledOnce.should.be.true
push.calledWith({pathname: '/devices', state: {initialSkip: skip}}).should.be.true
})
describe('mapStateToProps', () => {
const deviceId = 123
const device = {}
const isEnabled = true
const isLoadingInProgress = {}
let props
let skip = {}
beforeEach(() => props = mapStateToProps({devices: {isLoadingInProgress, skip}, entities: {devices: {[deviceId]: device}}}, {match: {params: {deviceId}}}))
it('should pass deviceId, form, isLoadingInProgress and skip from state', () => {
props.deviceId.should.be.equal(deviceId)
props.isLoadingInProgress.should.be.equal(isLoadingInProgress)
props.skip.should.be.equal(skip)
props.form.should.be.equal(`device-${deviceId}`)
})
})
describe('mapDispatchToProps', () => {
const response = {}
const deviceId = 123
let props
beforeEach(() => props = mapDispatchToProps(x=> x, {match: {params: {deviceId}}}))
it('init should call get from DeviceActions', () => {
sinon.stub(DeviceActions, 'get').returns(response)
props.init(deviceId).should.be.equal(response)
DeviceActions.get.calledOnce.should.be.true
DeviceActions.get.args[0][0].should.be.equal(deviceId)
DeviceActions.get.restore()
})
it('update should call createOrUpdate from DeviceActions', () => {
const device = {}
sinon.stub(DeviceActions, 'createOrUpdate').returns(response)
props.update(device).should.be.equal(response)
DeviceActions.createOrUpdate.calledOnce.should.be.true
DeviceActions.createOrUpdate.args[0][0].should.be.equal(device)
DeviceActions.createOrUpdate.restore()
})
})

Related

Don't update child component

I have two children Components, when I onChange in first children, then the second children re render, I don't want to the second children re render. Online code example:
https://codesandbox.io/s/ji-ben-antd-4-24-0-forked-efg56l?file=/demo.tsx
const ChildA = (props: {
name: string;
changeValue: (key: string, value: any) => void;
}) => {
const { name, changeValue } = props;
return (
<Input
value={name}
onChange={(e) => {
changeValue("name", e.target.value);
}}
/>
);
};
const ChildB = (props: {
age: number;
changeValue: (key: string, value: any) => void;
}) => {
const { age, changeValue } = props;
console.log("==when I change name====, this component re-render");
return (
<InputNumber
value={age}
onChange={(e) => {
changeValue("age", e);
}}
/>
);
};
const App: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [saveValue, setSaveValue] = useState({
name: "wang",
age: 18
});
const showModal = () => {
setIsModalOpen(true);
};
const handleOk = () => {
// send value
console.log("====saveValue==", saveValue);
setIsModalOpen(false);
};
const handleCancel = () => {
setIsModalOpen(false);
};
const changeValue = (key: string, value: any) => {
const newValue = JSON.parse(JSON.stringify(saveValue));
newValue[key] = value;
setSaveValue(newValue);
};
return (
<>
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
<Modal
title="Basic Modal"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
>
<ChildA name={saveValue?.name} changeValue={changeValue} />
<ChildB age={saveValue?.age} changeValue={changeValue} />
</Modal>
</>
);
};
When I change the name ,I don`t want to Child B re-render
The actual situation is that there are many sub-components in a Modal. When you click OK, the value of the sub-component is obtained, saved and sent to the server. If you have good design component ideas, please share
I don't want to the second children re render.
Wrap ChildB with React.memo for a basic memoization.
const ChildB = memo(...);
Wrap the changeValue function with React.useCallback to persist the instance.
const changeValue = useCallback(...);
Slightly modify the changeValue function so it does not use the saveValue as a dependency.
setSaveValue((prev) => {
const newValue = JSON.parse(JSON.stringify(prev));
newValue[key] = value;
return newValue;
});
Codesandbox demo

React & Ant Design - Submit form before switching tabs

I have an ant design tab group with an ant design form in each tab. I would like that upon switching tabs, the user is prompted to submit their changes. If the user selects yes, then the data should be submitted. We switch tabs only if the response comes back as a success, or the user opted not to save.
The forms are all child components, which means the parent needs to somehow indicate that the tab is switching, and tell the child to submit their form.
I can achieve this with the useImperativeHandle hook and forwardRef but I'm wondering if there is some non-imperative way to achieve this?
Here is a stripped down example, not checking if the form is dirty, and just using the native confirm popup. There is also an async function to simulate submitting the form, which will randomly succeed or error.
Stackblitz: https://stackblitz.com/edit/react-ts-zw2cgi?file=my-form.tsx
The forms:
export type FormRef = { submit: (data?: Data) => Promise<boolean> };
export type Data = { someField: string };
const MyForm = (props: {}, ref: Ref<FormRef>) => {
const [form] = useForm<Data>();
useImperativeHandle(ref, () => ({ submit }));
async function submit(data?: Data): Promise<boolean> {
if (!data) data = form.getFieldsValue();
const res = await submitData(data);
return res;
}
return (
<Form form={form} onFinish={submit}>
<Form.Item name="someField">
<Input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>
);
};
export default forwardRef(MyForm);
The parent component with the tabs:
const App: FC = () => {
const [activeKey, setActiveKey] = useState('tabOne');
const formOneRef = useRef<FormRef>();
const formTwoRef = useRef<FormRef>();
async function onChange(key: string) {
if (confirm('Save Changes?')) {
if (activeKey === 'tabOne' && (await formOneRef.current.submit()))
setActiveKey(key);
if (activeKey === 'tabTwo' && (await formTwoRef.current.submit()))
setActiveKey(key);
} else setActiveKey(key);
}
const tabs = [
{
label: 'Tab One',
key: 'tabOne',
children: <MyForm ref={formOneRef} />,
},
{
label: 'Tab Two',
key: 'tabTwo',
children: <MyForm ref={formTwoRef} />,
},
];
return <Tabs items={tabs} onChange={onChange} activeKey={activeKey} />;
};
The submit function
export default async function submitData(data: Data) {
console.log('Submitting...', data);
const res = await new Promise<boolean>((resolve) =>
setTimeout(
() => (Math.random() < 0.5 ? resolve(true) : resolve(false)),
1000
)
);
if (res) {
console.log('Success!', data);
return true;
}
if (!res) {
console.error('Fake Error', data);
return false;
}
}
Ant Design Tabs: https://ant.design/components/tabs/
Ant Design Form: https://ant.design/components/form/
I ended up making a state variable to store the submit function, and setting it in the child with useEffect.
A few caveats:
Had to set destroyInactiveTabPane to ensure forms were unmounted and remounted when navigating, so useEffect was called.
Had to wrap the form's submit function in useCallback as it was now a dependency of useEffect.
Had to make sure when calling setSubmitForm I had passed a function that returns the function, else the dispatch just calls submit immediately.
Stackblitz: https://stackblitz.com/edit/react-ts-n8y3kh?file=App.tsx
export type Data = { someField: string };
type Props = {
setSubmitForm: Dispatch<SetStateAction<() => Promise<boolean>>>;
};
const MyForm: FC<Props> = ({ setSubmitForm }) => {
const [form] = useForm<Data>();
const submit = useCallback(
async (data?: Data): Promise<boolean> => {
if (!data) data = form.getFieldsValue();
const res = await submitData(data);
return res;
},
[form]
);
useEffect(() => {
setSubmitForm(() => submit);
}, [setSubmitForm, submit]);
return (
<Form form={form} onFinish={submit}>
<Form.Item name="someField">
<Input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>
);
};
const App: FC = () => {
const [activeKey, setActiveKey] = useState('tabOne');
const [submitForm, setSubmitForm] = useState<() => Promise<boolean>>(
async () => true
);
async function onChange(key: string) {
if (confirm('Save Changes?')) {
if (await submitForm()) setActiveKey(key);
} else setActiveKey(key);
}
const tabs = [
{
label: 'Tab One',
key: 'tabOne',
children: <MyForm setSubmitForm={setSubmitForm} />,
},
{
label: 'Tab Two',
key: 'tabTwo',
children: <MyForm setSubmitForm={setSubmitForm} />,
},
];
return (
<Tabs
items={tabs}
onChange={onChange}
activeKey={activeKey}
destroyInactiveTabPane
/>
);
};

React TypeError is not a function with Onboarding implementation

I want to expand a demo provided by some tutorial about React Design Patterns, subject: Controlled Onboarding Flows, to implement multiple forms on several steps via Onboarding. But unfortunately the tutor did stop at the exciting part when it comes to having two-directional flows.
So I'm stuck and don't understand how to select the resp. function (marked with "// HOW TO DECIDE?!" in the 2nd code segment here).
So, every time I hit the prev. button, I receive the "Uncaught TypeError: goToPrevious is not a function" message, because both are defined.
Any suggestions on how to handle this?
This is what I got so far.
The idea behind this is to get the data from each form within the respo. Step Component and manage it witihin the parent component - which atm happens to be the App.js file.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my template for the resp. controlled form components I want to use:
export const ControlledGenericForm = ({ formData, onChange }) => {
return (
<form>
{Object.keys(formData).map((formElementKey) => (
<input
key={formElementKey}
value={formData[formElementKey]}
type="text"
id={formElementKey}
onInput={(event) => onChange(event.target.id, event.target.value)}
/>
))}
</form>
);
};
That's my controlled Onboarding component, I want to use:
import React from "react";
export const ControlledOnboardingFlow = ({
children,
currentIndex,
onPrevious,
onNext,
onFinish,
}) => {
const goToNext = (stepData) => {
onNext(stepData);
};
const goToPrevious = (stepData) => {
onPrevious(stepData);
};
const goToFinish = (stepData) => {
onFinish(stepData);
};
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
// HOW TO DECIDE?!
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
if (currentChild && onPrevious)
return React.cloneElement(currentChild, { goToPrevious });
return currentChild;
};
And that's the actual use of this two components within my App:
import { useState } from "react";
import { ControlledOnboardingFlow } from "./ControlledComponents/ControlledOnboardingFlow";
import { ControlledGenericForm } from "./ControlledComponents/ControlledGenericForm";
function App() {
const [onboardingData, setOnboardingData] = useState({
name: "Juh",
age: 22,
hair: "green",
street: "Main Street",
streetNo: 42,
city: "NYC",
});
const [currentIndex, setCurrentIndex] = useState(0);
const formDataPartOne = (({ name, age, hair }) => ({ name, age, hair }))(
onboardingData
);
const formDataPartTwo = (({ street, streetNo, city }) => ({
street,
streetNo,
city,
}))(onboardingData);
const onNext = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex + 1);
};
const onPrevious = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex - 1);
};
const onFinish = () => {
console.log("Finished");
console.log(onboardingData);
};
const handleFormUpdate = (id, value) => {
setOnboardingData({ ...onboardingData, [id]: value });
};
const StepOne = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 1</h1>
<ControlledGenericForm
formData={formDataPartOne}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)} >
Prev
</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepTwo = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 2</h1>
<ControlledGenericForm
formData={formDataPartTwo}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)}>Prev</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepThree = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 3</h1>
<h3>
Congrats {onboardingData.name} for being from, {onboardingData.city}
</h3>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
return (
<ControlledOnboardingFlow
currentIndex={currentIndex}
onPrevious={onPrevious}
onNext={onNext}
onFinish={onFinish}
>
<StepOne />
<StepTwo />
{onboardingData.city === "NYC" && <StepThree />}
</ControlledOnboardingFlow>
);
}
export default App;
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
Since onNext exists, this is the code that will run. It clones the element and gives it a goToNext prop, but it does not give it a goToPrevious prop. So when you press the previous button and run code like onClick={() => goToPrevious(onboardingData)}, the exception is thrown.
It looks like you want to pass both functions into the child, which can be done like:
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
if (currentChild) {
return React.cloneElement(currentChild, { goToNext, goToPrevious });
}
return currentChild;
If one or both of them happens to be undefined, then the child will get undefined, but that's what you would do anyway with the if/else.

OnGridReady enzyme test results in "TypeError: Cannot read property 'dispatchEvent' of null" for ag-grid React testing

I am facing an error TypeError: Cannot read property 'dispatchEvent' of null when trying to run an enzyme test to simulate onGridReady of an ag-grid table.
My questions and places where I am getting stuck...
How do I gracefully test the onGridReady function properly so that it doesn't error out and I can test that it has been called once ?
How do I test the other function onGridSizeChanged?
The Coverage report says I am missing the line setTableData to test - how do I test that line ?
This is how myfiles look
myTable.tsx
imports...
const myTable: React.FC = () => {
//intialize state variables
const onGridReady = ({ api }: { api: GridApi }) => {
setGridApi(api)
api.sizeColumnsToFit()
api.setHeaderHeight(10)
}
const onGridSizeChanged = () => {
gridApi?.sizeColumnsToFit()
}
useEffect(() => {
if (myTableData) {
if (selectedCountryName !== '') {
setTableData(objectCopy(myTableData))
}
}
}, [myTableData, selectedCountryName])
useEffect(() => {
// do some stuff
}, [businessDate])
return <div className={'my-table'}>
<Grid container spacing={12}>
<div>
<h1/>Title</h1>
</div>
<Grid item xs={12}>
<div data-testid="my-table-grid">
<AgGridReact
containerProps={{ style: { height: `${containerHeight}px` } }}
columnDefs={myTableColumns(moment(fromDate), moment(toDate))}
onGridReady={onGridReady}
onGridSizeChanged={onGridSizeChanged}
onModelUpdated={onGridSizeChanged}
rowData={tableData}
/>
</div>
</Grid>
</Grid>
</div >
}
export default myTable
myTable.test.tsx
import myTable from './myTable'
// other imports...
Enzyme.configure({ adapter: new Adapter() })
const onGridReady = jest.fn()
function setUpMyTableComponent() {
const wrapper = shallow(
<Provider store={store}>
<myTable >
<ColumnLayout>
<ColumnLayout>
<DataGrid onGridReady={onGridReady} />
</ColumnLayout>
</ColumnLayout>
</myTable >
</Provider>
)
return { wrapper }
}
describe('myTable', () => {
it('checking myTable', () => {
const { wrapper } = setUpMyTableComponent()
expect(wrapper).toBeDefined
expect(wrapper.find('.my-table')).toBeDefined
})
it('simulate onGridReady', async () => {
const { wrapper } = setUpMyTableComponent()
const aggrid = wrapper.find(AgGridReact)
dataGrid.simulate('onGridReady')
await tick()
expect(onGridReady).toHaveBeenCalled()
})
function tick() {
return new Promise(resolve => {
setTimeout(resolve, 0);
})
}
})

Dialog plays animation when state changes

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}
...
};

Resources