Implementing a loading message in React-Redux?

Implementing a loading message in React-Redux?
0

Hello, friends.

It’s been a while since I completed my boot camp with Thinkful, and my journey started with FCC before that. I was applying for jobs, but realized I was getting rusty with code, so I’ve gone back to my full-stack React app to implement some stretch goals of mine. One was to create a message or graphic when the page is loading. Typically, with React, this only happens when you’re retrieving data.

I can’t seem to figure out how to go about adding this feature, and I can’t find anything online using just React-Redux.

Let’s walk through my login component, action, and reducer. Maybe this will give a good idea for where I need to do this?

// login-form component

import React from 'react';
import { Field, reduxForm, focus } from 'redux-form';
import Input from './input';
import { login } from '../actions/auth';
import { required, nonEmpty } from '../validators';

import RegisterButton from './register-button';

import '../styles/login-form.css';

export class LoginForm extends React.Component {
  onSubmit(values) {
    return this.props.dispatch(login(values.username, values.password));
  }

  render() {
    let error;
    if (this.props.error) {
      error = (
        <div>
          <p className="form-error" aria-live="polite">
            {this.props.error}
          </p>
        </div>
      );
    }

    return (
      <form
        className="login-form"
        onSubmit={this.props.handleSubmit(values => this.onSubmit(values))}
      >
        {error}

        <label htmlFor="username">Username</label>
        <span className="demouser">Demo Username: demouser</span>
        <Field
          component={Input}
          type="text"
          name="username"
          id="username"
          validate={[required, nonEmpty]}
        />
        <label htmlFor="password">Password</label>
        <span className="demouser">Demo Password: password10</span>
        <Field
          component={Input}
          type="password"
          name="password"
          id="password"
          validate={[required, nonEmpty]}
        />
        <button className="main-btn" disabled={this.props.pristine || this.props.submitting}>
          Login
        </button>
        <RegisterButton />
      </form>
    );
  }
}

export default reduxForm({
  form: 'login',
  onSubmitFail: (errors, dispatch) => dispatch(focus('login', 'username'))
})(LoginForm);
// auth action

import jwtDecode from 'jwt-decode';
import { SubmissionError } from 'redux-form';

import { API_BASE_URL } from '../config';
import { normalizeResponseErrors } from './utils';
import { saveAuthToken, clearAuthToken } from '../local-storage';

export const SET_AUTH_TOKEN = 'SET_AUTH_TOKEN';
export const setAuthToken = authToken => ({
  type: SET_AUTH_TOKEN,
  authToken
});

export const CLEAR_AUTH = 'CLEAR_AUTH';
export const clearAuth = () => ({
  type: CLEAR_AUTH
});

export const AUTH_REQUEST = 'AUTH_REQUEST';
export const authRequest = () => ({
  type: AUTH_REQUEST
});

export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const authSuccess = currentUser => ({
  type: AUTH_SUCCESS,
  currentUser
});

export const AUTH_ERROR = 'AUTH_ERROR';
export const authError = error => ({
  type: AUTH_ERROR,
  error
});

// Stores the auth token in state and localStorage, and decodes and stores
// the user data stored in the token
const storeAuthInfo = (authToken, dispatch) => {
  const decodedToken = jwtDecode(authToken);
  dispatch(setAuthToken(authToken));
  dispatch(authSuccess(decodedToken.user));
  saveAuthToken(authToken);
};

export const login = (username, password) => dispatch => {
  dispatch(authRequest());
  return (
    fetch(`${API_BASE_URL}/auth/login`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        username,
        password
      })
    })
      // Reject requests not 200 status, creating
      // errors which follow a consistent format
      .then(res => normalizeResponseErrors(res))
      .then(res => res.json())
      .then(({ authToken }) => storeAuthInfo(authToken, dispatch))
      .catch(err => {
        console.log('err from auth.js action:', err);

        const { code } = err;
        const message =
          code === 401 ? 'Incorrect username or password' : 'Unable to login, please try again';
        dispatch(authError(err));
        // Could not authenticate, so return a SubmissionError for Redux Form
        return Promise.reject(
          new SubmissionError({
            _error: message
          })
        );
      })
  );
};

export const refreshAuthToken = () => (dispatch, getState) => {
  // set loading to true and clear earlier errors
  dispatch(authRequest());
  const authToken = getState().auth.authToken;
  return fetch(`${API_BASE_URL}/auth/refresh`, {
    method: 'POST',
    headers: {
      // Provide our existing token as credentials to get a new one
      Authorization: `Bearer ${authToken}`
    }
  })
    .then(res => normalizeResponseErrors(res))
    .then(res => res.json())
    .then(({ authToken }) => storeAuthInfo(authToken, dispatch))
    .catch(err => {
      // We couldn't get a refresh token because our current credentials
      // are invalid or expired, or something else went wrong, so clear
      // them and sign us out
      dispatch(authError(err));
      dispatch(clearAuth());
      clearAuthToken(authToken);
    });
};
// auth reducer

import {
  SET_AUTH_TOKEN,
  CLEAR_AUTH,
  AUTH_REQUEST,
  AUTH_SUCCESS,
  AUTH_ERROR
} from '../actions/auth';

const initialState = {
  isAuthenticated: false,
  authToken: null, // authToken !== null does not mean it has been validated
  currentUser: null,
  loading: false,
  error: null
};

