React-Redux: Using Actions To Interact With Database

Intro

Continuing our Twitch Clone project, we are now at the point where we need to put together a second server, which will serve as our API Server. Eventually we will want to hook this application up to a real database, but for the time being we can start with a temporary JSON Server to get started.

JSON Server

We can start by creating a new folder and repository and installing the server per the instructions provided.

This JSON Server will allow us to store records, which in this case is going to be the name and description of live video streams. We will deal with the actual video part later. First we just need to have a way to store and retrieve records.

Using JSON Server we can create our first database, which in this mock database we accomplish by creating a JSON file in the index per the instructions. Because the first thing we are going to track is a list of streams we add that as an empty array.

db.json
{
    "streams": []
}

Take note of the name streams because we are going to referencing that later in our axios request.

To interact with this API server we will need to install a couple of new packages. Firstly we will be making API requests, so as we have always done so far, we will use Axios. Secondly we are going to be making Async functions (API requests) inside of our Actions, which is going to require redux-thunk. Install both of those packages.

Configure Redux-Thunk

Let’s get our Thunk setup out of the way. Head over to our main index.js file where we have already set ourselves up to apply middleware. We can import redux-thunk and then pass it in.

index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import reduxThunk from 'redux-thunk'
import App from "./components/App";
import reducers from "./reducers";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  reducers,
  composeEnhancers(applyMiddleware(reduxThunk)));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector("#root")
);

Configure Axios

Let us make a new directory in src called apis and then an axios configuration file inside of that directory called streams.js

Inside of that we can simply let axios know that our streams api is available at localhost:3001 which is the default location that we set when we installed JSON Server.

apis/streams.js
import axios from 'axios';

export default axios.create({
    baseURL: 'http://localhost:3001'
})

Then we can switch over to our actions index, where we are going to be writing the action for contacting the server and creating a new record, and import this axios configuration file.

actions/index.js
import streams from '../apis/streams'import { SIGN_IN, SIGN_OUT } from './types'

export const signIn = (userId) => {
  return {
    type: SIGN_IN,
    payload: userId
  };
};

export const signOut = () => {
  return {
    type: SIGN_OUT,
  };
};

Creating Async Action

We are now ready to create our Async Action, which will make use of redux-thunk. You may want to brush up on redux-thunk with Ncoughlin - React-Redux: Asynchronous Actions (Redux-Thunk).

actions/index.js
import streams from '../apis/streams'
import { SIGN_IN, SIGN_OUT } from './types'

// 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,
  };
};

// create stream
export const createStream = formValues => (dispatch) => {  streams.post('./streams', formValues);};

So we will be sending a post request to the streams api, which we have designated in the axios configuration file. The location there we will be sending the request to is /streams and we will be sending the data formValues.

Connecting Action to Component

At this point we need to connect our actions to our component. Normally we would have already done this, but the only actions we had thus far were redux-form actions.

So to start we import connect and also our create stream action, as we will be submitting that into the connect function.

We can then modify our onSubmit function to call the createStream action and pass in the form values.

components/streams/CreateStream.js
import React, { Component } from "react";
import { Field, reduxForm } from "redux-form";
import { connect } from "react-redux";
import { createStream } from "../../actions";
class StreamCreate 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 }) => {
    console.log(meta);
    return (
      <div className="field">
        <label>{label}</label>
        <input {...input} />
        {this.renderError(meta)}
      </div>
    );
  };

  // send post request to api server on submit
  onSubmit(formValues) {
    this.props.createStream(formValues);  }

  render() {
    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
const formWrapped = reduxForm({  form: "Create Stream",  validate: validate,})(StreamCreate);
// redux connect
export default connect(null, { createStream })(formWrapped);

And down at the bottom here we have connected our actions to redux with the connect function. We also had to fix a weird collision here where the reduxForm connector was in our way, so we solved that with a slick little bit of syntaxery where we just wrapped that and passed in the whole previous export as the connect component.

Of course we have the worlds most common React error which is that this is undefined. We fix that by turning the onSubmit function into an arrow function to bind the context of this.

onSubmit = (formValues) => {
    this.props.createStream(formValues);
  };

And now we can test our form.

form inputs JSON data on API server

And we can see that we have successfully sent data to the API server! This very closely mimics the process that we would use to send the data to any other database (like a MongoDB Atlas server). The only real difference is the axios config file.

Handling Response

Our API server automatically sends us a response, and we want to get a handle on that response. First let us just observe this response inside our network traffic.

network response with id

And we can see that our database has assigned an id of 6 to this stream.

Now to get a handle on this response let us modify our create stream action.

actions/index.js
import streams from '../apis/streams'
import { SIGN_IN, SIGN_OUT } from './types'

// 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,
  };
};


// create stream
export const createStream = formValues => async (dispatch) => {
  const response = await streams.post('./streams', formValues);};

Now that we have a handle on the response, we will want to dispatch an action with the payload of that stream. We have start using fixed action types, so we need to add our new action type to the list.

actions/types.js
export const SIGN_IN = 'SIGN_IN';
export const SIGN_OUT = 'SIGN_OUT';
export const CREATE_STREAM = 'CREATE_STREAM';

And now we can send that response to redux dispatch.

actions/index.js
import streams from '../apis/streams'
import { CREATE_STREAM, SIGN_IN, SIGN_OUT } from './types'

// 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,
  };
};


// create stream
export const createStream = formValues => async (dispatch) => {
  const response = await streams.post('./streams', formValues);

  dispatch({ type: CREATE_STREAM, payload:response.data });};

Our createStream action now takes the form values and using an async function sends those form values to our API server where they are saved, and then takes the response and sends it to dispatch.

The next step would be to create a reducer for this action so that the payload data gets handled in some way.

Before we create our reducers however, let us go ahead and create the rest of our CRUD actions using the REST convention. If we follow this convention we can assume that our dummy API server will handle the requests the way that we would expect, since it was created using this same convention.

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) => {
  const response = await streams.post("/streams", formValues);

  dispatch({ type: CREATE_STREAM, payload: response.data });
};

// update stream
export const editStream = (streamId, formValues) => async (dispatch) => {  const response = await streams.put(`/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 })};

A couple quick things to note is that our edit stream action required two arguments (id and form data), and our delete action did not require sending the response data as the payload, because there is no data in that case.

GitHub Repo

Ncoughlin: React-Streams-Client

Ncoughlin: React-Streams-API