最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - React event listeners based on changes in state made with Redux - Stack Overflow

programmeradmin0浏览0评论

Good afternoon,

I am having some difficulty working with React and Redux when I am trying to redirect users of my app based on changes in state.

At a high level: I want my app's route to change when my user object in state is populated with information from home / to /student/:username.

Right now I have acplished this in a hacky sort of fashion.

In my Login ponent I use the ponentDidUpdate lifecycle hook to listen and dispatch an action when an access token from a 3rd party API is passed back to the client from my Express server.

import React from "react";

import LoginForm from "../minor/LoginForm";

const Login = React.createClass({

  ponentDidUpdate(){
    const {success, access_token} = this.props.loginStatus;

    if (success === true && access_token !== null){
      console.log("It worked getting your data now...");
      this.props.getUserData(access_token);
    } else if (success === false || access_token === null){
      console.log("Something went wrong...");
    } 

  }, 
render(){

    return(

        <div className="loginComponentWrapper">
          <h1>Slots</h1>
          <LoginForm loginSubmit={this.props.loginSubmit}
                     router={this.props.router}
                     user={this.props.user} />
          <a href="/register">New User?</a>
        </div>
      )
  }
});

Notice that I am passing router and user to my LoginForm ponent. I do this in order to use ANOTHER ponentDidUpdate where I use the .push method on router like so:

import React from "react";

const LoginForm = React.createClass({


  ponentDidUpdate(){
    const {router, user} = this.props;

    if (user.username !== null){
       router.push(`/student/${user.username}`);
    }
  },


  render(){

      return(
        <div className="loginWrapper">
          <div className="loginBox">
            <form className="loginForm" action="">
              <input ref={(input) => this.username_field = input} type="text" placeholder="username" defaultValue="[email protected]" />
              <input ref={(input) => this.password_field = input} type="text" placeholder="password" defaultValue="meowMeow3" />
              <button onClick={this.loginAttempt}>Login</button>
            </form>
          </div>
        </div>
        )
  }

});

Sure it works but I'm certain this is a overly plicated solution and not what is considered a best practice. I've tried adding a custom listener method within my Login ponent but I've never had it successfully fire off, instead my app gets stuck in a loop.

Is there a way I can do this using Redux and keep my ponents tidier, or take advantage of ponentDidUpdate in a more efficient way?

As always I'd appreciate some more experienced eyes on my issue and look forward to some feedback!

Thanks

UPDATE

App.js

import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actionCreators from "../actions/userActions.js";

import StoreShell from "./StoreShell.js";

function mapStateToProps(state){
  return{
    loginStatus: state.loginStatus,
    user: state.user
  }
}

function mapDispatchToProps(dispatch){
  return bindActionCreators(actionCreators, dispatch)
}

const App = connect(mapStateToProps, mapDispatchToProps)(StoreShell);

export default App;

This ponent "sprinkles" all my redux stuff and state data into my container ponent named StoreShell that in turn passes all that data to props for the elements that make up the UI like Login and LoginForm am I taking too many steps?

StoreShell.js

import React from "react";

const StoreShell = React.createClass({

    render(){

        return(

            <div className="theBigWrapper">
              {React.cloneElement(this.props.children, this.props)}
            </div>

          )

    }
})

export default StoreShell;

Good afternoon,

I am having some difficulty working with React and Redux when I am trying to redirect users of my app based on changes in state.

At a high level: I want my app's route to change when my user object in state is populated with information from home / to /student/:username.

Right now I have acplished this in a hacky sort of fashion.

In my Login ponent I use the ponentDidUpdate lifecycle hook to listen and dispatch an action when an access token from a 3rd party API is passed back to the client from my Express server.

import React from "react";

import LoginForm from "../minor/LoginForm";

const Login = React.createClass({

  ponentDidUpdate(){
    const {success, access_token} = this.props.loginStatus;

    if (success === true && access_token !== null){
      console.log("It worked getting your data now...");
      this.props.getUserData(access_token);
    } else if (success === false || access_token === null){
      console.log("Something went wrong...");
    } 

  }, 
render(){

    return(

        <div className="loginComponentWrapper">
          <h1>Slots</h1>
          <LoginForm loginSubmit={this.props.loginSubmit}
                     router={this.props.router}
                     user={this.props.user} />
          <a href="/register">New User?</a>
        </div>
      )
  }
});

