I am trying to dispatch another action inside one async action creator. However, when I am using dispatch function provided by redux-thunk middleware, it is throwing Uncaught TypeError: dispatch is not a function error.
Below are the files for more help
store/store.dev.js
use strict';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from '../reducers';
import ReduxThunk from 'redux-thunk';
export default function configureStore(initialState = {}) {
const store = createStore(rootReducer, initialState, applyMiddleware(ReduxThunk));
console.log('store returned', store);
return store;
}
src/index.js
'use strict';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { hydrate } from 'react-dom';
import { Provider } from 'react-redux';
import { renderRoutes } from 'react-router-config';
import configureStore from './store';
import App from './containers/App.js';
import routes from './routes/index.js';
hydrate(
<Provider store={configureStore()}>
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
reducer
'use strict';
import { LOGIN_SUBMITTED, LOGIN_COMPLETED } from '../constants/ActionTypes.js';
const loginSubmitReducer = (state = [], action) => {
console.log('in reducer');
switch (action.type) {
case LOGIN_SUBMITTED:
console.log('login submitted case');
return Object.assign({}, state, {
loginSubmitted: true
});
case LOGIN_COMPLETED:
console.log('login completed case');
return Object.assign({}, state, {
loginCompleted: true
});
default:
console.log('in default case');
return {
a:1
};
}
}
export default loginSubmitReducer;
containers/App.js
'use strict';
import React from 'react';
import { loginCompleted, loginSubmitted } from '../actions';
import { createStructuredSelector, createSelector } from 'reselect';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Header, ButtonDemo, LoginForm } from '../components';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
class App extends React.Component {
constructor(props, context) {
super(props);
this.state = {
appState: {
entry: 'yes'
}
}
console.log('props in App constructor', props);
console.log('context in App constructor', context);
this.newFunc = this.newFunc.bind(this);
}
newFunc () {
console.log('props', this.props);
}
render() {
console.log('props in main App', this.props);
console.log('Actions', this.props.actions);
console.log('context in App', this.context);
// this.props.actions.loginCompleted();
const muiTheme = getMuiTheme({
lightBaseTheme
});
return (
<MuiThemeProvider muiTheme={muiTheme}>
<div>
<Header />
<LoginForm
appstate = {this.props.appState}
dispatchAction = {this.props.actions}
/>
</div>
</MuiThemeProvider>
);
}
}
// const mapStateToProps = createStructuredSelector({});
const mapStateToProps = (state) => {
return { appState: state.loginSubmitReducer };
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ loginCompleted, loginSubmitted }, dispatch)
};
// dispatch(loginCompleted());
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
components
'use strict';
import React, { PropTypes } from 'react';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import FloatingActionButton from 'material-ui/FloatingActionButton';
import ContentAdd from 'material-ui/svg-icons/content/add';
import { loginAction } from '../actions';
// import '../css/style.css';
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.loginSubmit = this.loginSubmit.bind(this);
}
loginSubmit() {
console.log('Login fomr submitted', this.props);
console.log('loginAction', loginAction);
loginAction()();
}
render () {
console.log('props when login rendered', this.props);
return (
<div className="loginForm">
<div className="title">LOGIN</div>
<form>
<TextField
hintText="Username"
floatingLabelText="Username"/><br/>
<br/>
<TextField
hintText="Password"
type="Password"
floatingLabelText="Password"/><br/>
<br/>
<RaisedButton
label="Login"
secondary={true}
onClick={this.loginSubmit}
className="submitBtn" /><br/>
<br/>
<FloatingActionButton
secondary={true}
className="registerBtn">
<ContentAdd />
</FloatingActionButton>
</form>
</div>
)
}
}
export default LoginForm;
actions/loginAction.js
'use strict';
// import { polyfill } from 'es6-promise';
// require('es6-promise').polyfill();
// require('isomorphic-fetch');
import request from 'superagent';
// import fetch from 'isomorphic-fetch';
import loginCompleted from './loginCompletedAction.js';
import loginSubmitted from './loginSubmittedAction.js';
const loginAction = (dispatch) => {
console.log('validateUserLogin executed', this);
console.log('validateUserLogin dispatch', dispatch);
return (dispatch) => {
// console.log('first return', dispatch);
// this.props.dispatch(loginSubmitted());
// this.props.dispatchAction.loginSubmitted();
dispatch(loginSubmitted());
request.get('http://localhost:3000/loginSubmit')
.end((err, res) => {
console.log('res', res);
console.log('err', err);
// this.props.dispatchAction.loginCompleted();
});
}
}
export default loginAction;
Folder structure
I am using redux-thunk middleware that exposes dispatch function to async action creators. Still it doesn't work.
Please help!
Your first argument of the action creator should not be the dispatch but the argument you pass to the action creator.
const loginAction = () => (dispatch) => {
console.log('validateUserLogin executed', this);
console.log('validateUserLogin dispatch', dispatch);
// console.log('first return', dispatch);
// this.props.dispatch(loginSubmitted());
// this.props.dispatchAction.loginSubmitted();
dispatch(loginSubmitted());
request.get('http://localhost:3000/loginSubmit')
.end((err, res) => {
console.log('res', res);
console.log('err', err);
// this.props.dispatchAction.loginCompleted();
});
}
Edit
As a follow up to your comment, iat first i thought you are not implementing the redux-thunk properly as a middle-ware but according to your code it looks ok.
Then I've noticed that you are not really dispatching loginAction but just invoking it.
You should dispatch it dispatch(loginAction()). Your component should be connected to redux then you will have access to dispatch.
Related
I configured redux this way and it works.
This is the _app.js file reconfigured :
import App from 'next/app';
import { Provider } from 'react-redux';
import withRedux from 'next-redux-wrapper';
import store from '../redux/store';
import React from 'react';
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
const appProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};
console.log(appProps);
return {
appProps: appProps
};
}
render() {
const { Component, appProps } = this.props;
return (
<Provider store={store}>
<Component {...appProps} />
</Provider>
);
}
}
const makeStore = () => store;
export default withRedux(makeStore)(MyApp);
This is the index.js file which I've connected to redux :
import { connect } from 'react-redux';
import { callAction } from '../redux/actions/main';
const Index = (props) => {
console.log(props);
return (
<div>
Index js state <button onClick={() => props.callAction()}>Call action</button>
</div>
);
};
const mapStateProps = (state) => ({
name: state.main.name
});
const mapDispatchProps = {
callAction: callAction
};
export default connect(mapStateProps, mapDispatchProps)(Index);
This is the rootReducer file which gets only one reducer named main :
import { main } from './main';
import { combineReducers } from 'redux';
export const rootReducer = combineReducers({
main: main
});
And this is the store.js file :
import { createStore } from 'redux';
import { rootReducer } from './reducers/rootReducer';
const store = createStore(rootReducer);
export default store;
It all works fine but it throws a warning in the console which says :
/!\ You are using legacy implementaion. Please update your code: use createWrapper() and wrapper.withRedux().
What changes to which files I need to make to fix the legacy implementation warning?
I solved the warning by changing the way I get redux states and actions in index.js and the way passing them in _app.js files by using the createWrapper and withRedux :
_app.js
import App from 'next/app';
import store from '../redux/store';
import { Provider } from 'react-redux';
import { createWrapper } from 'next-redux-wrapper';
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
}
const makeStore = () => store;
const wrapper = createWrapper(makeStore);
export default wrapper.withRedux(MyApp);
index.js
import { callAction } from '../redux/action';
import { connect } from 'react-redux';
const Index = (props) => {
return (
<div>
hey {props.name}
<br />
<button onClick={() => props.callAction()}>Call action</button>
</div>
);
};
const mapState = (state) => {
return {
name: state.name
};
};
const mapDis = (dispatch) => {
return {
callAction: () => dispatch(callAction())
};
};
export default connect(mapState, mapDis)(Index);
I would like to leave this answer, it worked for me in TS
================== _app.tsx ==================
import type { AppProps } from 'next/app'
import { Provider } from 'react-redux';
import { createWrapper } from 'next-redux-wrapper';
import { store } from '../redux/store';
function MyApp({ Component, pageProps }: AppProps & { Component: { layout: any }}) {
const Layout = Component.layout || (({ children }) => <>{children}</>);
return (
<Provider store={store}>
<Layout>
<Component {...pageProps} />
</Layout>
</Provider>
);
}
MyApp.getInitialProps = async ({ Component, router, ctx }) => {
const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};
return { pageProps };
}
const makeStore = () => store;
const wrapper = createWrapper(makeStore);
export default wrapper.withRedux(MyApp);
================== store.tsx ==================
import { applyMiddleware, createStore } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import { rootReducer } from '../reducers';
export const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunkMiddleware)),
);
export type AppDispatch = typeof store.dispatch;
================== reducers.tsx ==================
import * as redux from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { Action } from 'typesafe-actions';
import user from './user';
export const rootReducer = redux.combineReducers({
user,
});
export type AppThunkDispatch = ThunkDispatch<RootState, void, Action>;
export type RootState = ReturnType<typeof rootReducer>;
================== onereducer.tsx ==================
import { HYDRATE } from "next-redux-wrapper";
import { Reducer } from 'redux';
import { ActionType } from 'typesafe-actions';
import { USER_ACTIONS } from '../../actions/types';
import { IUserData } from './types';
const userState: IUserData = {
_id: '',
email: '',
password: '',
role: '',
};
const userReducer: Reducer<IUserData, ActionType<any>> = (
state = userState,
action,
) => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload.userData };
case USER_ACTIONS.SET_USER_DATA:
return { ...state, ...action.payload.userData };
default:
return { ...state };
}
};
export default userReducer;
PD: Still a work in progress but works!
I am trying to update the redux store with a list of groupsNames.
The axios call inside the action works and there is a response.
When I dispatch the payload the redux store doesnt get updated with response data.
FileName: actions/index.js
CODE:
export const getGroups = () => async dispatch => {
axios.get(url + "groups")
.then(response => {
console.log(response.data);
dispatch({
type: GET_GROUPS,
payload: response.data
})
},
function(error) {
console.log(error)
}
);
};
Filename: reducers/groupsReducer.js
CODE:
import _ from 'lodash';
import {
GET_GROUPS,
} from '../actions/types';
export default (state = {}, action) => {
console.log(action.payload);
console.log("inside groupsREducer");
switch (action.payload) {
case GET_GROUPS:
console.log(action.payload);
return {...state, ..._.mapKeys(action.payload, 'id')};
default:
return state;
}
}
//mapKeys is a function from lowdadsh that takes an array and returns an object.
FileName: reducers/index.js
CODE:
import { combineReducers } from 'redux';
import authReducer from './authReducer';
import { reducer as formReducer } from 'redux-form';
import postsReducer from './postsReducer';
import groupsReducer from './groupsReducer';
export default combineReducers ({
auth: authReducer,
form: formReducer,
posts: postsReducer,
groups: groupsReducer,
});
FileName: GroupList.js
CODE:
import React from "react";
import { connect } from "react-redux";
import { getGroups } from '../../actions';
class GroupList extends React.Component {
componentDidMount() {
this.props.getGroups();
}
render() {
return <div>
<h1>GroupList</h1>
</div>;
}
}
export default connect(null, { getGroups })(GroupList);
FileName: App.js
CODE:
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose} from 'redux';
import reduxThunk from 'redux-thunk';
import reducers from './reducers';
import Landing from './components/Landing';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducers,
composeEnhancers(applyMiddleware(reduxThunk))
);
ReactDOM.render(
<Provider store={store}>
<Landing />
</Provider>,
document.querySelector('#root')
);```
Ive connected to ReduxDevToolsExtension in chrome and when i check the state in there, it shows empty.
Your are using the wrong argument for your switch-case.
Replace switch (action.payload) with switch (action.type)
I am building a simple app to retrieve some recipes from an API URL.
I am reading the documentation of Thunk to implement it but I cannot understand how to set the async get request.
What is strange is that if I console.log the action passed into the reducer it definitely retrieves the correct object (a list of recipes for shredded chicken).
When I pass the action onto the the reducer, instead, it throws the error:
"Unhandled Rejection (Error): Given action "FETCH_RECIPES", reducer "recipes" returned undefined. To ignore an action, you must explicitly return the previous state. If you want this reducer to hold no value, you can return null instead of undefined."
Is there any error in my action creator? is the API call properly done?
Store
import React from 'react';
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import SearchBar from './components/App';
import thunk from 'redux-thunk';
import 'bootstrap/dist/css/bootstrap.css';
import rootReducer from './reducers';
const store = createStore(rootReducer, applyMiddleware(thunk));
render(
<Provider store={store}>
<SearchBar />
</Provider>,
document.getElementById('root')
)
Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './App.css';
import { Button } from 'reactstrap';
import { Form } from 'reactstrap';
import { bindActionCreators } from 'redux';
import {fetchRecipe } from '../actions';
class SearchBar extends Component {
constructor(props) {
super(props)
this.state = { term: ''};
this.typeRecipe = this.typeRecipe.bind(this)
this.onFormSubmit = this.onFormSubmit.bind(this);
}
onFormSubmit(e) {
e.preventDefault()
this.props.fetchRecipe(this.state.term)
}
typeRecipe(e) {
this.setState({term: e.target.value});
}
render() {
return (
<div className="SearchBar">
<Form onSubmit={this.onFormSubmit}>
<input type='text'
value={this.state.term}
placeholder='ciao'
onChange={this.typeRecipe}
/>
<br/>
<Button id='ciao' className='btn-success'>Submit</Button>
</Form>
</div>
);
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchRecipe }, dispatch);
}
export default connect(null, mapDispatchToProps)(SearchBar);
Action creator
import axios from 'axios';
export const FETCH_RECIPES = 'FETCH_RECIPES';
const API_KEY = 'xxx';//not the real one.
export function fetchRecipe() {
const request = axios.get(`http://food2fork.com/api/search?key=${API_KEY}&q=shredded%20chicken`);
return (dispatch) => {
request.then(({data}) =>{
dispatch({ type: FETCH_RECIPES, payload: data})
})
}
}
reducer
import { FETCH_RECIPES } from '../actions';
export default function (state = {}, action) {
switch(action.type) {
case FETCH_RECIPES:
const newState = action.payload.data;
return newState;
default:
return state
}
}
combineReducer (index)
import recipeReducer from '../reducers/recipes_reducer';
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
recipes: recipeReducer
});
export default rootReducer;
Mistake is in the reducer return statement.
export default function (state = {}, action) {
switch(action.type) {
case FETCH_RECIPES:
const newState = {...state, data : action.payload.data};
return newState;
default:
return state
}
}
Here we are adding data key to the reducer state, to access this you can use do this in you container :
export default connect((state)=>{
var mapStateToProps = {};
if(state.recipes.data) {
mapStateToProps['recipes'] = state.recipes.data
}
return mapStateToProps;
}, mapDispatchToProps)(SearchBar);
and recipes data will be available as this.props.recipes.
I am having an issue with my React-Redux app. I have created Redux action & reducer and connected them to my React component. However, it seems like it never gets to my Redux action & reducer.
I put "debugger" in my action and reducer but those debuggers never get initiated.
Could you please help me fix this issue?
Am I missing something here?
Here the current codes in the following files:
Here is the code for my app.js:
import React, { Component } from 'react';
import { render } from 'react-dom';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import ReduxPromise from 'redux-promise';
import rootReducer from './reducers';
import App from './components/index';
const createStoreWithMiddleware = applyMiddleware(thunk, ReduxPromise)(createStore);
render(
<Provider store={createStoreWithMiddleware(rootReducer)}>
<App />
</Provider>,
document.getElementById('app')
);
Here is my code for index.js:
As you can see below, I put a debugger in the "componentDidMount()", and when In run it, it gets to that point.
import React, { Component } from 'react';
import { render } from 'react-dom';
import { connect } from 'react-redux';
import * as AllNewsApiActions from '../actions/newsApiActions';
import '../../css/style.css';
class App extends Component {
componentDidMount() {
this.props.getNewsApi();
debugger;
}
render() {
return (
<div>
<p>Hello from react</p>
<p>Sample Testing</p>
</div>
);
}
}
const mapStateToProps = (state) => ({
newNews: state.newNews
});
export default connect(mapStateToProps, {
...AllNewsApiActions
})(App);
Here is my action:
import * as types from './newsTypes';
const API_KEY = "6c78608600354f199f3f13ddb0d1e71a";
export const getNewsApi = () => {
debugger;
return (dispatch, getState) => {
dispatch({
type: 'API_REQUEST',
options: {
method: 'GET',
endpoint: `https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=${API_KEY}`,
actionTypes: {
success: types.GET_NEWS_API_SUCCESS,
error: types.GET_NEWS_API_ERROR
}
}
});
}
}
Here is my reducer:
import * as types from '../actions/newsTypes';
const initialState = {
newNews: []
};
const getNewsAPIReducer = (state = initialState, action) => {
switch(action.type) {
case types.GET_NEWS_API_SUCCESS:
debugger;
return { ...state, newNews: action.data };
case types.GET_NEWS_API_ERROR:
debugger;
return { ...state, error: action.data };
default: {
return state;
};
};
};
export default getNewsAPIReducer;
Here is the index.js file in the reducer folder:
import { combineReducers } from 'redux';
import NewsApiReducer from './newsApiReducer.js';
const rootReducer = combineReducers({
newNews: NewsApiReducer
});
export default rootReducer;
I'm trying to follow this tutorial (https://medium.com/#stowball/a-dummys-guide-to-redux-and-thunk-in-react-d8904a7005d3) on React Redux but am getting an empty object when I print out this.props. It seems like mapStateToProps isn't actually setting this.props since react redux's connect isn't being called, but I'm not sure why
This is what the state should be (like in the tutorial), all I did was change the component's name ItemList to Home
Here's what I'm getting (nothing was mapped to the state):
actions/items.js
export function itemsHasErrored(bool) {
return {
type: 'ITEMS_HAS_ERRORED',
hasErrored: bool
};
}
export function itemsIsLoading(bool) {
return {
type: 'ITEMS_IS_LOADING',
isLoading: bool
};
}
export function itemsFetchDataSuccess(items) {
return {
type: 'ITEMS_FETCH_DATA_SUCCESS',
items
};
}
export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsIsLoading(true));
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(itemsIsLoading(false));
return response;
})
.then((response) => response.json())
.then((items) => dispatch(itemsFetchDataSuccess(items)))
.catch(() => dispatch(itemsHasErrored(true)));
};
}
components/Home.js
export class Home extends Component {
componentDidMount() {
console.log(this.props)
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
return (
<ul>
{this.props.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
reducers/index.js
export function itemsHasErrored(state = false, action) {
switch (action.type) {
case 'ITEMS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
}
export function itemsIsLoading(state = false, action) {
switch (action.type) {
case 'ITEMS_IS_LOADING':
return action.isLoading;
default:
return state;
}
}
export function items(state = [], action) {
switch (action.type) {
case 'ITEMS_FETCH_DATA_SUCCESS':
return action.items;
default:
return state;
}
}
store/configureStore.js
import { applyMiddleware, createStore } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk)
)
}
App.js
import React, { Component } from 'react'
import { Provider } from 'react-redux'
import configureStore from './store/configureStore'
import { Home } from './components/Home'
const store = configureStore()
export default class App extends Component {
render () {
return (
<Provider store={store}>
<Home />
</Provider>
)
}
}
index.js (I am using create-react-app boilerplate)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();
The issue is that you're exporting two components... the non-connected (via the named export Home with export class Home...) and connected (via export default). Then, you're importing and rendering the non-connected component:
import { Home } from './components/Home'
Since you want to use the connected component, you should be importing the default export like this:
import Home from './components/Home'
You may want to just export the connected component, unless you have some reason to use the unconnected one.