Will Little Subscribe

Sending form data with Redux and React :: Full-stack web development "Hello World" tutorials for entrepreneurs: Part 6 of 10


This post is part 6 of a 10-part series within a series that is designed to teach full-stack web development for entrepreneurs. Here we’ll dive even further into Redux and React, which we introduced in part 5.

We’ll be working here on replicating the form that we did for StimulusReflex, in Redux/React so we can see by comparison what it looks like to submit and create a quote.

Let’s start by changing the first line of our quotes_view.js file to:

import React, { useEffect, useState } from 'react'

What we’re setting up here is the use of another React Hook called useState() that is intended to manage state within a presentational component. There are times (such as creating a simple form for educational purposes, or switching the view of a tab or something) when we don’t need to manage state with Redux.  

In our propTypes let’s go ahead and add in a couple new parameters at the end:

  createQuote: PropTypes.func.isRequired,
  creatingQuote: PropTypes.bool,

And in our defaultProps we’ll add:

  creatingQuote: false, 

And we’ll make sure to include these two new parameters in the QuotesView component declaration right under fetchQuotes:

  createQuote,
  creatingQuote, 

And just before the return() let’s add:

  const [content, setContent] = useState("");
  const [authorName, setAuthorName] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    createQuote(content, authorName)
    e.target.reset();
  }

This is how we’ll store and update the value of our quote content and author name. We also set up a function (handleSubmit) to handle the submission of our form.

FYI: “e.preventDefault();” prevents the form from submitting as a standard HTML form (which would reload our page).

Now, under just under our “<h2>React Quotes App</h2>” let’s add the form:

      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="content">Quote:</label>
          <br />
          <textarea
            name="content"
            id="RQA_quote_content"
            onChange={e => setContent(e.target.value)}
            title="Quote"
            required
          ></textarea>
        </div>  
        <div>
          <label htmlFor="author_name">Author:</label>
          <br />
          <input
            type="text"
            name="author_name"
            id="RQA_quote_author_name"
            onChange={e => setAuthorName(e.target.value)}
            title="Author"
            required
          />
        </div>  
        <br /> 
        {!creatingQuote ? 
          <button type="submit">Create Quote</button>
          :
          <button type="submit" disabled>Creating Quote...</button>
        }
                     
      </form>

Instead of using a special Rails helper to generate our form, we’ll instead use manual HTML within our React component and set a function (which we defined above) to handle our form submission.

Importantly, for our two fields we need to update our React component state parameters every time the values change. This means React is changing the values with every keystroke, and when you press the submit button it will take those current values and send them off to our API server to create our quote.

Now, in our app>controllers>api>v1>quotes_controller.rb file let’s add just under the first line:

include CableReady::Broadcaster 

...which will set up cable_ready. And under the index method let’s add a create method:

  def create
    quote = Quote.create(content: params[:content], author_name: params[:author])
    cable_ready['hello_quotes'].insert_adjacent_html(
      selector: '#quotes_list',
      position: 'afterBegin',
      html: render_to_string(partial: 'welcome/quote.html.erb', locals: {quote: quote})
    )    
    cable_ready.broadcast    
    respond_to do |format|
      format.json { render json: quote }
    end    
  end

All we’re doing here is creating our quote in our database and then broadcasting through cable_ready like we did before.  Finally, we pass back to the server just the single new quote in JSON form that was created.

Since we just created a new controller action, open up your config>routes.rb file and add on a new line 8:

post '/quotes', to: 'quotes#create', as: 'create_quote'

That’s all we need on the Rails side for now.

We’ll next want Redux to properly submit the form and handle that returned JSON, so let’s open up our actions.js file in our app>javascript>react_quotes_app folder again and starting on a new line 7 we’ll define our action names:

  CREATE_QUOTE_REQUEST: 'RQA::CreateQuoteRequest',
  CREATE_QUOTE_SUCCESS: 'RQA::CreateQuoteSuccess',
  CREATE_QUOTE_FAILURE: 'RQA::CreateQuoteFailure',

And on line 29 before our final export we’ll insert our action function using our genericAPI function that we set up before:

export const createQuote = (content, author) =>
  genericAPI(
    () => api.createQuote(content,author),
    ACTIONS.CREATE_QUOTE_REQUEST,
    ACTIONS.CREATE_QUOTE_SUCCESS,
    ACTIONS.CREATE_QUOTE_FAILURE,
    'Failed to create quote',
  );

Now let’s be sure to open up our api.js file in that same folder and on a new line 18 add:

const normalizeQuote = response => normalize(response.data, schema.quote);

...and at the end of the file add:

export const createQuote = (content, author) =>
  axios.post('/api/v1/quotes', {content, author}).then(normalizeQuote);

This will send a POST request to our API server with our content and author name in order to create our quote, then it will normalize the quote JSON object for our reducer to handle.

So, open up your quotes_reducer.js file and enter on a new line 19:

  case actions.CREATE_QUOTE_SUCCESS: 
    return state.insert(0, action.response.result);

What this will do is make sure to listen for when we’ve successfully created a quote and then insert our new quote id at the beginning of our allIds List so that our new quote will show up at the beginning of our list on our page.

Note that because we had already set up the “byId” reducer in this file to listen for quote entities to be returned from our server, that quote will be automatically added to our store.

Now, on a new line 38 in that file let’s go ahead and create a new reducer function:

function creating(state = false, action) {
  switch (action.type) {
  case actions.CREATE_QUOTE_REQUEST:
    return true;
  case actions.CREATE_QUOTE_SUCCESS:
  case actions.CREATE_QUOTE_FAILURE:
    return false; 
  default:
    return state;
  }
}