export default function reducer(state = initialState, action) {
  if (action.type === SET_AUTH_TOKEN) {
    return Object.assign({}, state, {
      authToken: action.authToken
    });
  } else if (action.type === CLEAR_AUTH) {
    return Object.assign({}, state, {
      isAuthenticated: false,
      authToken: null,
      currentUser: null
    });
  } else if (action.type === AUTH_REQUEST) {
    return Object.assign({}, state, {
      loading: true,
      error: null
    });
  } else if (action.type === AUTH_SUCCESS) {
    return Object.assign({}, state, {
      isAuthenticated: true,
      loading: false,
      currentUser: action.currentUser // set state.auth.currentUser to user payload delivered from storeAuthInfo action
    });
  } else if (action.type === AUTH_ERROR) {
    return Object.assign({}, state, {
      loading: false,
      error: action.error
    });
  }
  return state;
}
// users action
import { SubmissionError } from 'redux-form';

import { API_BASE_URL } from '../config';
import { normalizeResponseErrors } from './utils';

export const registerUser = user => dispatch => {
  return fetch(`${API_BASE_URL}/users`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json'
    },
    body: JSON.stringify(user)
  })
    .then(res => normalizeResponseErrors(res))
    .then(res => res.json())
    .catch(err => {
      const { reason, message, location } = err;
      if (reason === 'ValidationError') {
        // Convert ValidationErrors into SubmissionErrors for Redux Form
        return Promise.reject(
          new SubmissionError({
            [location]: message
          })
        );
      }
    });
};

Thanks in advance!

Hey SlouchingToast,

Code looks good. In order to render anything conditionally you need to have a state that changes based on that condition.

So in your case “Loading : true/ false”

When you push on submit, you to dispatch an action to set loading to true,
and when the fetch is finished you need to dispatch and action to set loading false.

You can then use a conditional in the view based on loading.

There are a few quick examples in this article : here
it’s a good read as well about promises, but don’t worry about that too much .


EDIT:

const initialState = {
  isAuthenticated: false,
  authToken: null, // authToken !== null does not mean it has been validated
  currentUser: null,
  loading: false,  < _------------------------------------ here
  error: null
};

You already have a loading state .

You just need to handle it in your view code

Access state.loading in the view and conditionally render your loading component when it is set to true.

Thanks @J1marotta

I’m having trouble with the rendering for some reason. Don’t I need to write the function inside my parent component and pass it down to my child component to render it? I’m getting console logs appropriately, but just can’t seem to render the message.

Here’s what I’ve done:

// login-page - parent to login-form

export function LoginPage(props) {
  const loadMsg = () => {
    if (props.loading) {
      return (
        <div className="load-msg">
          <h2>Loading...</h2>
        </div>
      );
    }
  };
// ...
  return (
    <section id="form-section">
      <h2 className="page-title">Login</h2>
      <LoginForm loadMsg={loadMsg} />
    </section>
  );
// ...
// login-form

// ...
return (
      <form
        className="login-form"
        onSubmit={this.props.handleSubmit(values => this.onSubmit(values))}
      >
        {error}
        {this.props.loadMsg()}
// ...

Notice that I wrote the error message inside the child component. That works.

But if I do that with my loading message, it doesn’t either way. I must be missing something…

// login-form

    let error;
    if (this.props.error) {
      error = (
        <div>
          <p className="form-error" aria-live="polite">
            {this.props.error}
          </p>
        </div>
      );
    }

    let loadMsg;
    if (this.props.loading) {
      console.log('loading'); // not showing
      loadMsg = (
        <div className="load-msg">
          <h2>Loading...</h2>
        </div>
      );
    }
// ...
return (
// ...
        {error}
        {loadMsg}
// ...

EDIT Okay, it’s working with the loading message written in the parent component, mapped to props, and called in the parent with {loadMsg()}. I just placed the error above the form instead of inside it. However, I wonder how this works for accessibility.

okay there are few mixups but they are small

to get it working how you think.

you are checking for the loading prop

 if (this.props.loading) {
      console.log('loading'); // not showing
// you are looking for the prop loading. 

but in your loginForm you pass the prop loadMsg

 return (
    <section id="form-section">
      <h2 className="page-title">Login</h2>
      <LoginForm loadMsg={loadMsg} />   <---- here
    </section>

okay so first things

let loadMsg;  //  You should use const as it is not going to change 

    if (this.props.loading) {  // loading is undefined so nothing happens
      console.log('loading'); // not showing
      loadMsg = (
        <div className="load-msg">
          <h2>Loading...</h2>
        </div>
      );
    }
//  To fix it as is.. do this
 
<LoginForm loading={... in here you put your loading state from the store  } />

There are some other little things you can do instead for example you don’t need to declare the LoadMsg inside the component. it is a function you can pass it loading as an argument.

const loadMsg = loading => {
    if (loading) {
      return (
        <div className="load-msg">
          <h2>Loading...</h2>
        </div>
      );
    }


export function LoginPage(props) {

    return (
    <section id="form-section">
      <h2 className="page-title">Login</h2>
      <loadMsg> 
      <LoginForm />
    </section>

but if you do it this way you need to make sure your other components don’t load when loading is true.

Are you familiar with ternary operations? you can simply quite a bit by just having condition in the view

this.props error ? <errorComponent /> : null
this.props.loading ? <loading Component /> : null 

you can even string them together

{ error, loading } = this. props
error ? <errorComponent /> : loading ? <LoadingComponent /> : <loginForm /> 

You can also use arrow functions to get rid of the if statements in your code because arrow functions have an implicit return ( meaning you don’t need the return keyword )

for example our new LoadMsg function

const loadMsg = loading => loading 
?  ( <div className="load-msg">
          <h2>Loading...</h2>
        </div>
) : null

So loading is true return the divs, else return null (which won’t render anything)

sorry if that was over your head hope it helped