Notice that I am passing router and user to my LoginForm ponent. I do this in order to use ANOTHER ponentDidUpdate where I use the .push method on router like so:

import React from "react";

const LoginForm = React.createClass({


  ponentDidUpdate(){
    const {router, user} = this.props;

    if (user.username !== null){
       router.push(`/student/${user.username}`);
    }
  },


  render(){

      return(
        <div className="loginWrapper">
          <div className="loginBox">
            <form className="loginForm" action="">
              <input ref={(input) => this.username_field = input} type="text" placeholder="username" defaultValue="[email protected]" />
              <input ref={(input) => this.password_field = input} type="text" placeholder="password" defaultValue="meowMeow3" />
              <button onClick={this.loginAttempt}>Login</button>
            </form>
          </div>
        </div>
        )
  }

});

Sure it works but I'm certain this is a overly plicated solution and not what is considered a best practice. I've tried adding a custom listener method within my Login ponent but I've never had it successfully fire off, instead my app gets stuck in a loop.

Is there a way I can do this using Redux and keep my ponents tidier, or take advantage of ponentDidUpdate in a more efficient way?

As always I'd appreciate some more experienced eyes on my issue and look forward to some feedback!

Thanks

UPDATE

App.js

import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as actionCreators from "../actions/userActions.js";

import StoreShell from "./StoreShell.js";

function mapStateToProps(state){
  return{
    loginStatus: state.loginStatus,
    user: state.user
  }
}

function mapDispatchToProps(dispatch){
  return bindActionCreators(actionCreators, dispatch)
}

const App = connect(mapStateToProps, mapDispatchToProps)(StoreShell);

export default App;

This ponent "sprinkles" all my redux stuff and state data into my container ponent named StoreShell that in turn passes all that data to props for the elements that make up the UI like Login and LoginForm am I taking too many steps?

StoreShell.js

import React from "react";

const StoreShell = React.createClass({

    render(){

        return(

            <div className="theBigWrapper">
              {React.cloneElement(this.props.children, this.props)}
            </div>

          )

    }
})

export default StoreShell;
Share Improve this question edited Mar 7, 2017 at 19:28 m00saca asked Mar 6, 2017 at 23:20 m00sacam00saca 3631 gold badge8 silver badges22 bronze badges
Add a ment  | 

2 Answers 2

Reset to default 3

There are several things that could make the login flow easier to manage and reason about and tidy up your ponents a bit. First a few general points:

I'm not certain why you have divided login logic between the two ponents? The idiomatic React/Redux approach would be to have a container ponent that deals with logic, and a presentation ponent that deals with presentation. Currently both ponents do a little bit of each.

You don't need to pass props.router up and down through your ponents. React router provides a HOC that provides router as a props called withRouter( docs here). You just wrap a ponent in withRouter and props.router is available - everything else stays the same.

export default withRouter(LoginForm);

My personal feeling is that the URI is part of your app's state, and as such it should be maintained within the store and updated by dispatching actions. There is a cracking library available to do this - react-router-redux. Once you have it setup then you can pass the push method to your ponents (or elsewhere... see the next points) to navigate:

import { push } from 'react-router-redux';
import { connect } from 'react-redux';

const NavigatingComponent = props => (
    <button onClick={() => props.push('/page')}>Navigate</button>
);

const mapStateToProps = null;
const mapDispatchToProps = {
    push
};

export default connect(mapStateToProps, mapDispatchToProps)(NavigatingComponent);

One of the great things about having the URI in our store is that we don't need access to props.router to change location, which opens the avenue of moving the login logic outside of our ponents. There are several ways we can do this, the simplest is probably redux-thunk, which allows our action creators to return functions instead of objects. We could then write our ponent to simply call a login function with username and password entered, and our thunk takes care of the rest:

import { push } from 'react-router-redux';

// action creators....
const loginStarted = () => ({
    type: 'LOGIN_STARTED'
)};

