I'm a bit confused because this method of updating my store has worked for me in the past - Though the setup is a little different.
In its simplest form, my action gets called, but my dispatch seems to be getting neglected
(reduxMulti and thunk middleware are applied to my store which enables async and multiple actions to be dispatched. )
export function modalDocDeleteAction() {
// This logs as expected
console.log("MODAL/ACTIONS.JS // Document Delete Action Fired");
return dispatch => {
// We don't make it here :(
console.log("MODAL/ACTIONS.JS // Document Delete Action Fired In Return");
dispatch([{ type: MODAL_DOC_DELETE }, modalOpenAction()]);
};
}
Some random JS file:
I can successfully fire my action if I dispatch as below, so I don't think it's anything to do with my setup:
import { modalDocDeleteAction } from "../presentation/modal/actions";
import store from "../container/store";
export function someFunction() {
store.dispatch(modalDocDeleteAction());
});
From component: (I've left comments in as I think I may need to use mapStateToProps in the future)
When I try to dispatch from a component is where I seem to be having issues. I'm confused as this method has worked for me previously... But this is the first time I've tried to update my store to modify a component within another component (Hopefully that makes sense)
Imports:
import { connect } from "react-redux";
import { modalDocDeleteAction } from "./modal/actions"; // This is the correct location
Component:
const ProdRow = ({ document, index, edit, view, promoteRevert, remove }) => {
// I've removed a bunch of unnecessary stuff out of here.
return (
<TableRow>
<TableCell colSpan="2">
<ActionsIcon
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
onClick={modalDocDeleteAction} // This fires my console log (above), but never makes it to the actual dispatch
>
<path
id="cross"
d="M21,6.611,19.389,5,13,11.389,6.611,5,5,6.611,11.389,13,5,19.389,6.611,21,13,14.611,19.389,21,21,19.389,14.611,13Z"
transform="translate(-5 -5)"
fill="#3c3c3c"
/>
<title>Delete</title>
</ActionsIcon>
)}
</TableCell>
</TableRow>
);
};
Redux / Export:
// const mapStateToProps = state => ({
// modalTitle: state.modalReducer.modalTitle,
// modalText: state.modalReducer.modalText,
// closeText: state.modalReducer.closeText,
// continueText: state.modalReducer.continueText,
// completeAction: state.modalReducer.completeAction,
// visible: state.modalReducer.visible
// });
const mapDispatchToProps = dispatch => ({
modalDocDeleteAction: () => dispatch(modalDocDeleteAction())
});
export default connect(
// mapStateToProps,
null,
mapDispatchToProps
)(Results);
This is the exact setup I use to modify my modal component within itself - I think because I'm trying to update the store "externally" is where the problems are coming from. Any help would be great as I'm very stuck! Thanks!
Redux bits:
ACTIONS.js Removed excess code
import {
MODAL_OPEN,
MODAL_CLOSE,
MODAL_COMPLETE_USER_ACTION,
MODAL_DOC_DELETE,
MODAL_DOC_PUBLISH,
MODAL_DOC_REVERT,
MODAL_DOC_CREATE_EXIT,
MODAL_DOC_EDIT_EXIT
} from "./actionTypes";
import store from "../../container/store";
export function modalOpenAction() {
console.log("MODAL/ACTIONS.JS // Modal Open Action Fired");
return dispatch => {
dispatch({ type: MODAL_OPEN });
};
}
export function modalDocDeleteAction() {
console.log("MODAL/ACTIONS.JS // Document Delete Action Fired");
console.log(store.getState());
return dispatch => {
console.log(
"MODAL/ACTIONS.JS // Document Delete Action Fired In Return"
);
dispatch([{ type: MODAL_DOC_DELETE }, modalOpenAction()]);
};
}
REDUCER.js Removed excess code
const initialState = {
modalTitle: "Placeholder Title",
modalText: "Placeholder Text",
closeText: "Placeholder Close",
continueText: "Placeholder Continue",
visible: false,
completeAction: false
};
function modalReducer(state = initialState, action) {
switch (action.type) {
case "MODAL_OPEN":
return {
...state,
visible: true,
completeAction: false
};
case "MODAL_DOC_DELETE":
return {
...state,
modalTitle: "Delete Document?",
modalText:
"Deleting a document will permanently remove it from S3",
closeText: "No, Keep Document",
continueText: "Yes, Delete Document"
};
STORE.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reduxMulti from "redux-multi";
import notificationReducer from "../presentation/notification/reducer";
import modalReducer from "../presentation/modal/reducer";
const reducers = combineReducers({
notificationReducer,
modalReducer
});
const store = createStore(reducers, applyMiddleware(thunk, reduxMulti));
console.log("STORE.JS // Default State:");
console.log(store.getState());
export default store;
Working example:
Following the exact same process as above in my Modal component works as expected:
Imports:
import { connect } from "react-redux";
import Button from "../../form/button";
import { modalCloseAction, modalCompleteUserAction } from "./actions";
Component (Styled Components):
function Component({ ...props }) {
const {
modalTitle,
modalText,
closeText,
continueText,
visible,
modalCloseAction,
modalCompleteUserAction
} = props;
console.log("MODAL.JSX // Modal State:");
console.log(props);
return (
<div>
<ModalContainer visible={visible}>
<Overlay visible={visible} />
<Modal visible={visible}>
<ModalTitle>{modalTitle}</ModalTitle>
<ModalText>{modalText}</ModalText>
<ModalButtons>
<ModalButton
buttonStyle="Back"
onClick={modalCloseAction}
>
{closeText}
</ModalButton>
<ModalButton
buttonStyle="Primary"
onClick={modalCompleteUserAction}
>
{continueText}
</ModalButton>
</ModalButtons>
</Modal>
</ModalContainer>
</div>
);
}
Redux/Export
const mapStateToProps = state => ({
modalTitle: state.modalReducer.modalTitle,
modalText: state.modalReducer.modalText,
closeText: state.modalReducer.closeText,
continueText: state.modalReducer.continueText,
completeAction: state.modalReducer.completeAction,
visible: state.modalReducer.visible
});
const mapDispatchToProps = dispatch => ({
modalCloseAction: () => dispatch(modalCloseAction()),
modalCompleteUserAction: () => dispatch(modalCompleteUserAction())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Component);
Dispatch Actions:
export function modalCloseAction() {
return dispatch => {
dispatch({ type: MODAL_CLOSE });
};
}
export function modalCompleteUserAction() {
return dispatch => {
dispatch([{ type: MODAL_COMPLETE_USER_ACTION }, modalCloseAction()]);
};
}
Related
I need a help to solve this error:
"useDispatch is called in function that is neither a React function component nor a custom React Hook function".
Explanation:
store.js and userSlice.js hold the definition of my Redux related things (rtk).
Auth.js is meant to hold functions to authenticate/logout and keep redux "user" storage updated. By now I have just the google auth, that is authenticated when I call redirectToGoogleSSO.
The authentication part is working flawless and i'm retrieving the user info correctly, but I'm having a hard time making it update the user store.
The dispatch(fetchAuthUser()) is where I get the error.
Sidebar.js is a navigation sidebar that will hold a menu to sign in/sign out and to access the profile.js (not implemented yet).
If I bring all the code from Auth to inside my Sidebar component, the authentication work and the redux store is filled, but I would like to keep things in the Auth.js so I can use that in other components and not just in the Sidebar.
//store.js:
import { configureStore } from '#reduxjs/toolkit';
import userReducer from './userSlice';
export default configureStore({
reducer: {
user: userReducer
}
});
//userSlice.js
import { createSlice } from '#reduxjs/toolkit';
import axios from "axios";
export const userSlice = createSlice({
name: 'user',
initialState: {
email: 'teste#123',
name: 'teste name',
picture: 'teste pic',
isAuthenticated: false
},
reducers: {
setUser (state, actions) {
return {...state,
email: actions.payload.email,
name: actions.payload.name,
picture: actions.payload.picture,
isAuthenticated: true
}
},
removeUser (state) {
return {...state, email: '', name: '', picture: '', isAuthenticated: false}
}
}
});
export function fetchAuthUser() {
return async dispatch => {
const response = await axios.get("/api/auth/user", {withCredentials: true}).catch((err) => {
console.log("Not properly authenticated");
dispatch(removeUser());
});
if (response && response.data) {
console.log("User: ", response.data);
dispatch(setUser(response.data));
}
}
};
export const { setUser, removeUser } = userSlice.actions;
export const selectUser = state => state.user;
export default userSlice.reducer;
//Auth.js
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { fetchAuthUser } from '../../redux/userSlice';
export const AuthSuccess = () => {
useEffect(() => {
setTimeout(() => {
window.close();
},1000);
});
return <div>Thanks for loggin in!</div>
}
export const AuthFailure = () => {
useEffect(() => {
setTimeout(() => {
window.close();
},1000);
});
return <div>Failed to log in. Try again later.</div>
}
export const redirectToGoogleSSO = async() => {
const dispatch = useDispatch();
let timer = null;
const googleAuthURL = "http://localhost:5000/api/auth/google";
const newWindow = window.open(
googleAuthURL,
"_blank",
"toolbar=yes,scrollbars=yes,resizable=yes,top=200,left=500,width=400,height=600"
);
if (newWindow) {
timer = setInterval(() => {
if(newWindow.closed) {
console.log("You're authenticated");
dispatch(fetchAuthUser()); //<----- ERROR HERE ---->
if (timer) clearInterval(timer);
}
}, 500);
}
}
//Sidebar.js
import React from 'react';
import { Link } from 'react-router-dom';
import { redirectToGoogleSSO } from '../auth/Auth';
import { useSelector } from 'react-redux';
export const Sidebar = () => {
const handleSignIn = async() => {
redirectToGoogleSSO();
};
const {name,picture, isAuthenticated} = useSelector(state => state.user);
return (
<div id="sidenav" className="sidenav">
<div className="nav-menu">
<ul>
{
isAuthenticated
? <li>
<img className="avatar" alt="" src={picture} height="40" width="40"></img>
<Link to="/" className="user">{name}</Link>
<ul>
<li><Link to="/"><i className="pw-icon-export"/> logout</Link></li>
</ul>
</li>
: <li>
<Link to="/" className="login" onClick={handleSignIn}>
<i className="pw-icon-gplus"/>
Sign In / Sign Up
</Link>
</li>
}
</ul>
</div>
</div>
)
}
You only can use the useDispatch hook from a react component or from a custom hook, in your case, you should use store.dispatch(), try to do the following:
import { configureStore } from '#reduxjs/toolkit';
import userReducer from './userSlice';
// following the docs, they assign configureStore to a const
const store = configureStore({
reducer: {
user: userReducer
}
});
export default store;
Edit: i also noticed that you are trying to dispatch a function that is not an action, redux doesn't work like that, you should only dispatch the actions that you have defined in your reducer, otherwise your state will be inconsistent.
So first of all, move the fetchAuthUser to another file, like apiCalls.ts or anything else, it's just to avoid circular import from the store.js.
after this, call the store.dispatch on the fetchAuthUser:
// File with the fetch function
// Don't forget to change the path
import store from 'path/to/store.js'
export function fetchAuthUser() {
const response = await axios.get("/api/auth/user", {withCredentials: true}).catch((err) => {
console.log("Not properly authenticated");
store.dispatch(removeUser());
});
if (response && response.data) {
console.log("User: ", response.data);
store.dispatch(setUser(response.data));
}
};
In the Auth.js you don't have to call the dispatch, because you have already called it within your function.
export const redirectToGoogleSSO = async() => {
let timer = null;
const googleAuthURL = "http://localhost:5000/api/auth/google";
const newWindow = window.open(
googleAuthURL,
"_blank",
"toolbar=yes,scrollbars=yes,resizable=yes,top=200,left=500,width=400,height=600"
);
if (newWindow) {
timer = setInterval(() => {
if(newWindow.closed) {
console.log("You're authenticated");
// Just call the fetchAuthUser, you are already dispatching the state inside this function
await fetchAuthUser();
if (timer) clearInterval(timer);
}
}, 500);
}
}
So keep in mind that ever you need to use dispatch outside a react component or a custom hook, you must use the store.dispatch, otherwise it will not work, and don't forget to only dispatch actions to keep the state consistent. I suggest you to read the core concepts about redux, and also see this video to understand better how it works under the hoods. Hope i helped a bit!
Just as the error states, you are calling useDispatch in Auth.js-> redirectToGoogleSSO. This is neither a React Component nor a React Hook function. You need to call useDispatch in either of those. So you can:
Handle the redux part of the user information and the Google SSO part in a component by calling both useDispatch and redirectToGoogleSSO in handleSignIn itself (this is probably easier to implement right now, you just need to move the dispatch code from redirectToGoogleSSO to handleSignIn), or
turn redirectToGoogleSSO into a Hook you can call from within components.
I am trying to trying to get my textbox to update the word count when the user types something in the box. But the setWordCount action is not getting passed to the reducer. I am at a loss for why this isn't working.
In troubleshooting, I confirmed that the component is pulling the initial word count off state the way it's supposed to. I also confirmed that setWordCount is getting called when the user types something. This should trigger off the action which passes the updated word count to state, but it's not firing. I am not getting any errors in the console and none of my middleware loggers is firing.
This is my component.
import React from 'react';
import { connect } from 'react-redux';
import { setWordCount } from '../../redux/writing/writing.actions';
import { UpdateWordCount, UpdatePercentage } from '../../redux/writing/writing.utils';
import './writing-text-box.styles.scss';
const WritingTextBox = ({wordCount, goal}) => {
var updatedPercentage = UpdatePercentage(wordCount, goal);
var percent = updatedPercentage + '%';
return (
<div className='writing-box-container'>
<textarea className='writing-box' type='text' onChange={
(e) => {
setWordCount(UpdateWordCount(e.target.value));
}
}
/>
<div id='Progress'>
<div id='Bar' style={{width: percent}}></div>
</div>
<p key='writing-box-word-count' className='wordcount' >
{wordCount} / {goal}</p>
</div>
)}
const mapStateToProps = state => ({
wordCount: state.writing.wordCount,
goal: state.writing.goal,
percentage: state.writing.percentage
});
const mapDispatchToProps = dispatch => ({
setWordCount: ({wordCount}) => dispatch(setWordCount(wordCount)),
// setPercentage: percentage => dispatch(setPercentage(percentage)),
});
export default connect(mapStateToProps, mapDispatchToProps)(WritingTextBox);
This is my actions file, which is nearly copy-pasted from another app that works:
import WritingTypes from "./writing.types";
export const setWordCount = wordCount => ({
type: WritingTypes.SET_WORD_COUNT,
payload: wordCount,
});
and my reducer:
import WritingTypes from "./writing.types";
const INITIAL_STATE = {
wordCount: 0,
goal: 124,
percentage: 0
}
const writingReducer = (currentState = INITIAL_STATE, action) => {
switch(action.type) {
case WritingTypes.SET_WORD_COUNT:
return {
...currentState,
wordCount: action.payload
};
default:
return currentState;
}
}
export default writingReducer;
and my root Reducer:
import { combineReducers } from "redux";
import writingReducer from "./writing/writing.reducer";
const rootReducer = combineReducers ({
writing: writingReducer
})
export default rootReducer;
You need to be a little more careful with the namings. currently, setWordCount is the name of:
the action creator, which simply creates the action object.
export const setWordCount = wordCount => ({
type: WritingTypes.SET_WORD_COUNT,
payload: wordCount,
});
the prop that dispatches the action.
setWordCount: ({wordCount}) => dispatch(setWordCount(wordCount)),
in here it should be the second one:
<textarea className='writing-box' type='text' onChange={
(e) => {
setWordCount(UpdateWordCount(e.target.value));
}
}
/>
to make it work, destructure it from props:
const WritingTextBox = ({wordCount, goal,setWordCount}) => {
now it points to the prop, and works as expected.
I need a little help with this redux problem that I am encountering. Here, I have an APP.js code that called the action from a file called duck.js.
import {
selectBaseCurrency,
selectExchangeCurrency,
selectExhangeRate,
setBaseCurrency, //calling this action
initialState,
} from "./configureStore/duck";
In this code, I have specified the mapDispatchToProp to dispatch the action.
const mapDispatchToProps = (dispatch,ownProps)=> ({
onClick: () => dispatch(setBaseCurrency(ownProps.baseCurrency)),
});
I've also connected it to the connect().
export default connect(
state => ({
exchangeRate: selectExhangeRate(state),
exchangeCurrency: selectExchangeCurrency(state),
baseCurrency: selectBaseCurrency(state)
}), mapDispatchToProps
)(App);
However, for some reason, when I click on the button, the value is not updated accordingly to the input. The button code looks like following:
<button onClick={() => onClick("USD")}>
Change Currency Value
</button>
Have I missed out a code to dispatch this correctly? What could be the problem with it.
Here below, I attach the full duck and also the App.js for more reference.
App.js:
import React, { useEffect, useState } from "react";
import { PropTypes } from "prop-types";
import { connect } from "react-redux";
import {
selectBaseCurrency,
selectExchangeCurrency,
selectExhangeRate,
setBaseCurrency, //calling this action
// setExchangeCurrency,
// setExchangeRate,
initialState,
} from "./configureStore/duck";
const App = ({
exchangeRate,
exchangeCurrency,
baseCurrency,
onClick
}) => {
return (
<div>
<div>
<b>Exchange Rate</b>: {exchangeRate}
</div>
<div>
<b>Exchange Currency</b>: {exchangeCurrency}
</div>
<div>
<b>Base Currency</b>: {baseCurrency}
</div>
<button onClick={() => onClick("USD")}>
Change Currency Value
</button>
</div>
);
};
App.propTypes = {
exchangeRate: PropTypes.number,
exchangeCurrency: PropTypes.string,
baseCurrency: PropTypes.string,
setBaseCurrency: PropTypes.func.isRequired,
// setExchangeCurrency: PropTypes.func.isRequired,
// setExchangeRate: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired
};
App.defaultProps = {
exchangeRate: initialState.exchangeRate,
exchangeCurrency: initialState.exchangeCurrency,
baseCurrency: initialState.baseCurrency
};
const mapDispatchToProps = (dispatch,ownProps)=> ({
onClick: () => dispatch(setBaseCurrency(ownProps.baseCurrency)),
// on: setExchangeCurrency,
// setExchangeRate: setExchangeRate
});
export default connect(
state => ({
exchangeRate: selectExhangeRate(state),
exchangeCurrency: selectExchangeCurrency(state),
baseCurrency: selectBaseCurrency(state)
}), mapDispatchToProps
)(App);
duck.js
import { defineAction } from "redux-define";
import { createAction, handleActions } from "redux-actions";
export const initialState = {
exchangeRate: 3.06,
baseCurrency: "SGD",
exchangeCurrency: "MYR"
};
//Action-types
export const SET_EXCHANGE_RATE = defineAction("SET_EXCHANGE_RATE");
export const SET_BASE_CURRENCY = defineAction("SET_BASE_CURRENCY");
export const SET_EXCHANGE_CURRENCY = defineAction("SET_EXCHANGE_CURRENCY");
//Action-creators
export const setExchangeRate = createAction(
SET_EXCHANGE_RATE,
params => params
);
export const setExchangeCurrency = createAction(
SET_EXCHANGE_CURRENCY,
params => params
);
export const setBaseCurrency = createAction(
SET_BASE_CURRENCY,
params => params
);
//reducer
const reducer = handleActions(
{
[setExchangeRate]: (state, { exchangeRate }) => ({
...state,
exchangeRate
}),
[setExchangeCurrency]: (state, { exchangeCurrency }) => ({
...state,
exchangeCurrency
}),
[setBaseCurrency]: (state, { baseCurrency }) => ({
...state,
baseCurrency
})
},
initialState
);
export default reducer;
//Selector
export const selectExhangeRate = state => state.exchangeRate;
export const selectExchangeCurrency = state => state.exchangeCurrency;
export const selectBaseCurrency = state => state.baseCurrency;
Edit : As additional info, here is my sandbox link: https://codesandbox.io/s/todoapp-with-redux-and-normalized-store-jkp8z
and here is my github link:
https://github.com/sc90/test-sandbox
So there are at least two issues here, I'll try to explain them one by one, I'm not sure how these frameworks you're using interact but here are a few points that will at least fix your issue.
Your reducer is trying to extract { baseCurrency } but this is not a property of your action. You instead need to extract the payload here, like this: { payload }, this payload value will contain your baseCurrency, and to properly save it in the reducer you should return { ...state, baseCurrency: payload }
Your selectors are trying to read directly from the state variable, but this one contains your reducers under the keys you sent to combineReducers, in your case you called your reducer reducer, thus you need to select state like this state => state.reducer.baseCurrency
See my fork of your Sandbox where I've fixed the baseCurrency case for you:
https://codesandbox.io/s/todoapp-with-redux-and-normalized-store-ih79q
I am trying to learn the react and for that I am trying to create a sample todo app. I have a python flask backend which servers as REST server and react as web server.
Everything works find an I am able to show todos and delete particular todo as well. However now I have started learning Redux, and that seems really confusing.
I am not sure how to make call to my rest server. Following just returns promise, not sure how to get the data, rather than promise.
store.js
import axios from 'axios'
import { createStore } from 'redux'
export const ADD_TODO = 'ADD_TODO'
let nextTodoId = 0
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})
export const listTodo = todos => ({
type: 'LIST_TODO',
todos
})
const add_todo = (id, text) => {
return axios.post("http://localhost:5001/todos", {id:id, data:text})
.then(Response=>{
store.dispatch(addTodo(Response.data));
})
}
const fetch_data = () => {
return axios.get("http://localhost:5001/todos")
.then(Response=>{
store.dispatch(listTodo(Response.data))
})
}
const initialState ={
todos: {},
new_todo: ''
}
function todoApp(state = initialState, action) {
console.log("reducer called...")
switch (action.type) {
case ADD_TODO:
return Object.assign({}, state, {
new_todo: action.text
})
default:
return state
}
}
const store = createStore(todoApp)
export default store
app.js
import React, {Component} from 'react'
import {connect} from 'react-redux'
class App extends Component{
render(){
return(
<div>
<button onClick={this.props.addTodo('testing')}>fetch_Data</button>
</div>
);
}
}
export default connect() (App)
index.js
ReactDOM.render(<Provider store={store}> <App /> </Provider>,
document.getElementById('root'));
Firstly, you should export the actions you have created which will then be imported and used in the components using the connect HOC.
You can dispatch the 'fetch_data' action to get the data in your component. Also, you can dispatch 'addTodo' action to add new todo in the list.
export const ADD_TODO = 'ADD_TODO';
export const GET_TODO = 'GET_TODO';
export const fetch_data = () => {
return (dispatch) => axios.get("http://localhost:5001/todos")
.then(response => {
dispatch({type: GET_TODO, todos: response.data});
})
}
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text: text
});
Use the actions constants like ADD_TODO, GET_TODO to save or to update the redux state in reducers
const todoApp = (state = initialState, action) => {
console.log("reducer called...")
switch (action.type) {
case ADD_TODO:
const todos = {...state.todos};
todos[action.id] = action.text;
return Object.assign({}, state, {
todos: todos
});
case GET_TODO:
return Object.assign({}, state, {
todos: action.todos
});
default:
return state
}
}
Importing the actions and then call the function you have added in the 'mapDispatchToProps' to dispatch the actions.
import React, {Component} from 'react'
import {connect} from 'react-redux';
import { addTodo, fetch_data } from "../store";
class App extends Component{
render(){
return(
<div>
<button onClick={this.props.addTodo(todoId, 'testing')}>fetch_Data</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
todos: state.todoApp.todos
});
const mapDispatchToProps = (dispatch) => ({
addTodo: (id, text) => dispatch(addTodo(id, text)),
fetch_data: () => dispatch(fetch_data())
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
redux is based on actions and reducers, basically reducers are pure functions which means no side effects as for example api calls, I'd advice you read more about redux and how to use redux with redux-chunk for making api calls
You make this work like this. You need to dispatch action when you have response.
const fetch_data = () => {
return axios.get("http://localhost:5001/todos")
.then(Response=>{
store.dispatch(addTodo(Response.data));
})
}
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text: text
})
I'm working on setting up a user login screen in React Native using Recompose, with separate actions and reducer files, but my reducer is never being called. Currently, there is just a login button that triggers a doUserLogin() recompose handler:
loginScreen.js
import React from 'react';
import { Button, Text, View } from 'react-native';
import { connect } from 'react-redux';
import { withHandlers, compose } from 'recompose';
import { loginUser } from './user/userActions';
const LoginScreen = ({ user, doUserLogin }) => {
return (
<View style={styles.loginContainer}>
{user ? <Text>Hi, {user.name}!</Text> : <Text>NOT Logged in!</Text>}
<Button title="Log In" onPress={doUserLogin} />
</View>
);
};
export default compose(
connect((state, props) => ({
...state.user,
})),
withHandlers({
doUserLogin: props =>
(event) => {
console.log('LOGIN USER IN HANDLER'); // <-- THIS IS WORKING
loginUser();
},
}),
)(LoginScreen);
The doUserLogin() handler in turn calls loginUser() in my actions file:
userActions.js:
import { LOGIN_REQUEST } from './actionTypes';
export const loginUser = () => {
return (dispatch) => {
console.log('In action'); // <-- THIS IS WORKING
dispatch({ type: LOGIN_REQUEST });
};
};
So far, so good. However, when I dispatch(), my reducer is never called. But the reducer is picking up other actions (from navigation, etc.) - it simply isn't receiving the action from loginUser() above:
userReducer.js:
import { LOGIN_REQUEST } from './actionTypes';
const userReducer = (state = initialState, action) => {
console.log('In reducer'); <-- ** THIS IS NEVER CALLED **
switch (action.type) {
case LOGIN_REQUEST:
return Object.assign({}, state, {
isFetching: true,
});
case LOGOUT:
return initialState;
default:
return state;
}
};
export default userReducer;
Any suggestions would be greatly appreciated.
Ok, looks like I was able to figure this out. In a nutshell, in loginScreen.js I needed to add mapStateToProps and mapDispatchToProps functions, which are passed to connect. withHandlers can then dispatch the loginUser() function in my actions file as a prop.
updated loginScreen.js
import React from 'react';
import { Button, Text, View } from 'react-native';
import { connect } from 'react-redux';
import { withHandlers, compose } from 'recompose';
import { loginUser } from './user/userActions';
const LoginScreen = ({ user, doUserLogin }) => {
return (
<View style={styles.loginContainer}>
{user ? <Text>Hi, {user.name}!</Text> : <Text>NOT Logged in!</Text>}
<Button title="Log In" onPress={doUserLogin} />
</View>
);
};
const mapStateToProps = state => ({
...state.user,
});
const mapDispatchToProps = dispatch => ({
loginUser: () => {
dispatch(loginUser());
},
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withHandlers({
doUserLogin: props =>
() => {
console.log('LOGIN USER IN HANDLER');
props.loginUser();
},
}),
)(LoginScreen);
Any additional advice/suggestions would still be appreciated.
Actually, for this particular case, you can dismiss completely withHandlers helper.
You only need to pass the action creator to the react-redux connect function, in order to bind it to the dispatch function, just as you shown. Even more, check connect docs. You can access the props of the component, in the 3rd parameter of connect, and further create handlers that depend on props.
In your case it could be something like this
const mergeProps = (stateProps, dispatchProps, ownProps) => {
return Object.assign({}, ownProps, stateProps, dispatchProps, {
doUserLogin: () => {
console.log('LOGIN USER IN HANDLER');
console.log('accessing a prop from the component', ownProps.user);
dispatchProps.loginUser();
}
});
}
export default connect(mapStateToProps,
mapDispatchToProps,
mergeProps)(LoginScreen);
Notice how we can create new functions, that will be available as a new prop to the component, in a similar way to withHandler helper