Tutorial on how to handle forms with React, presented at React.Sofia
Forms with ReactRadoslav Stankov 07/03/2017
View Slide
Radoslav Stankov@rstankovhttp://rstankov.comhttp://github.com/rstankov
! https://github.com/tannerlinsley/react-form" https://github.com/prometheusresearch/react-forms# https://github.com/codecks-io/react-reform$ https://github.com/erikras/redux-form% https://github.com/Semantic-Org/Semantic-UI-ReactLibraries
export default class SubmissionForm extends React.Component {render() {return (SpeakerName: Email: );}}
export default class SubmissionForm extends React.Component {render() {return (SpeakerName: Email: );}handleSubmit = (e) => {e.preventDefault();const allValues = Array.from(e.target.elements).reduce((data, input) => {console.log(input);data[input.name] = input.value;return data;}, {});remoteCall({speakerName: allValues.speakerName,speakerEmail: allValues.speakerEmail,});};}
export default class SubmissionForm extends React.Component {render() {return (SpeakerName: this.speakerName = ref} type="text" id="speakerName" name="speakerName" defaultVEmail: this.speakerEmail = ref} type="email" id="speakerEmail" name="speakerEmail" defa);}handleSubmit = (e) => {e.preventDefault();remoteCall({speakerName: this.speakerName.value,speakerEmail: this.speakerEmail.value,});};}
export default class SubmissionForm extends React.Component {render() {return (SpeakerName: this.speakerName = ref} type="text" id="speakerName" name="speakerName" defaultVEmail: this.speakerEmail = ref} type="email" id="speakerEmail" name="speakerEmail" defa);}handleSubmit = (e) => {e.preventDefault();remoteCall({speakerName: this.speakerName.value,speakerEmail: this.speakerEmail.value,});};}&
Uncontrolled Components• fast• simple• integration with external libraries• no much control• gets messy quite easy
Controlled Components• you have more control• easier to reason about• needed for complex form interactions• recommend by react documentation
class ExampleForm extends React.Component {state = { value: '' }handleChange = (event) => {this.setState({value: event.target.value});}handleSubmit = (event) => {e.preventDefault();remoteCall(this.target.value)/};render() {return (value={this.state.value}onChange={this.handleChange} />);}}
class ExampleForm extends React.Component {state = { value: '' }handleChange = (event) => {this.setState({value: event.target.value});}handleSubmit = (event) => {e.preventDefault();remoteCall(this.target.value)/};render() {return (value={this.state.value}onChange={this.handleChange} />);}}Input change
class ExampleForm extends React.Component {state = { value: '' }handleChange = (event) => {this.setState({value: event.target.value});}handleSubmit = (event) => {e.preventDefault();remoteCall(this.target.value)/};render() {return (value={this.state.value}onChange={this.handleChange} />);}}Input change handleChangesetState
class ExampleForm extends React.Component {state = { value: '' }handleChange = (event) => {this.setState({value: event.target.value});}handleSubmit = (event) => {e.preventDefault();remoteCall(this.target.value)/};render() {return (value={this.state.value}onChange={this.handleChange} />);}}Input change handleChangesetState renderNew value
export default class SubmissionForm extends React.Component {state = {fields: {speakerName: '',speakerEmail: '',},};render() {return (SpeakerName: Email: );}handleChange = (e) => {const name = e.target.name;const value = e.target.value;this.setState({fields: { ...this.state.fields,[name]: value}});};handleSubmit = (e) => {e.preventDefault();remoteCall(this.state.fields);};}
export default class SubmissionForm extends React.Component {state = {fields: {speakerName: '',speakerEmail: '',},isSumitting: false,};render() {return (SpeakerName: Email: );}handleChange = (e) => { /* … */ };handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) { return; }this.setState({ isSubmitting: true });await remoteCall(this.state.fields);};}
export default class SubmissionForm extends React.Component {state = {fields: {speakerName: '',speakerEmail: '',talkTitle: '',},isSumitting: false,};render() {return (SpeakerName: Email: Title: );}handleChange = (e) => { /* … */ };handleSubmit = (e) => { /* … */ };}
let Field = ({ name, type, label, ...props }) => {return ({label}: );};
export default class SubmissionForm extends React.Component {state = { /* … */ };render() {return (SpeakerTalk);}handleChange = (e) => { /* … */ };handleSubmit = (e) => { /* … */ };}
'
(
let Field = ({ name, input, label, ...props }) => {if (input === 'textarea') {return ({label}: );}return ({label}: );};
export default class SubmissionForm extends React.Component {state = {fields: {speakerName: '',speakerEmail: '',talkTitle: '',talkDescription: '',},isSumitting: false,};render() {return (SpeakerTalk);}handleChange = (e) => { /* … */ };handleSubmit = (e) => { /* … */ };}
const LENGTH_OPTIONS = [{value: 15, label: '15 minutes'},{value: 30, label: '30 minutes'},{value: 45, label: '45 minutes'},];
name="talkLength"label="Length"value={this.state.fields.talkLength}onChange={this.handleChange}input="select"options={LENGTH_OPTIONS} />
let Select = ({ options, ...props }) => ({options.map(({ label, value }, i) => ({label || value}))});
let Field = ({ name, input, label, ...props }) => {if (input === 'textarea') {return ({label}: );}if (input === 'select') {return ({label}: );}return ({label}: );};
let Input = ({ ...props }) => ();
const INPUTS = {'textarea': Textarea,'select': Select,'text': Input,};let Field = ({ name, input = 'text', label, ...props }) => {const Component = INPUTS[input] || Input;const inputProps = Component === Input ? { type: input, ...props } : props;return ({label}: );};
const INPUTS = {'textarea': Textarea,'select': Select,'text': Input,};let Field = ({ name, input = 'text', label, ...props }) => {const Component = typeof input === 'function' ? input : INPUTS[input] || Input;const inputProps = Component === Input ? { type: input, ...props } : props;return ({label}: );};
let RadioGroup = ({ value: fValue, options, name, id, ...props }) => ({options.map(({ label, value }, i) => ({label || value}))});
const INPUTS = {'textarea': Textarea,'select': Select,'text': Input,'radioGroup': RadioGroup,};
SpeakerTalk
Component
ComponentChild Component
ComponentChild ComponentChild Child Component
ComponentChild ComponentChild Child Component Child Child … Child Component
ComponentChild ComponentChild Child Component Child Child … Child ComponentContext
ComponentChild ComponentChild Child Component Child Child … Child ComponentContextContext
export default class SubmissionForm extends React.Component {state = {fields: {speakerName: '',speakerEmail: '',talkTitle: '',talkDescription: '',talkLength: '15',},isSumitting: false,};static childContextTypes = {formState: React.PropTypes.object.isRequired,formHandleChange: React.PropTypes.func.isRequired,};getChildContext() {return {formState: this.state,formHandleChange: this.handleChange,};}render() { /* … */ };handleChange = (e) => { /* … */ };handleSubmit = (e) => { /* … */ };}
let Field = ({ name, input, label, ...props }, { formState: { fields }, formHandleChange }) => {const value = fields[name];const Component = typeof input === 'function' ? input : INPUTS[input] || Input;const inputProps = Component === Input ? { type: input, ...props } : props;return ({label}: );};Field.contextTypes = {formState: React.PropTypes.object.isRequired,formHandleChange: React.PropTypes.func.isRequired,};
let SubmitButton = ({ children }, { formState: { isSubmitting }}) => ();SubmitButton.contextTypes = {formState: React.PropTypes.object.isRequired,};
SpeakerTalkSubmit
export default class SubmissionForm extends React.Component {state = {fields: {speakerName: '',speakerEmail: '',talkTitle: '',talkDescription: '',talkLength: '15',},isSumitting: false,};static childContextTypes = {formState: React.PropTypes.object.isRequired,formHandleChange: React.PropTypes.func.isRequired,};getChildContext() {return {formState: this.state,formHandleChange: this.handleChange,};}render() {return (SpeakerTalkSubmit
SpeakerTalkSubmit);}handleChange = (e) => {const name = e.target.name;const value = e.target.value;const fields = {...this.state.fields,[name]: value};this.setState({ fields });};handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) {return;}this.setState({ isSubmitting: true });await remoteCall(this.state.fields);};}
export default class SubmissionForm extends React.Component {state = {fields: {speakerName: '',speakerEmail: '',talkTitle: '',talkDescription: '',talkLength: '15',},isSumitting: false,};static childContextTypes = {formState: React.PropTypes.object.isRequired,formHandleChange: React.PropTypes.func.isRequired,};getChildContext() {return {formState: this.state,formHandleChange: this.handleChange,};}render() {return (SpeakerTalkSubmit);}handleChange = (e) => {const name = e.target.name;const value = e.target.value;const fields = {...this.state.fields,[name]: value};this.setState({ fields });};handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) {return;}this.setState({ isSubmitting: true });await remoteCall(this.state.fields);};}
class Form extends React.Component {state = {fields: this.props.fields,isSumitting: false,};static childContextTypes = {formState: React.PropTypes.object.isRequired,formHandleChange: React.PropTypes.func.isRequired,};getChildContext() {return {formState: this.state,formHandleChange: this.handleChange,};}render() {return ({this.props.children});}handleInputChange = (e) => {const name = e.target.name;const value = e.target.value;const fields = {...this.state.fields,[name]: value};this.setState({ fields });};
return {formState: this.state,formHandleChange: this.handleChange,};}render() {return ({this.props.children});}handleInputChange = (e) => {const name = e.target.name;const value = e.target.value;const fields = {...this.state.fields,[name]: value};this.setState({ fields });};handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) {return;}this.setState({ isSubmitting: true });await this.props.onSubmit(this.state.fields);};}
const FIELDS = {speakerName: '',speakerEmail: '',talkTitle: '',talkDescription: '',talkLength: '15',};export default class SubmissionForm extends React.Component {render() {return (SpeakerTalkSubmit);}}
const FIELDS = {speakerName: '',speakerEmail: '',talkTitle: '',talkDescription: '',talkLength: '15',};let SubmissionForm = () => (SpeakerTalkSubmit);
Submit
Submit Server
Submit ServerSuccess
Submit ServerSuccessErrors
remoteCall Server{ result: 'ok' }{ errors: {…} }
remoteCall Server{ result: 'ok' }{ errors: {field1: [ 'error1', 'error2field2: [ 'error1'] } }
{ errors: {field1: [ 'error1', 'error2' ], field2: [ 'error1'] } }
class Form extends React.Component {state = {fields: this.props.fields,isSumitting: false,};static childContextTypes = { /* … */ };getChildContext() = { /* … */ }render() = { /* … */ }handleInputChange = (e) => { /* … */ };handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) {return;}await this.props.onSubmit(this.state.fields);};}
class Form extends React.Component {state = {fields: this.props.fields,errors: {},isSumitting: false,};static childContextTypes = { /* … */ };getChildContext() = { /* … */ }render() = { /* … */ }handleInputChange = (e) => { /* … */ };handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) {return;}this.setState({ isSubmitting: true, errors: {} });const { errors } = await this.props.onSubmit(this.state.fields);this.setState({ isSubmitting: false, errors: errors || {} });};}
let Field = ({ name, input, label, ...props }, { formState: { fields, errors }, formHandleInputChange })const value = fields[name];const error = errors[name] && errors[name][0];const Component = typeof input === 'function' ? input : INPUTS[input] || Input;const inputProps = Component === Input ? { type: input, ...props } : props;return ({label}: { error && {error}});};
class Form extends React.Component {state = { /* … */ };static childContextTypes = { /* … */ };getChildContext() = { /* … */ }render() = { /* … */ }handleInputChange = (e) => { /* … */ };handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) {return;}this.setState({ isSubmitting: true, errors: {} });const { errors } = await this.props.onSubmit(this.state.fields);this.setState({ isSubmitting: false, errors: errors || {} });};}
class Form extends React.Component {state = { /* … */ };static childContextTypes = { /* … */ };getChildContext() = { /* … */ }isUnmounted: boolean = false;componentWillUnmount() {this.isUnmounted = true;}render() = { /* … */ }handleInputChange = (e) => { /* … */ };handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) {return;}this.setState({ isSubmitting: true, errors: {} });const { errors } = await this.props.onSubmit(this.state.fields);if (!this.isUnmounted) {this.setState({ isSubmitting: false, errors: errors || {} });}};}
class Form extends React.Component {state = { /* … */ };static childContextTypes = { /* … */ };getChildContext() { /* … */ }isUnmounted: boolean = false;componentWillUnmount() {this.isUnmounted = true;}render() { /* … */ }handleChange = (e) => { /* … */ };handleSubmit = async (e) => {e.preventDefault();if (this.state.isSubmitting) {return;}this.setState({ isSubmitting: true, errors: {} });const { errors } = await this.props.onSubmit(this.state.fields);if (!this.isUnmounted) {this.setState({ isSubmitting: false, errors: errors || {} });}};}
• simple form interface• extensible fields• protection for double submit• standardized form layout• standardized server responses
What is missing?• client side validations• theming• handling of deep nested forms• tests!• fun features ¯\_(ϑ)_/¯
https://github.com/RStankov/talks-code
https://speakerdeck.com/rstankov/forms-with-react
https://www.meetup.com/React-Sofia/
Thanks )