React + Redux subscribe to action outside of mapStateToProps - reactjs

I have the mapStateToProps workflow down, but what if I want to respond to actions in a way that doesn't fit well into the state => props paradigm? For instance:
this.props.dispatch(saveArticle(...))
// on successful save, redirect to article page
If I'm just using regular old XHRs rather than actions, it would look something like this:
saveArticle(...).then(article => this.router.push(`/articles/${article.id}`))
It's not clear how this would fit in with the standard React/Redux workflow; I've seen people suggest that the saveArticle() action creator could fire off the router change, but I want to keep those separate; I should be able to save an article without being forced to redirect.
A workaround could be to do it in mapStateToProps; have the action set a flag or something, like articleWasSaved, and have the component that does the saving look for that prop and redirect if it sees it, but that seems really ugly, especially if multiple things are looking for that update, since it would likely require the component(s) to clear the flag.
Is there a simple/standard solution I'm missing?

Redux-thunk allows you to dispatch functions as actions. It is ideally to dispatch async operations.
Here I've created an example I think It will be useful for you:
actions.js
export const tryingAsyncAction = () => {
return {
type: 'TRYING_ASYNC_ACTION'
}
}
export const actionCompleted = () => {
return {
type: 'ACTION_COMPLETED'
}
}
export const errorAsyncAction = (error) => {
return {
type: 'ERROR_ASYNC_ACTION',
error
}
}
export function someAsynAction() {
return dispatch => {
dispatch(tryingAsyncAction())
ApiService.callToAsyncApi(...)
.then(() => {
dispatch(actionCompleted())
}, (cause) => {
dispatch(errorAsyncAction(cause))
})
}
}
reducer.js
const initialState = {
tryingAction: false,
actionCompleted: false,
error: null,
shouldRedirect: false,
redirectUrl: null
}
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'TRYING_ASYNC_ACTION':
return Object.assign({}, state, {
tryingAction: true
})
case 'ACTION_COMPLETED':
return Object.assign({}, state, {
tryingAction: false,
actionCompleted: true,
shouldRedirect: true,
redirectUrl: 'someUrl'
})
case 'ERROR_ASYNC_ACTION':
return Object.assign({}, state, {
tryingAction: false,
actionCompleted: false,
error: action.error
})
default:
return state
}
}
Your createStore file
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk' //npm install --save redux-thunk
//Other imports...
const store = createStore(
reducer,
applyMiddleware(
thunkMiddleware
)
)
YourComponent.js
componentWillReceiveProps(nextProps){
if(nextProps.shouldRedirect && nextProps.redirectUrl)
this.router.push(`/articles/${article.id}`)
}
Let me know if there is something you dont understand. I will try to clarify

You could make use of react-thunk in this case.
actions/index.js
export function saveArticle(data) {
return (dispatch, getState) => (
api.post(data).then(response => {
dispatch({ type: 'SAVE_ARTICLE', payload: response })
return response;
})
)
}
reducer/index.js
import { combineReducers } from 'redux';
const initialState = {
list: [],
current: null,
shouldRedirect: false,
redirectTo: null
};
export function articles(state = initialState, action) {
switch(action.type) {
case 'SAVE_ARTICLE':
return {
shouldRedirect: true,
redirectTo: '/some/url',
current: action.payload,
list: [...state.list, action.payload]
};
default: return state;
}
}
export default combineReducers({ articles });
store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux#>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
component/index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as Actions from 'actions/index';
class MyComponent extends Component {
_handleSubmit = () => {
// get form values somehow...
// const values = getFormValues();
this.props.saveArticle(values).then(response => {
// you can handle you redirect here as well,
// since saveArticle is returning a promise
});
};
componentWillReceiveProps(nextProps) {
// you can handle the redirection here listening to changes
// on shouldRedirect and redirectTo that will be triggered
// when the action 'SAVE_ARTICLE' is dispatched
if(nextProps.shouldRedirect && nextProps.redirectTo) {
this.routes.push(nextProps.redirectTo);
}
}
render() {
// just an example
return (
<form onSubmit={this._handleSubmit}>
{ /* ... other elements here */ }
</form>
)
}
}
export default connect(
state => ({
articles: state.articles.list,
article: state.articles.current,
redirectTo: state.articles.redirectTo,
shouldRedirect: state.articles.shouldRedirect
}),
Actions
)(MyComponent);
PS: I'm using some babel syntax sugar here, so make sure you're the following presets are set in your .babelrc.
es2015
stage-2
stage-0
react

Related

Issue with react redux and reducer

