mxstbr / login-flow

:key: A login/register flow built with React&Redux
https://loginflow.mxstbr.com
MIT License
1.6k stars 215 forks source link

Update App Structure #18

Open ghost opened 8 years ago

ghost commented 8 years ago

Hey, I was wonding if you could bring this example up to date with the styles outlined in https://github.com/mxstbr/react-boilerplate/tree/v3.0.0/app

mxstbr commented 8 years ago

Yeah, definitely coming. Would appreciate any help if you're up for submitting a PR! :+1:

lhammond commented 8 years ago

hi, @mxstbr / @OpenKerley

I'm attempting to do this as my first foray into React dev. If I can get it all working, I'll submit the PR.

I'm currently over my head and and guidance is appreciated. Here is what I've done so far.

  1. cloned and started the boilerplate and added some additional modules that login-flow uses
    • npm -i bcryptjs --save
    • npm -i whatwg-fetch --save
  2. used npm generate to create LoginForm component and LoginPage container
  3. copied code from the login-flow files over the generated code for the above in the generated index.js files
  4. copied the requisite utils files into the boilerplate project and adjusted all import file paths
  5. updated reducer.js with the
    • updated the constant import statement
    • imported auth from utils
    • added the additional states to initialState
    • added CHANGE_FORM, SET_AUTH, SENDING_REQUEST cases to switch statement
  6. added a button to the HomePage to navigate to /login
  7. added "/login" to app/routes.js
  8. added a new function in app/selectors.js to inject the state .. using the same approach as the other functions
const selectState = () => {
  return (state) => {
    console.log("selectors.js : returning state",state);
    return {
      data: state
    };
  };
};
  1. updated HomePage/index.js to inject the state ( not sure if this is the correct approach )
export default connect(createSelector(
  selectState(),
  selectRepos(),
  selectUsername(),
  selectLoading(),
  selectError(),
  (state, repos, username, loading, error) => ({ state, repos, username, loading, error })
), mapDispatchToProps)(HomePage);

I can click the button and navigate to the LoginPage, but don't understand how to get the state injected into the render. "this.props.data" is undefined in the render function of LoginPage/index.js

What am I missing here?
My guess is that I'm doing something wrong in how/where I'm injecting the state. I see the state function in login-flow that creates the data json object .. do I need to add that to the connect function of any page that will navigate to LoginPage?

Below is the console ouput .. and I'm printing out this.props.data

image

mxstbr commented 8 years ago

This all looks good at first glance, did you connect the LoginPage component? (similarly to the HomePage one)

lhammond commented 8 years ago

As a hack, I created the object in the render() function as below. This atleast has the page rendering.

    //const { formState, currentlySending } = this.props.data;
    const formState = {
      username: '',
      password: ''
    }
    const currentlySending = false;

I'm connecting the LoginPage at the bottom of LoginPage/index.js..as below

// Which props do we want to inject, given the global state?
function selectData(state) {
  console.log("LoginPage state function",state);
  return {
    data: state,
  };
}

// Wrap the component to inject dispatch and state into it
export default connect(selectData)(LoginPage);

I also

The LoginPage renders, and styles are there. There is a problem with the field inputs that I'm now looking into. When I type, I get the below.

image

lhammond commented 8 years ago

I added thunk and looks like that resolved the above issue.

where is the appropriate place for the actions and constants to be?

the login-flow demo has them in the app component .. should they remain there? or should the be in the LoginPage or LoginForm?

lhammond commented 8 years ago

So I think I'm going to have to focus on figuring out the data injection issue.

Because render() gets recalled after the form state changes, and I'm hardcoding the data to be empty strings, the text inputs are getting cleared each time I type into the input.

mxstbr commented 8 years ago

I added thunk and looks like that resolved the above issue. Where is the appropriate place for the actions and constants to be?

Instead of using thunked actions, move those asynchronous actions to redux-saga.

Because render() gets recalled after the form state changes, and I'm hardcoding the data to be empty strings, the text inputs are getting cleared each time I type into the input.

Those'll have to live in the redux state!

lhammond commented 8 years ago

thanks for the help.

I'm working on the state stuff now.

the code below taken from login-flow does not populate the data prop ( works in login-flow ) I'm initializing the component the same way that the boilerplate HomePage does.

Couple of Questions 1) Any recommendations on where to debug this props issue? The only difference between login-flow seems to be how the route is being setup .. should I be using reselect for this? 2) Which async actions should be sagas? The actual HTTP request, or other actions, too?

