Redux-Form: Refactor Form Into Standalone Component

GitHub Repos

💾 Ncoughlin: React-Streams-Client

💾 Ncoughlin: React-Streams-API

Intro

We will continue here working on our Twitch Clone Project called ”Glitch“.

current glitch progress

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.

actions/index.js
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.

components/StreamForm.js
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.

components/StreamCreate.js
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.

actions/index.js
// 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.

components/StreamEdit.js
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.

🔗 Redux-Form: Initialize From State

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.