I'm new in react-redux, and I have a problem with communication between reducer and store.
This is the idea on which I base:
I have a component "Login", that contains a button and two text inputs, and when i click that, I send the action to the reducer. Then, I update the state, and send it to the UI again (thats the way i understand the logic, correct me if i'm wrong). The problem occurs in the reducer, it never enter there, but yes in the action file (tested with console.logs). Maybe the connect is not working? or is in the store part?
Here I detach how I did it
action.js, with two operations only
const logout = () => {
return {
type: "USER_LOGOUT",
payload: false,
};
};
const login = () => {
return {
type: "USER_LOGIN",
payload: true,
};
};
export { logout, login };
reducer.js implementation, only change one boolean value
const initialState = {
logged: false,
};
export default (state = initialState, action) => {
if (action.type === "USER_LOGOUT") {
return {
...state,
logged: false,
};
}
if (action.type === "USER_LOGIN") {
return {
...state,
logged: true,
};
}
return state;
};
index.js (store), here's how i declare the store part
import { createStore, combineReducers } from "redux";
import loginReducer from "./reducer";
const reducers = combineReducers({
loginReducer,
});
const store = createStore(reducers);
export default store;
Login.js, only the touchable part
import { logout, login } from "../redux/actions";
import { connect } from "react-redux";
...
connect(null, { logout, login }, Login);
...
<TouchableOpacity>
...
onPress={() => checkValidation()}
...
</TouchableOpacity>
Here checkValidation, who calls the action "login"
checkValidation() =>
...
login();
...
You are not dispatching the action. To make Redux aware of an action you must dispatch it.
If you are using a class component you need to connect the component and pass it the dispatch action from redux.
I suggest you to use the hooks because its way easier.
1-Import the useDispatch hook
import { useDispatch } from "react-redux";
2-Create the dispatch:
const dispatch = useDispatch();
3-Dispatch your action:
checkValidation() =>
...
// Since your function login already returns the action object:
dispatch(login());
...

React Native Connect Redux Action & Reducer

I have been trying to connect my Redux Action and Reducer to my component. But it doesn't seem to work properly.
Currently, when I call my Action, it does get to that Action but it does not move onto my reducer. I think I am missing something here but having a hard time finding out what is the issue.
Could anyone please help me with this issue?
Thank you.
Here is my Action:
export const getItem = () => {
return (dispatch, getState) => {
debugger;
dispatch({
type: 'API_REQUEST',
options: {
method: 'GET',
endpoint: `18.222.137.195:3000/v1/item?offset=0`,
actionTypes: {
success: types.GET_ITEM_SUCCESS,
loading: types.GET_ITEM_LOADING,
error: types.GET_ITEM_SUCCESS
}
}
});
};
};
Here is my Reducer:
export const initialState = {
getItem: {}
};
const registerItemReducer = (state = initialState, action) => {
switch (action.type) {
case types.GET_ITEM_LOADING:
debugger;
return { ...state, loading: true, data: null };
case types.GET_ITEM_SUCCESS:
debugger;
return { ...state, loading: false, getItem: action.data};
case types.GET_ITEM_ERROR:
debugger;
return { ...state, loading: false, error: action.data};
default: {
return state;
}
}
}
export default registerItemReducer;
Here is my store:
/* global window */
import { createStore, applyMiddleware, compose } from 'redux';
import { persistStore, persistCombineReducers } from 'redux-persist';
import storage from 'redux-persist/es/storage'; // default:
localStorage if web, AsyncStorage if react-native
import thunk from 'redux-thunk';
import reducers from '../reducers';
// Redux Persist config
const config = {
key: 'root',
storage,
blacklist: ['status'],
};
const reducer = persistCombineReducers(config, reducers);
const middleware = [thunk];
const configureStore = () => {
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
compose(applyMiddleware(...middleware)),
);
const persistor = persistStore(
store,
null,
() => { store.getState(); },
);
return { persistor, store };
};
export default configureStore;
Lastly here is my component that has "connect" part & componentDidMount:
componentDidMount() {
this.props.getItem();
}
const mapStateToProps = state => ({
registerItem: state.registerItem || {},
});
const mapDispatchToProps = {
getItem: getItem
};
export default connect(mapStateToProps, mapDispatchToProps)(RegisterItemComponent);
Is registerItem name of your reducer? Your reducer has two state getItem and loading. But in the below code you are calling state.registerItem. Looks like there is some mismatch between the actual state and the mapped state.
In the code below, try to print the state value, it will help you to navigate to the exact parameter you are looking for.
Add the below line in your existing code to debug:
const mapStateToProps = state => ({
console.log("State of reducer" + JSON.stringify(state));
registerItem: state.registerItem || {},
});