function select(state) {
  return {
    data: state
  };
}

// Wrap the component to inject dispatch and state into it
export default connect(select)(LoginPage);

image

lhammond commented 8 years ago

I've gotten things mostly working. Tomorrow or later this week I'll write up what all I've done. I'll need some input as to whether I'm doing things correctly, but I'm now attempting auth.login .. where do I find th e request.js? I tried pasting in the request.js from login-flow, but it doesn't work.

lhammond commented 8 years ago

here are some of the things I had to do to get things partially working please take a look and see if you can identify some bad practices. the saga code is from me looking at https://github.com/yelouafi/redux-saga/tree/master/examples/real-world

for brevity I omitted the API call files.

a couple of things I'm unsure about.

1) am I dispatching the event to the saga correctly from LoginPage/index.js? 2) am I using the selectors correctly when connect()ing the LoginPage/index.js? 3) can/should the reducer code be optimized or changed? 4) when completing async work in a saga, how do I return flow to the app?

Changes Made

LoginPage/reducer.js

Using the existing boilerplate code, I was unable to inject state props into my render() method

To get the props injected, I had to change the LoginPage reducer function as below

function loginPageReducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_FORM:

      var streamMap = {};

      for (var i in action.stream_data) {
        var item = action.stream_data[i];
        streamMap[item.id] = item;
      }
      return state.merge(fromJS(streamMap).toOrderedMap());
      break;
    case SET_AUTH:
      return assign({}, state, {
        loggedIn: action.newState
      });
      break;
    default:
      return state;
  }
}

LoginPage/index.js

changed boilerplate connect() added selector and mapStateToProps for dispatch

/*
 * LoginPage
 *
 * Users login on this page
 * Route: /login
 *
 */

import React, { Component } from 'react';
import { connect } from 'react-redux';
import Form from 'components/LoginForm';
import auth from 'utils/auth';
import { login } from './actions';
import LoadingIndicator from 'components/LoadingIndicator';

import { createSelector } from 'reselect';

import { LOGIN } from './constants';

import {
    selectState,
} from './selectors';

import styles from './styles.css';

export default class LoginPage extends React.Component {
  render() {

    const dispatch = this.props.dispatch;

    // unable to access the formState using dot-notation .. I think this is related to Immutable.fromJS() in the reducer
    const formState = this.props.myTestState.data.get('formState');
    const currentlySending = this.props.myTestState.data.get('currentlySending');

    return (
        <div className={styles.formPageWrapper}>
          <div className={styles.formPageFormWrapper}>
            <div className={styles.formPageFormHeader}>
              <h2 className={styles.formPageFormHeading}>Login</h2>
            </div>
            {/* While the form is sending, show the loading indicator,
             otherwise show "Log in" on the submit button */}
            <Form data={formState} dispatch={dispatch} location={location} history={this.props.history} onSubmit={::this._login} btnText={"Login"} currentlySending={currentlySending}/>
          </div>
        </div>
    );
  }

  _login(username, password) {
    console.log('LoginPage/index.js : _login()',username,password);
    this.props.dispatch(login({
      username: username,
      password: password
    }));
  }
}

// Which props do we want to inject, given the global state?
function select(state) {
  return {
    data: state
  };
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    dispatch
  };
};

export default connect(createSelector(
    selectState(),
    (myTestState) => ({ myTestState })
), mapDispatchToProps)(LoginPage);

LoginPage/constants.js

export const CHANGE_FORM = 'app/LoginPage/CHANGE_FORM';
export const SET_AUTH = 'app/LoginPage/SET_AUTH';
export const SENDING_REQUEST = 'app/LoginPage/SENDING_REQUEST';
export const LOGIN = 'app/LoginPage/LOGIN';

LoginPage/actions.js

/*
 *
 * LoginPage actions
 *
 */

import {
    SET_AUTH,
    CHANGE_FORM,
    SENDING_REQUEST,
    LOGIN
} from './constants';

/**
 * event for LOGIN dispatch
 *
 */
export function login(formState) {
  return { type: LOGIN, formState };
}

/**
 * Sets the authentication state of the application
 * @param {boolean} newState True means a user is logged in, false means no user is logged in
 */
export function setAuthState(newState) {
  return { type: SET_AUTH, newState };
}