const loginFailed = (error) => ({
    type: 'LOGIN_FAILED',
    payload: {
       error
    }
};

const loginSucceeded = (user) => ({
    type: 'LOGIN_SUCCEEDED',
    payload: {
       user
    }
};

const getUserData = (access_token) => (dispatch) => {
    Api.getUserData(access_token) // however you get user data
    .then(response => {
        dispatch(loginSucceeded(response.user));
        dispatch(push(`/student/${response.user.username}`));
    });

export const login = (username, password) => (dispatch) => {
    dispatch(loginStarted());

    Api.login({ username, password }) // however you call your backend
    .then(response => {
        if (response.success && response.access_token) {
            getUserData(access_token)(dispatch);
        } else {
            dispatch(loginFailed(response.error));
        }
    });
}

The only thing your ponents do is call the initial login function, which could be implemented something like:

Container:

import React from 'react';
import { connect } from 'react-redux';

import { login } from '../path/to/actionCreators';
import LoginForm from './LoginForm';

const LoginContainer = React.createClass({
    handleSubmit() {
        this.props.login(
           this.usernameInput.value,
           this.passwordInput.value
        );
    },

    setUsernameRef(input) {
        this.usernameInput = input;
    },

    setPasswordRef(input) {
        this.passwordInput = input;
    },

    render() {
        return (
           <LoginForm
              handleSubmit={this.handleSubmit.bind(this)}
              setUsernameRef={this.setUsernameRef.bind(this)}
              setPasswordRef={this.setPasswordRef.bind(this)}
           />
        );
    }
});

const mapStateToProps = null;
const mapDispatchToProps = {
    login
};

export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer);

Component:

import React from 'react';

export const LoginForm = ({ handleSubmit, setUsernameRef, setPasswordRef }) => (  
    <div className="loginWrapper">
        <div className="loginBox">
            <form className="loginForm" action="">
                <input ref={setUsernameRef} type="text" placeholder="username" defaultValue="[email protected]" />
                <input ref={setPasswordRef} type="text" placeholder="password" defaultValue="meowMeow3" />
                <button onClick={handleSubmit}>Login</button>
            </form>
        </div>
    </div>
);

This has achieved separation of a logic/data providing container, and a stateless presentational ponent. The LoginForm ponent can be written simply as a function above because it has no responsibility to deal with logic. The container is also a very simple ponent - the logic has all been isolated in our action creator/thunk and is much easier to read and reason about.

redux-thunk is just one way of managing asynchronous side effects with redux - there are many others with different approaches. My personal preference is toward redux-saga, which may be interesting for you to look at. In my own redux journey, however, I certainly benefited from using and understanding redux-thunk first before finding it's limitations/drawbacks and moving on, and would suggest this route to others.

If you're using react-router version 4.x.x: You can just render a Redirect ponent that handles the redirection for you (see example in react-router docs).

import React from "react";
import { Redirect } from "react-router";

import LoginForm from "../minor/LoginForm";

const Login = React.createClass({

  ponentDidUpdate(){
    const {success, access_token} = this.props.loginStatus;

    if (success === true && access_token !== null){
      console.log("It worked getting your data now...");
      this.props.getUserData(access_token);
    } else if (success === false || access_token === null){
      console.log("Something went wrong...");
    } 

  }

  render(){
    // if user is defined, redirect to /student/:username
    if (this.props.user.username !== null) {
      return (<Redirect to={ `/student/${this.props.user.username}` } />)
    }

    // otherwise render the login form
    return(
      <div className="loginComponentWrapper">
        <h1>Slots</h1>
        <LoginForm loginSubmit={this.props.loginSubmit}
                   router={this.props.router}
                   user={this.props.user} />
        <a href="/register">New User?</a>
      </div>
    )
  }
});

If you're using react-router version 3.x.x: The the way you're doing it is mostly correct. I would move the redirect from the LoginForm ponent to Login ponent; that way LoginForm does not need to depend on the user prop.

I know, I don't like the pattern much either.

Hope this helps!

发布评论

评论列表(0)

  1. 暂无评论