I'm making a React/Redux app. In one of my actions, dispatch is firing 6-8 times when called for no apparent reason. See addMarkersRequestAddress below in the action file for my component:
export function addMarkersSuccess(response) {
return {
type: 'addMarkersSuccess',
status: 'success',
response: response,
receivedAt: Date.now(),
};
}
export function addMarkersFailure(error) {
return {
type: 'addMarkersFailure',
status: 'error',
error: error,
receivedAt: Date.now(),
};
}
export function addMarkersRequestCoordinates(submitFormData) {
// Why is this always returning addMarkersFailure? Is it possibly related to why it always fires multiple times?
// Same code as in virtualFenceWalk actions
return (dispatch) => {
console.log('running addMarkersRequestCoordinates');
console.log('submitFormData: ',submitFormData);
let JSONbody = JSON.stringify(submitFormData);
console.log('JSONbody: ',JSONbody);
fetch('http://localhost:8080/virtualFence', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSONbody
}).then(function(response){
dispatch(addMarkersSuccess(response));
}).catch(function(error) {
dispatch(addMarkersFailure(error));
});
}
}
export function addMarkersRequestAddress(submitFormData) {
return (dispatch) => {
console.log('running addMarkersRequestAddress');
console.log('submitFormData: ',submitFormData);
let JSONbody = JSON.stringify(submitFormData);
console.log('JSONbody: ',JSONbody);
// Make a request to a backend route that gets the coordinates from the Google Maps API
fetch('http://localhost:8080/virtualFenceAddress', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSONbody
}).then(function(response){
console.log('addMarkersRequestAddress success');
console.log('response: ',response);
dispatch(addMarkersSuccess(response));
}).catch(function(error) {
console.log('addMarkersRequestAddress failure');
console.log('error: ',error);
dispatch(addMarkersFailure(error));
});
}
}
When this code runs, addMarkersSuccess will fire 6-8 times. It is somehow related to dispatch specifically, because if I remove the dispatch calls and leave only the console logs, addMarkersSuccess fires once as expected and that's it. It also seems unrelated to fetch or asynchronicity since an identical outcome occurs if fetch is removed and the same thing is tried in the main body of the function.
Here is the container wrapping around the component (since I've narrowed it down to an issue with how dispatch is called, as without dispatch other parts of the action only fire once, maybe there is an issue with how dispatch is set up here?):
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, View, Text, TouchableOpacity, TouchableHighlight } from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import VirtualFence from '../components/VirtualFence';
import * as VirtualFenceActions from '../actions/virtualFence';
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'flex-end',
alignItems: 'center',
},
back: {
margin: 10,
fontSize: 20,
},
});
// Map the Redux state to props
#connect(
state => ({
bigState: state,
markers: state.markers,
}),
dispatch => bindActionCreators(VirtualFenceActions, dispatch),
)
export default class VirtualFenceContainer extends Component {
render() {
return (
<View style={styles.container}>
<VirtualFence {...this.props} />
</View>
);
}
}
Here is where the action is called in the component itself:
render() {
const {
addMarkersRequestAddress, addMarkersSuccess, addMarkersFailure
} = this.props;
return (
<View>
<TouchableOpacity onPress={this.toggleModal}>
<Text style={styles.bottomText}>Add markers by street address</Text>
</TouchableOpacity>
<Modal isVisible={this.state.isVisible}>
<View style={{ flex: 1 }}>
<TouchableOpacity onPress={this.toggleModal}>
<Text style={styles.bottomText}>Hide me!</Text>
</TouchableOpacity>
<Form
ref="form"
type={Points}
options={pointsOptions}
/>
<Button title="Add form field" onPress={this.addFormField}></Button>
<Button title="Delete form field" onPress={this.deleteFormField}></Button>
<Button
title="Submit markers"
onPress={(argument)=>addMarkersRequestAddress(this.refs.form.getValue())}
/>
</View>
</Modal>
</View>
);
}
While not answering my question, some other answers here and elsewhere seemed to hint that the resolution may have something to do with my configureStore.js file, so here it is:
/* eslint global-require: 0 */
import { Platform } from 'react-native';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// Presumably I need to add the other action files here somehow? Nothing seems to change as long as one file is listed...
import * as actionCreators from './actions/activityTracker';
let composeEnhancers = compose;
if (__DEV__) {
// Use it if Remote debugging with RNDebugger, otherwise use remote-redux-devtools
/* eslint-disable no-underscore-dangle */
composeEnhancers = (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ||
require('remote-redux-devtools').composeWithDevTools)({
name: Platform.OS,
...require('../package.json').remotedev,
actionCreators,
});
/* eslint-enable no-underscore-dangle */
}
const enhancer = composeEnhancers(applyMiddleware(thunk));
// I think the problem with multiple dispatches may be in here
// See https://stackoverflow.com/questions/49734848/redux-dispatch-fires-multiple-times
export default function configureStore(initialState) {
const store = createStore(reducer, initialState, enhancer);
if (module.hot) {
module.hot.accept(() => {
store.replaceReducer(require('./reducers').default);
});
}
return store;
}
Please note that I don't really know what this file is doing. I began the app using react-native-boilerplate so this file is taken from there. If changes need to be made there, it would be super appreciated if you can detail what exactly those changes do.
EDIT 1: When this post was originally written, all dispatches after the first threw errors. After some further work in other parts of the application, the additional firings all log successful now. However, the essential question (the cause of the multiple firings) remains.
EDIT 2: Added the container wrapping around the component.
The cause of my problem turned out to be in the file where I call the combineReducers helper function. I did not suspect this file had anything to do with the problem, so I had not posted it. For components with multiple keys in the initial state object, I incorrectly thought I had to do an import for each key, when in fact I needed a single import for each reducer file. I imported six variables from the virtualFence reducer, and each one caused dispatch to fire.
This is the incorrect version:
import { combineReducers } from 'redux';
import nav from './nav';
import virtualFence from './virtualFence';
import latitude from './virtualFence';
import longitude from './virtualFence';
import latitudeDelta from './virtualFence';
import longitudeDelta from './virtualFence';
import markers from './virtualFence';
export default combineReducers({
nav,
latitude,
longitude,
latitudeDelta,
longitudeDelta,
markers,
virtualFence,
});
And this is the correct version:
import { combineReducers } from 'redux';
import nav from './nav';
import virtualFence from './virtualFence';
export default combineReducers({
nav,
virtualFence,
});
are you using preventDefault() when calling event this might be the case:
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
use preventdefault to disallow to call method when page is loading
<Button title="Add form field" onPress={this.addFormField}></Button>
<Button title="Delete form field" onPress={this.deleteFormField}></Button>
<Button
title="Submit markers"
onPress={(argument)=>addMarkersRequestAddress(this.refs.form.getValue())}
/>
So you state:
addMarkersSuccess will fire once, followed by several firings of addMarkersFailure
addMarkersFailure only gets called when there is an error. This error, of course, contains all the information you need to solve the problem. In particular, it has a stack that indicates not only the exact place the error was fired, but a complete call stack that indicates the entire call chain leading up to the error.
When a Promise has several stages, each stage has an opportunity to fail. A catch following any of the stages will be passed the error.
So:
Promise.resolve('This is just a string, and no error')
.then(theString => {
throw new Error('This is not the original promise result, but a new one: A rejection.');
})
.catch(err => {
console.log('Something went wrong. Maybe it happened in the original promise.');
console.log('Maybe it happened later. To find out, look closely at this:');
console.log(err.stack);
});
In your case, it's probably dispatch that throws. Now, there's nothing wrong with dispatch itself, but when it goes to call your reducer, the reducer is probably doing something wrong and throwing an error. This in turn leads your .catch callback (aka the rejection handler) to be called.
Since you did not include your reducer code I can't point out the error in it. However, you should be able to find it by examining the error message and stack.
In your addMarkersRequestAddress action, try to return the dispatch in .then() like:
.then((response) => {
dispatch(addMarkersSuccess(response));
}).catch((error) => {
dispatch(addMarkersFailure(error));
});
Maybe this will work.
Related
I am using "react-query" to call an API from a component . For the purpose of this question , I am returning a mock response from the API .
Every time , I open the dropdown , the useQuery function is called which in turn calls the mock API .
App.js
import React from 'react';
import './style.css';
import { QueryClient, QueryClientProvider } from 'react-query';
import { DropDown } from './Dropdown.js';
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<DropDown />
</div>
</QueryClientProvider>
);
}
Dropdown.js
import React from 'react';
import { useQuery } from 'react-query';
export const DropDown = () => {
console.log('DropDown re-rendered');
const { data, isLoading, isError } = useQuery('API', () => {
return new Promise((resolve, reject) => {
console.log('API called');
resolve(['mockData']);
});
});
return (
<>
<select>
<option> 1 </option>
<option> 2 </option>
</select>
</>
);
};
You can find the demo here : https://react-quxtxd.stackblitz.io
In the console you will see that every time you open the dropdown , useQuery is called.
Stackblitz Editor url : https://stackblitz.com/edit/react-quxtxd?file=src/Dropdown.js
As an alternative to avoid this , I can use the traditional useEffect to make the API calls but I was looking at leveraging the caching advantage that useQuery provides but I am stuck due to this "re-rendering" issue .
Any suggestions / modifications ?
This works for me
{ refetchOnWindowFocus: false }
Usage:
const { data, status } = useQuery("users", fetchUsers, {
refetchOnWindowFocus: false
});
It seems that the original stackblitz has been fixed, so the issue is no longer reproducible. For posterity:
You've probably seen a background refetch due to focusing the window. This is because staleTime defaults to 0 and refetchOnWindowFocus defaults to true. You can either turn off the flag, or set a higher staleTime (recommended).
So let me explain what's going on. I have this component called PaymentAccountLandingPage and in that component I import and render this component ConnectedAccountsButton which is a Stripe button that I created using a Stripe image and essentially making it just like button. So I want users to be redirected to the landing page for creating an account with Stripe when they click img/button. The problem is to do that I need to make an axios call with createAcctLink so that it returns a pathname that contains an accountId that I need to have in the path (I XXXXXX'd out the acctId below obviously). But I need that info in the path because someone else on my team parses at a later point when they get redirected back the problem is the response data that comes back from response.item is the following:
https://connect.stripe.com/express/onboarding/XXXXXXXXXXXX
Note it is not a string and I even used the toString method to TRY and turn it into a string but no luck. Does anyone have any other solutions? I don't want to use window.location.assign() or window.location.href because I already know how to do it that way but the reason why I want to figure out another way is because that takes 10-15 seconds to redirect users to Stripe. The reason why I want to use Link react-router-dom is because it's fast and I know there's also Redirect but either way I need the path to be a string so that's my issue. If anyone has any solutions or workarounds that'd be awesome. Here's my ConnectedAccountsButton:
import React from "react";
import connectbutton from "#assets/images/stripe/ConnectedAccountsButton.png";
import { createAccountLink } from "../../services/connectedAccountsService";
import { Link } from "react-router-dom";
class ConnectedAccountsButton extends React.Component {
state = {
path: "",
};
componentDidMount() {
createAccountLink()
.then(this.onAccountLinkSuccess)
.catch(this.onAccountLinkError);
}
onAccountLinkSuccess = (response) => {
let pathUrl = response.item.toString();
console.log("Success Response:", pathUrl);
this.setState(() => {
return {
path: pathUrl,
};
});
};
onAccountLinkError = (err) => {
console.error("Error Response: ", err);
};
render() {
return (
<Link to={this.state.path}>
<img
src={connectbutton}
style={{ width: "200px", cursor: "pointer" }}
alt="Connected Accounts Link"
/>
</Link>
);
}
}
export default ConnectedAccountsButton;
I'm answering my own question because I figured out the solution with a friends help. The strange thing is we used typeof on response.item and it was already a string. There's no reason this shouldn't have worked but it didn't. We even checked the React Dev Tools and sure enough in state was the correct path but for some reason when I clicked the img it would send me to one of my error routes.
This is what ended up working:
import React, { useState, useEffect } from "react";
import connectbutton from "#assets/images/stripe/ConnectedAccountsButton.png";
import { createAccountLink } from "../../services/connectedAccountsService";
import debug from "sabio-debug";
import { useHistory } from "react-router-dom";
const _logger = debug.extend("ConnectedAccountsButton");
const ConnectedAccountsButton = () => {
const [url, setUrl] = useState("");
const history = useHistory();
useEffect(() => {
createAccountLink().then(onAccountLinkSuccess).catch(onAccountLinkError);
}, []);
const redirectConnectedAccount = () => {
history.push(url);
};
const onAccountLinkSuccess = (response) => {
_logger("Success Response", response.item);
setUrl(response.item);
};
const onAccountLinkError = (err) => {
_logger("Error Response", err);
};
return (
<img
src={connectbutton}
style={{ width: "200px", cursor: "pointer" }}
alt="Connected Accounts Link"
onClick={redirectConnectedAccount}
/>
);
};
export default ConnectedAccountsButton;
I want to build some buttons (from react-bootstrap Buttons) that know when the mouse cursor has entered and left them. No problem with hooking this up, and I get everything to fire just fine. However, if I have several such buttons, and each gets a name passed down to it, how do I get the name into the reducer?
In the following example, I want bsName to be passed to the reducer, take a peek at mapsDispatchToProps:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
const InteractiveButton = ({
bsStyle,
bsName,
bsText,
dispButtonEnter,
dispButtonLeave
}) => (
<div>
<Button
onMouseEnter={dispButtonEnter}
onMouseLeave={dispButtonLeave}
bsStyle={bsStyle}
bsSize="large"
title={bsName}
block
>
{bsText}
</Button>
</div>
);
InteractiveButton.propTypes = {
bsStyle: PropTypes.string.isRequired,
bsName: PropTypes.string.isRequired,
bsText: PropTypes.string.isRequired,
dispButtonEnter: PropTypes.func.isRequired,
dispButtonLeave: PropTypes.func.isRequired
};
const dispButtonEnter = { type: 'BUTTON_MOUSE_ENTER' };
const dispButtonLeave = { type: 'BUTTON_MOUSE_LEAVE' };
function mapStateToProps() {
return {};
}
function mapDispatchToProps(dispatch) {
return {
dispButtonEnter: e => dispatch({
...dispButtonEnter,
buttonName: bsName <<<<<<<<<<<<<<<<<<<<<<<< something like this, but functioning
buttonEnterTarget: e.relatedTarget
}),
dispButtonLeave: e => dispatch({
...dispButtonLeave,
buttonName: bsName <<<<<<<<<<<<<<<<<<<<<<<< something like this, but functioning
buttonLeaveTarget: e.relatedTarget
})
};
}
export default connect(mapStateToProps, mapDispatchToProps)(InteractiveButton);
You should utilize closures properly here don't just dispatch directly instead make the call to the closure method that will receive the the name of button and then add that name in the payload.
closureFunction(btnName){
dispatch({
...dispButtonEnter,
payload:{name: btnName}
})
}
In reducer you will be able to access the payload in action argument.
My action creator is not calling my reducer. Any help will be much appreciated.
types.js
export const SELECTED_FOOD = 'selected_food';
index.js (action creator / action)
import {
SELECTED_FOOD
} from './types';
export function selectedFood({data}) {
console.log('SELECTED_FOOD **********************',data);
return({
type: SELECTED_FOOD,
payload: data
});
}
Output from console.log in action creator
Object {_id: "18240", description: "Croissants, apple", score: 0.75, fields: Object}
selected_food_reducer.js
import {
SELECTED_FOOD
} from '../actions/types';
export default function(state = [], action) {
switch(action.type) {
case SELECTED_FOOD:
console.log('Selected Food Reducer *************', state);
return action.payload ;
}
return state;
}
EDIT component failing to call dispatch.
I should have added this on my initial post, it appears there is something wrong in how dispatch is called. ESLint is flagging dispatch on line 3 for defined but never used.
import React from 'react';
import { connect } from 'react-redux';
import { dispatch } from 'react-redux';
import { selectedFood } from '../actions/index';
class TableRow extends React.Component {
render() {
const {
data
} = this.props;
console.log('PROPS TableRow', this.props);
const row = data.map((data) =>
<tr onClick={() => selectedFood({data})}>
<td key={data.description}>{data.description}</td>
<td key={data.id}>{data.fields.nutrients[0].amountPer100G}</td>
<td key={data.id}>{data.fields.nutrients[1].amountPer100G}</td>
<td key={data.id}>{data.fields.nutrients[4].amountPer100G}</td>
</tr>
);
return (
<tbody>{row}</tbody>
);
}
}
const mapStateToProps = (state) => {
return {
selectedFood: state.selectedFood
}
}
const mapDispatchToProps = (dispatch) => {
console.log('IN mapDispatchToProps')
return {
onClick: ({data}) => {
dispatch(selectedFood({data}))
}
}
}
export default connect(mapStateToProps, mapDispatchToProp)(TableRow);
The action creator does not call the reducer. It is what it is, nothing more, it creates an action - that is, an object with the action type and the payload for that action.
You need to dispatch an action, to force redux to call the reducer, and that is the place where you use your action creator, that is:
import { Dispatch } from "redux";
import { selectedFood } from "./actions";
Dispatch(selectedFood({type:"hamburger"}));
that should call the reducer, however mostly you'll not call the Dispatch directly, rather in your mapDispatchToProps method used to connect your react component to the redux store.
There are plenty of sample how to use react-redux to use above map functionality, so I would suggest to read into it, and to read how redux works.
====== EDIT after question updated =========
So firstly dispatch from import is not used and ESLint is right telling it, you don't need that import since in:
const mapDispatchToProps = (dispatch) => {
console.log('IN mapDispatchToProps')
return {
onClick: ({data}) => {
dispatch(selectedFood({data}))
}
}
}
you don't call dispatch from your import only from the argument, it's passed to your mapDispatchToProps by the connect function.
Then this is just plain wrong:
<tr onClick={() => selectedFood({data})}>
you imported an action creator which is called on click of table row, that is an action definition is created by the action creator, and that's it. Your code does exactly what you wrote.
The mapDispatchToProps function does what the name suggests - it maps dispatch functions to props of your component.
So it should be:
<tr onClick={() => this.props.onClick({data})}>
and that should dispatch the action and work.
However I would strongly suggest to take some courses or read more about react, redux and react-redux, because your code samples and the question itself suggest, that you are just trying to get something to work, without the basic understanding how it works, and even how javascript works. Sorry for that comment but that's how it looks like.
I think you might need to show how you're importing other files. My observations from what you shared:
1) You need to import SELECTED_FOOD from types.
2) Your return state should be within the context of the switch statement.
There is an often forgotten rule in Redux that when you change the reducer you need to restart your localhost server.
You are doing it correctly- there are many ways to architect Redux into React. You don't need to use mapDispatchToProps if you are importing your action creators using connect()().
Just restart your localhost server if you don't see any typos. (I usually use NPM, so I control+c out of npm start in the terminal and run npm start again each time I add a new reducer.)
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 accomplished this in a hacky sort of fashion.
In my Login component I use the componentDidUpdate 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({
componentDidUpdate(){
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} />
New User?
</div>
)
}
});
Notice that I am passing router and user to my LoginForm component. I do this in order to use ANOTHER componentDidUpdate where I use the .push method on router like so:
import React from "react";
const LoginForm = React.createClass({
componentDidUpdate(){
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="kitties#kit.com" />
<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 complicated solution and not what is considered a best practice. I've tried adding a custom listener method within my Login component 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 components tidier, or take advantage of componentDidUpdate 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 component "sprinkles" all my redux stuff and state data into my container component 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;
There are several things that could make the login flow easier to manage and reason about and tidy up your components a bit. First a few general points:
I'm not certain why you have divided login logic between the two components? The idiomatic React/Redux approach would be to have a container component that deals with logic, and a presentation component that deals with presentation. Currently both components do a little bit of each.
You don't need to pass props.router up and down through your components. React router provides a HOC that provides router as a props called withRouter( docs here). You just wrap a component 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 components (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 components. 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 component 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 components 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="kitties#kit.com" />
<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 component. The LoginForm component can be written simply as a function above because it has no responsibility to deal with logic. The container is also a very simple component - 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 component 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({
componentDidUpdate(){
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} />
New User?
</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 component to Login component; 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!