/**
 * Sets the form state
 * @param  {object} newState          The new state of the form
 * @param  {string} newState.username The new text of the username input field of the form
 * @param  {string} newState.password The new text of the password input field of the form
 * @return {object}                   Formatted action for the reducer to handle
 */
export function changeForm(newState) {
  //console.log("LoginPage/actions.js : changeForm : newState : ",newState);
  return { type: CHANGE_FORM, newState };
}

/**
 * Sets the requestSending state, which displays a loading indicator during requests
 * @param  {boolean} sending The new state the app should have
 * @return {object}          Formatted action for the reducer to handle
 */
export function sendingRequest(sending) {
  return { type: SENDING_REQUEST, sending };
}

/**
 * Forwards the user
 * @param {string} location The route the user should be forwarded to
 */
function forwardTo(location) {
  console.log('forwardTo(' + location + ')');
  browserHistory.push(location);
}

let lastErrType = "";

/**
 * Called when a request failes
 * @param  {object} err An object containing information about the error
 * @param  {string} err.type The js-form__err + err.type class will be set on the form
 */
function requestFailed(err) {
  // Remove the class of the last error so there can only ever be one
  removeLastFormError();
  const form = document.querySelector('.form-page__form-wrapper');
  // And add the respective classes
  form.classList.add('js-form__err');
  form.classList.add('js-form__err-animation');
  form.classList.add('js-form__err--' + err.type);
  lastErrType = err.type;
  // Remove the animation class after the animation is finished, so it
  // can play again on the next error
  setTimeout(() => {
    form.classList.remove('js-form__err-animation');
  }, 150);
}

/**
 * Removes the last error from the form
 */
function removeLastFormError() {
  const form = document.querySelector('.form-page__form-wrapper');
  form.classList.remove('js-form__err--' + lastErrType);
}

/**
 * Checks if any elements of a JSON object are empty
 * @param  {object} elements The object that should be checked
 * @return {boolean}         True if there are empty elements, false if there aren't
 */
function anyElementsEmpty(elements) {
  for (let element in elements) {
    if (!elements[element]) {
      return true;
    }
  }
  return false;
}

LoginPage/sagas.js

import { take, call, put, select, race } from 'redux-saga/effects';

import { LOGIN } from 'containers/LoginPage/constants';
import { LOCATION_CHANGE } from 'react-router-redux';
import { getCreds } from 'containers/LoginPage/selectors';
import bcrypt from 'bcryptjs';
import genSalt from 'utils/salt';
import auth from 'utils/auth';
import { api, history } from 'services';
import * as actions from './actions';

// each entity defines 3 creators { request, success, failure }
const { user, session } = actions

/***************************** Subroutines ************************************/

// resuable fetch Subroutine
// entity :  user | repo | starred | stargazers
// apiFn  : api.fetchUser | api.fetchRepo | ...
// id     : login | fullName
// url    : next page url. If not provided will use pass it to apiFn
function* fetchEntity(entity, apiFn, id, url) {
  console.log("fetchEntity");
  yield put( entity.request(id) )
  const {response, error} = yield call(apiFn, url || id)
  if(response)
    yield put( entity.success(id, response) )
  else
    yield put( entity.failure(id, error) )
}

// yeah! bind Generators
export const loginUser       = fetchEntity.bind(null, session, api.loginUser)

export default function* root(getState) {
}

export function* login(action) {

  while (true) { // eslint-disable-line no-constant-condition
    const watcher = yield race({
      login: take(LOGIN),
      stop: take(LOCATION_CHANGE), // stop watching if user leaves page
    });

    if (watcher.stop) break;

    const creds = yield select(getCreds());

    yield call(loginUser, "");

  }
}

// All sagas to be loaded
export default [
  defaultSaga,
  login
];

// Individual exports for testing
export function* defaultSaga() {

}
wullaski commented 8 years ago

Hey @lhammond,

I'm trying to get this up and running within the latest version of the boilerplate as well. Are you still using thunk? I've followed along and am stuck at re-working how it's handling the formState. Could you by any chance share your repo? Thanks for working on this.

lhammond commented 8 years ago

@wullaski

I am no longer using thunk. I don't currently have the repo online, but should have it up for you early next week.

that said, everything I've done is above. you should be able to copy/paste all of that stuff.

michaelcuneo commented 7 years ago

I am also working on this... would be good to get it finalised. I will eventually.