This allows us to store the state of our form submission so we can disable the button and indicate to our user that we are submitting our form.

So make sure on a new line 66 to add the reducer:

  creating,

And on a new line 81 we’ll add our selector function:

export const getCreatingQuote = (state) =>
  state.get('creating') ;

Now open up our reducer.js file and enter this line at the end of the file:

export const getCreatingQuote = (state) => QuotesSelect.getCreatingQuote(state.get('quotes'));

This allows us to call our selector function from our rootReducer, which is best practice when importing selectors in our container files so we don’t need to think about what part of the state to send our selector (i.e. we just send the whole thing and let our selectors figure it out within our reducer files).

Next let’s open our quotes_container.js file and starting at line 3 let’s make it contain:

import { 
  getQuotesIds, 
  getQuotesLoaded, 
  getQuotesLoading, 
  getCreatingQuote,
} from './reducer'
import { fetchQuotes, createQuote } from './actions'

...and a new line 16 add:

    creatingQuote: getCreatingQuote(state),

...and on line 20 we’ll make sure it says:

const mapDispatchToProps = {
  fetchQuotes, 
  createQuote,
};

These entries set up our container file to import our new selector, pass our new parameter (creatingQuote) into our view component, and pass our new action function (createQuote) in as well.

And that’s it for now. You should have these 8 file changes teed up in VSC:

Go ahead and restart your Rails server and reload localhost:3000 in Chrome and you should see the new form and the ability to create quotes.

The new quotes submitted from Redux/React show up automatically in both the Redux/React list because of our state update, and they show up in our StimulusReflex list because of our CableReady broadcast.

Go ahead and inspect your code changes again to make sure everything looks right, which should look like this commit diff, then:

  • git commit -a -m "add a form in our Redux/React example to create a hello world quote"
  • git push

(Note that because we didn’t actually create any new files we didn’t have to do “git add .” at the beginning)

Next we’ll go ahead and replicate the behavior we created above in Stimulus to delete a quote by clicking on it in the Redux/React list.

Open up the quote_view.js file and update the propTypes object to include a new function we’ll pass it called deleteQuote():

const propTypes = {
  quote: PropTypes.instanceOf(Map),
  deleteQuote: PropTypes.func.isRequired,  
}

We’ll then add that parameter to be passed into QuoteView and add a function to handle the click of any one of our <li> elements:

const QuoteView = ({
  quote,
  deleteQuote,
}) => {

  const handleClick = () => {
    deleteQuote(quote.get('id'))
  }

  return (
    <li onClick={handleClick}>
      "{quote.get('content')}" 
      - {quote.get('author_name')}
    </li>
  )
}

Now let’s open the quote_container.rb file and add our import on a new line 4:

import { deleteQuote } from './actions'

And on line 12 we’ll update our mapDispatchToProps to add:

const mapDispatchToProps = {
  deleteQuote
};

Now let’s go define that action in our action.js file. Add these actions on a new line 12:

  DELETE_QUOTE_REQUEST: 'RQA::DeleteQuoteRequest',
  DELETE_QUOTE_SUCCESS: 'RQA::DeleteQuoteSuccess',
  DELETE_QUOTE_FAILURE: 'RQA::DeleteQuoteFailure', 

And on a new line 41 we’ll add our action function:

export const deleteQuote = (id) =>
  genericAPI(
    () => api.deleteQuote(id),
    ACTIONS.DELETE_QUOTE_REQUEST,
    ACTIONS.DELETE_QUOTE_SUCCESS,
    ACTIONS.DELETE_QUOTE_FAILURE,
    'Failed to delete quote',
  );  

And then we’ll update our api.js file at the bottom to include our axios call function:

export const deleteQuote = (id) =>
  axios.delete(`/api/v1/quotes/${id}`);

Note that we’re using a new type of request here called DELETE, which is intended to do just that with data on our API server. So let’s open up our app>controllers>api>v1>quotes_controller.rb file and add a new method starting a new line 20:

  def destroy
    Quote.find(params[:id]).destroy
    cable_ready['hello_quotes'].remove(
      selector: "#quote-#{params[:id]}"
    )    
    cable_ready.broadcast
    respond_to do |format|
      format.json { render json: params[:id] }
    end       
  end

Similar to what we did on the Rails side of things, we’ll destroy our quote and broadcast it to other clients, then we’ll simply pass back the id of the deleted quote so that we can update the state in Redux.

Be sure to open up your config>routes.rb file to create a route for the method we just created.  On a new line 9 add the following:

delete '/quotes/:id', to: 'quotes#destroy', as: 'destroy_quote'

Now let’s go ahead and update our quotes_reducer.js file back in our app>javascript>react_quotes_app folder and on a new line 12 add:

  if (action.response && action.response.data){
    return state.delete(action.response.data.toString())
  }

This will make sure our quote is deleted from our “byId” reducer, and let’s also enter starting on a new line 24:

  case actions.DELETE_QUOTE_SUCCESS:
    return state.remove(state.indexOf(action.response.data));

….which will ensure that the deleted id is removed from our “allIds” immutable List.

Now in your localhost:3000 in Chrome you should be able to create a new quote and delete it. If you watch your Redux dev tools you should see the following six actions fired (a request/success pair for loading your quotes, for creating one, and for deleting it):

Let’s go ahead and prep our commit, so your VSC file changes should look like this:

And then after your own review, which should look like this commit diff, go ahead and:

  • git commit -a -m "delete a hello world quote when clicking it in our Redux/React example"
  • git push

Good. Now let’s move on to Part 7 →

Comments

Interested in participating in the comments on this article? Sign up/in to get started:

By signing up/in with Satchel you are agreeing to our terms & conditions and privacy policy.