Redux-Form: Refactor Form Into Standalone Component
GitHub Repos
Intro
We will continue here working on our Twitch Clone Project called "Glitch".
It's time to make our edit button actually do something. We could once again embed a redux-form into our StreamEdit component like we did on StreamCreate. However because the form is identical except the actions that are taken upon submission, we want to refactor this form into it's own component that takes an onSubmit()
prop, which defines the actions that we want to execute on submission.
Our Current Redux Actions
Just for reference, here are our current Redux Actions.
import streams from "../apis/streams";
import {
SIGN_IN,
SIGN_OUT,
FETCH_STREAMS,
FETCH_STREAM,
CREATE_STREAM,
DELETE_STREAM,
EDIT_STREAM,
} from "./types";
// USER AUTHENTICATION ACTIONS
// change user status to signed in
export const signIn = (userId) => {
return {
type: SIGN_IN,
payload: userId,
};
};
// change user status to signed out
export const signOut = () => {
return {
type: SIGN_OUT,
};
};
// STREAMS CRUD REST ACTIONS
// fetch streams
export const fetchStreams = () => async (dispatch) => {
const response = await streams.get("/streams");
dispatch({ type: FETCH_STREAMS, payload: response.data });
};
// fetch stream
export const fetchStream = (streamId) => async (dispatch) => {
const response = await streams.get(`/streams/${streamId}`);
dispatch({ type: FETCH_STREAM, payload: response.data });
};
// create stream
export const createStream = (formValues) => async (dispatch, getState) => {
// retrieve user ID from store
const { userId } = getState().auth;
const response = await streams.post("/streams", {...formValues, userId});
dispatch({ type: CREATE_STREAM, payload: response.data });
};
// update stream
export const editStream = (streamId, formValues) => async (dispatch) => {
const response = await streams.patch(`/streams/${streamId}`, formValues);
dispatch({ type: EDIT_STREAM, payload: response.data })
};
// delete stream
export const deleteStream = (streamId) => async (dispatch) => {
await streams.delete(`/streams/${streamId}`);
dispatch({ type: DELETE_STREAM, payload: streamId })
};
and when we look at this list we can see that we are going to be making use of createStream and editStream.
StreamForm Component
Now we need to take our form, put it into it's own component.
import React, { Component } from "react";
import { Field, reduxForm } from "redux-form";
import { Redirect } from "react-router-dom";
class StreamForm extends Component {
renderError({ error, touched }) {
if (touched && error) {
return <div className="ui pointing label">{error}</div>;
}
}
// must be arrow function so that context of this is bound for this.renderError
renderInput = ({ input, label, meta }) => {
return (
<div className="field">
<label>{label}</label>
<input {...input} />
{this.renderError(meta)}
</div>
);
};
// submission action is prop from parent component
onSubmit = (formValues) => {
this.props.onSubmit(formValues);
};
render() {
// check for successful form submission
// redirect to index if true
if (this.props.submitSucceeded === true) {
return <Redirect to="/" />;
}
return (
<form
className="ui form"
onSubmit={this.props.handleSubmit(this.onSubmit)}
>
<Field
name="title"
component={this.renderInput}
label="Enter Title: "
/>
<Field
name="description"
component={this.renderInput}
label="Enter Description: "
/>
<button className="ui button primary">Submit</button>
</form>
);
}
}
const validate = (formValues) => {
const errors = {};
if (!formValues.title) {
errors.title = "You must enter a title";
}
if (!formValues.description) {
errors.description = "You must enter a description";
}
return errors;
};
// form wrapper
export default reduxForm({
form: "streamForm",
validate: validate,
})(StreamForm);
Take note of the onSubmit callback function. This is where we specify what actions we want to take when the form is submitted. We will choose those actions within each component where we insert the form.
Form Component in StreamCreate
Now it is easy to vastly simplify our StreamCreate component. We literally just add in our form component and pass in our onSubmit callback function as a prop. This callback function is where we decide what we want the form to do when it has been submitted. Which is always going to be an action. Remember our list of actions up top?
This is exactly where we use the createStream action.
import React, { Component } from "react";
import { connect } from "react-redux";
import StreamForm from "./StreamForm.js"
import { createStream } from "../../actions";
class StreamCreate extends Component {
// send post request to api server on submit
onSubmit = (formValues) => {
this.props.createStream(formValues);
};
render() {
return (
<>
<h3>Create Stream:</h3>
<StreamForm onSubmit={this.onSubmit} />
</>
);
}
}
export default connect(null, { createStream })(StreamCreate)
Form Component in StreamEdit
The StreamEdit component is very similar but slightly different. If we quickly review the editStream action we can see that this action takes in two arguments. We are going to need to pass in the stream ID in addition to the form values.
// update stream
export const editStream = (streamId, formValues) => async (dispatch) => {
const response = await streams.patch(`/streams/${streamId}`, formValues);
dispatch({ type: EDIT_STREAM, payload: response.data })
};
So knowing this, in our onSubmit callback function, we are calling our editStream action, and passing in the two pieces of data that it needs, the ID, which we are pulling from the URL variable, and the formValues.
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchStream, editStream } from "../../actions";
import StreamForm from "./StreamForm.js"
class StreamEdit extends Component {
componentDidMount() {
this.props.fetchStream(this.props.match.params.id);
}
// the actions that we want to use when form is submitted
onSubmit = (formValues) => {
this.props.editStream(this.props.match.params.id, formValues);
}
render() {
// if we have not retrieved stream yet use loading animation
if (!this.props.stream) {
return <div>Loading...</div>;
}
return (
<>
<h3>Edit Stream:</h3>
<StreamForm
onSubmit={this.onSubmit}
initialValues={{
title: this.props.stream.title,
description: this.props.stream.description
}}
/>
</>
);
}
}
const mapStateToProps = (state, ownProps) => {
// the stream that matches the ID in the URL
return { stream: state.streams[ownProps.match.params.id] };
};
export default connect(mapStateToProps, { fetchStream, editStream })(StreamEdit);
In addition note that we have submitted two props into the StreamForm component this time as well. The first is our onSubmit callback, just like before, and the second is a pre-defined piece of state for Redux-Form called initialValues. This is the built-in way that Redux-Form provides us to pre-populate our form fields with the current stream data. There is no need to manually set a "value" for the inputs ourselves, Redux-Form handles this for us.
We do want to be careful though, and not include all of our stream data in this initialValues. There are two pieces of data that will not be visible in the form fields, but are still part of the stream
object. The user ID and the stream ID.
Those two things should be immutable, we never want to overwrite those things in the database, even if we are passing in identical values to the ones that were there before. Therefore, instead of inserting the whole stream
object into this initial values prop, we insert a new object with just the values that we are willing to over-write, which would be the title and description so far. That way when we submit the form, we don't accidentally also submit the stream id and user id inside of formValues
, thereby over-writing those values in the database.
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.