Get data from Redux thunk

I've just started implementing Redux in a React application, and it's the first time i try to, so please bear with me.
My problem is that i can't access the data in my component this this.props.questions
I have a simple action which is supposed to async fetch some data
export function fetchQuestions(url) {
const request = axios.get('url');
return (dispatch) => {
request.then(({data}) => {
dispatch({ type: 'FETCH_QUESTIONS', payload: data });
console.log(data);
});
};
}
Which is picked up my reducer questions_reducer
export default function(state = [], action) {
switch(action.type) {
case 'FETCH_QUESTIONS':
console.log('Getting here');
return state.concat([action.payload.data]);
console.log('But not here');
}
return state;
}
My index reducer looks like this:
import { combineReducers } from 'redux';
import fetchQuestions from './question_reducer';
const rootReducer = combineReducers({
questions: fetchQuestions
});
export default rootReducer;
I pass it to my store where i apply the thunk middleware and finally into <Provider store={store}> which wraps my app, but the prop just returns undefined in my React component
configureStore:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk)
);
}
I don't know if the console.log is to be trusted but it logs from my questions_reducer before the data is returned from the dispatch in my action
EDIT (Component)
class QuestionsRoute extends Component {
constructor(props) {
super(props);
this.state = {
};
}
componentDidMount() {
this.props.fetch('someUrl);
setTimeout(function(){ console.log(this.props.questions) },
1500);
}
render() {
{console.log(this.props.questions)}
return (
<div>
<1>Hello</1>
{this.props.questions !== undefined ?
<p>We like props</p>: <p>or not</p>
}
</div>
);
}
};
const mapStateToProps = (state) => {
return {
questions: state.questions,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetch: () => dispatch(fetchQuestions())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(QuestionsRoute);
In your reducer
export default function(state = [], action) {
switch(action.type) {
case 'FETCH_QUESTIONS':
return state.concat([action.payload.data]);
}
return state;
}
You should probably instead have return state.concat([action.payload]);
Since from dispatch({ type: 'FETCH_QUESTIONS', payload: data }); we see that payload is data, it doesn't contain it.
Update: I'd recommend setting up redux-devtools / redux-devtools-extension / react-native-debugger so you can visually see your actions and store state live - makes things like this a lot easier to debug!

This.props.action is not a function

Just started writing custom stuff with Redux and I'd like to pass an input into my action.
(Also using this tutorial: https://www.youtube.com/watch?v=kNkTQtRUH-M)
Basically, I'd like to pass a phone number (in the example: '+1**********') to my action using this.props.testUserPhone('+1**********'), but I get a this.props.testUserPhone is not a function error.
What am I doing wrong here? I feel like there's a specific way to do functions with parameters that I'm missing, or my binding is wrong or something.
phonepage.js
#connect(
state => ({
testUserPhone: state.phonepage.testUserPhone
}),
{ testUserPhone }
)
class PhonePage extends Component {
componentDidMount() {
this.props.testUserPhone('+1**********')
}
render() {
console.log(this.props.testUserPhone('+1**********'))
return(
// Render stuff
)
}
}
actions.js
import { UserAPI } from '../../constants/api'
const userAPI = new UserAPI()
export const TEST_USER_PHONE = 'TEST_USER_PHONE'
export const testUserPhone = (user) => ({
type: TEST_USER_PHONE,
payload: userAPI.testPhone(user)
})
reducer.js
import {
TEST_USER_PHONE
} from './actions'
const INITIAL_STATE = {
testedByPhone: {
data: [],
isFetched: false,
error: {
on: false,
message: null
}
}
}
export default (state = INITIAL_STATE, action) => {
switch(action.type) {
case '${TEST_USER_PHONE}_PENDING':
return INITIAL_STATE
case '${TEST_USER_PHONE}_FULFILLED':
return {
testedByPhone: {
data: action.payload,
isFetched: true,
error: {
on: false,
message: null
}
}
}
case '${TEST_USER_PHONE}_REJECTED':
return {
testedByPhone: {
data: [],
isFetched: true,
error: {
on: true,
message: 'Error when fetching testedByPhone'
}
}
}
default:
return state
}
}
reducers.js
import { combineReducers } from 'redux'
import {
phonereducer
} from '../scenes'
export default combineReducers({
phonepage: phonereducer
})
I wasn't paying attention and missed that you need babel transform legacy decorators to actually use this.
npm install --save-dev babel-plugin-transform-decorators-legacy
and then
{
"presets": ["react-native"],
"plugins": ["transform-decorators-legacy"]
}
Your code has a binding issue: the action named 'testUserPhone' which you have created in action.js is not imported in your component where you are trying to call it.
import {testUserPhone} from 'action.js'//plz correct the path according to your file structure
#connect(
state => ({
testUserPhone: state.phonepage.testUserPhone
}),(dispatch)=>{
return {testUserPhone:(user)=>{
dispatch(testUserPhone(user))
}
}
)
class PhonePage extends Component {
componentDidMount() {
this.props.testUserPhone('+1**********')
}
render() {
console.log(this.props.testUserPhone('+1**********'))
return(
// Render stuff
)
}
}
The above code is just one way to dispatch an action from a container, there are more ways to do the same.
#connect take to parameters mapStateToProps and mapDispatchToProps. Both are function ,in mapStateToProps you specify which part of global state you need in your component and in mapDispatchToProps you specify
which action you want to pass as a prop to your component ,So that you can change the global state by dispatching appropriate actions.

React Redux how to dispatch async actions and update state

I try to deal with ajax data in my learning react,redux project and I have no idea how to dispatch an action and set the state inside a component
here is my component
import React, {PropTypes, Component} from 'react';
import Upload from './Upload';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as profileActions from '../../../actions/profileActions';
class Profile extends Component {
static propTypes = {
//getProfile: PropTypes.func.isRequired,
//profile: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
profile:{
username: '',
password: ''
}
}
this.onUpdate = this.onUpdate.bind(this)
}
onUpdate(event) {
alert()
}
componentWillMount() {
}
componentDidMount() {
}
render() {
const {profile} = this.props;
return (
<div>
</div>
);
}
}
function mapStateToProps(state) {
console.log(state)
return {
profile: state.default.profile,
};
}
function mapDispatchToProps(dispatch, ownProps) {
return {
actions: bindActionCreators(profileActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
I create the store as it follows
import { createStore, combineReducers, applyMiddleware } from 'redux'
import createLogger from 'redux-logger'
import thunk from 'redux-thunk'
import { routerReducer, routerMiddleware, push } from 'react-router-redux'
import reducers from '../reducers'
import { browserHistory } from 'react-router';
const middleware = [ thunk ];
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger());
}
middleware.push(routerMiddleware(browserHistory));
// Add the reducer to your store on the `routing` key
const store = createStore(
combineReducers({
reducers,
routing: routerReducer
}),
applyMiddleware(...middleware),
)
export default store;
reducer
export const RESOLVED_GET_PROFILE = 'RESOLVED_GET_PROFILE'
const profileReducer = (state = {}, action) => {
switch (action.type) {
case 'RESOLVED_GET_PROFILE':
return action.data;
default:
return state;
}
};
export default profileReducer;
actions
import * as types from './actionTypes';
import Api from '../middleware/Api';
export function getProfile() {
return dispatch => {
dispatch(setLoadingProfileState()); // Show a loading spinner
Api.getAll('profile').then(profile => {
dispatch(doneFetchingProfile(profile));
}).catch(error => {
throw(error);
});
/*Api.fetch(`profile`, (response) => {
console.log(response)
dispatch(doneFetchingBook()); // Hide loading spinner
if(response.status == 200){
dispatch(setProfile(response.json)); // Use a normal function to set the received state
}else {
dispatch(error)
}
}) */
}
}
function setProfile(data) {
return {type: types.SET_PROFILE, data: data}
//return { type: types.SET_PROFILE, data: data };
}
function setLoadingProfileState() {
return {type: types.SHOW_SPINNER}
}
function doneFetchingProfile(data) {
console.log(data)
return {
type: types.HIDE_SPINNER,
profile: data
}
}
function error() {
return {type: types.SHOW_ERROR}
}
but I have no idea how would I dispatch action and update the state after getProfile action
You need to only dispatch your event RESOLVED_GET_PROFILE right after dispatching doneFetchingProfile, or simply listen RESOLVED_GET_PROFILE and hide spinner on reducing it.
Api.getAll('profile').then(profile => {
dispatch(doneFetchingProfile(profile));
dispatch(resoloveGetProfile(profile));
})
Actually you r doing everything right - so I didn't understand what is your question is, so if you meant something else - let me know, I`ll try to describe you.
About dispatch(resoloveGetProfile(profile));
There you dispatch action, which will update your state, simple as you do with some static state, I saw that you already have setProfile action, so you can change that line, to call your existed function.
dispatch(setProfile(profile))
Than you need to reduce your state in this action
case 'SET_PROFILE' : (action, state) => {...state, profile: action.data}
Than your state will change and your components will update. Note that your 'get profile method' better to call from componentDidMount to avoid freezing at rendering because of performing web request.

Resources