I'm using React.js with Redux, React-Router and React-Router-Redux.
I would like to to manually reroute (or call an action) when the store reaches a specific state. How do I listen if the store has a specific state? Where do I reroute or call the action?
If these calls are made async then using an async action creator and the promise would handle this quite well.
// Action creator
function doTheStuff(someData, router) {
return dispatch => {
dispatch(somePendingAction());
return myRequestLib.post('/the-endpoint/', someData)
.then((response) => {
if (response.isTransactionDone) {
dispatch(transactionDoneAction(response));
router.transitionTo('/theUrl');
}
});
}
}
I wouldn't put this into the store since I believe the stores only job is to contain/handle the data.
To initiate a page change when a series of other actions have completed and been persisted to the store, dispatch the following redux-thunk. The key here is that you have a chain of thunks which return promises, and then once they all complete, you can update the router with another action.
In a component:
import React from 'react';
import { push } from 'react-router-redux';
import { connect } from 'react-redux';
import { doStuff, doMoreStuff } from './actions';
class SomeComponent extends React.Component {
...
onClick = e => {
const { doStuff, doMoreStuff, push } = this.props; // these are already wrapped by dispatch because we use connect
...
doStuff()
.then(r => doMoreStuff())
.then(r => push('/newPath')) // if actions succeeded, change route
.catch(e => console.warn('doStuff() or doMoreStuff failed'));
...
}
...
}
export default connect(null, { doStuff, doMoreStuff, push })(SomeComponent);
some actions in ./actions.js:
export function doStuff() {
return (dispatch, getState) => {
return API.getSomeInfo()
.then(info => dispatch({ type: 'GET_SOME_INFO', payload: info }))
}
}
export function doMoreStuff() {
return (dispatch, getState) => {
return API.getSomeMoreInfo()
.then(info => dispatch({ type: 'GET_SOME_MORE_INFO', payload: info }));
}
}
Alternatively, you could make a single promise that looks like this, where the redirect happens:
export function doAllStuff() {
return dispatch => {
return Promise.all(doStuff(), doAllStuff())
.then(results => dispatch(push('/newPath')))
.catch(e => console.warn('Not all actions were successful'))
}
}
the component code that intiates it would just be
onClick = e => {
...
doAllStuff(); // Since we caught the promise in the thunk, this will always resolve
...
}
Related
I'm using redux with redux-observable and get this strange error:
Actions must be plain objects. Use custom middleware for async >actions.
/* Component.jsx */
import React from "react"
import { serialNumberCheck } from '../actions'
const Component = props => {
...
<button
onClick={() => props.serialNumberCheck('123456789123456')}
>
Check
</button>
...
}
const mapDispatchToProps = dispatch =>
bindActionCreators({serialNumberCheck}, dispatch)
export default compose(
reduxForm({
...
}),
withStyles(styles),
connect(mapDispatchToProps)
)(Component)
/* actions.js */
export const SERIAL_NUMBER_CHECK = 'SERIAL_NUMBER_CHECK'
export const SERIAL_NUMBER_CHECK_SUCCESS = 'SERIAL_NUMBER_CHECK_SUCCESS'
export const serialNumberCheck = (serialNumber) => ({
type: SERIAL_NUMBER_CHECK,
payload: serialNumber
})
export const serialNumberCheckSuccess = (data) => ({
type: SERIAL_NUMBER_CHECK,
payload: data
})
/* epics.js */
...
import { serialNumberCheck } from "../actions"
import ... from 'rxjs'
...
function serialNumberCheckEpic(action$) {
return action$
.ofType(SERIAL_NUMBER_CHECK)
.switchMap((data) => {
return ajax.getJSON(`http://localhost:3004/sn/?sn=${data.payload}`)
.map((data) => data)
})
.map(data => {
if(data.length !== 0) {
serialNumberCheckSuccess({success: true});
}
})
}
...
export const rootEpic = combineEpics(
...
serialNumberCheckEpic
);
/* reducer.js */
import {
SERIAL_NUMBER_CHECK_SUCCESS,
} from '../actions'
...
export default function epicReducer(state = initialState, action) {
switch (action.type) {
case SERIAL_NUMBER_CHECK_SUCCESS:
return {
...state,
success: action.payload
}
}
}
/* JSON-SERVER RESPONSE */
[
{
"id": 1,
"sn": "123456789123456"
}
]
Inside component i'am calling function serialNumberCheck() and passing inside sierial number that we need to check.
Inside Epic im passing serial number to json-server that checks if this number exists in my "database". If serial number exists, server response is .json containing some parameters.
So if response isn't empty we need to write success: true inside redux store.
But in the end we get successfull GET request, and then error: Actions must be plain objects. Use custom middleware for async actions., but no changes inside redux-store and nothing from SERIAL_NUMBER_CHECK_SUCCESS action.
Finally, I found the solution. I've just missed the return before calling action inside my epic.
function serialNumberCheckEpic(action$) {
return action$
.ofType(SERIAL_NUMBER_CHECK)
.switchMap((data) => {
return ajax.getJSON(`http://localhost:3004/sn/?sn=${data.payload}`)
.map((data) => data)
})
.map(data => {
if(data.length !== 0) {
+ return serialNumberCheckSuccess({success: true});
}
})
}
I'm looking at trying to chain actions together. In my current issue, when the SET_CURRENT_USER action occurs, I'd like it to modify the state (set the current user) and then fire off a bunch of other side-effect tasks (go fetch data, rebuild the UI, etc). My first thought was "well, I'll set a listener on the store"... which resulted in this previous question: How to access 'store' in react redux? (or how to chain actions) There, I was basically that setting listeners is an anti-pattern.
The solution suggested was to 'dispatch' multiple actions chained together. I didn't follow how to do that (my "mapDispatchToProps" is based on the redux tutorials and looks nothing like the suggested mapDispatchToProps) so I did some additional googling about how to chain side-effect actions together and got to this page: https://goshakkk.name/redux-side-effect-approaches/
Trying the first example, I went to my action creator file, which looks like this:
(actionCreators.js)
export function setCurrentUser(username) {
return { type: SET_CURRENT_USER, payload: username }
}
export function actionTwo(username) {
return { type: ACTION_TWO, payload: username }
}
export function actionThree(username) {
return { type: ACTION_THREE, payload: username }
}
and I tried to change the 'setCurrentUser' action creator to something resembling what was in the demo but without the async parts for simplicity - just looking to see if the actions fire:
export function setCurrentUser(username) {
return dispatch => {
dispatch( { type: SET_CURRENT_USER, payload: username } );
dispatch( { type: ACTION_TWO, payload: username } );
dispatch( { type: ACTION_THREE, payload: username } );
}
}
In my app, my mapDispatchToProps looks like this:
const mapDispatchToProps = {
setCurrentUser: setCurrentUser,
}
and I call this in the switchUser handler, like this: this.props.setCurrentUser(this.state.newUsername);
... and I get an error saying that actions must be plain objects.
How can I chain actions together?
Perhaps a deeper issue is that I don't know how to access the store in order to be able to call store.dispatch. (which was my previous question noted above)
i cannot leave a comment so a question: why dont you just set mapState and pass the state as an argument to dispatched action?
Here are Component stuff
class AComponent extends Component{
...
... onClick={()=>this.props.anAction(this.props.state)}
}
const mapStateToProps = state => {
return { state: state.YourReducer }
}
const mapDispatchToProps = dispatch => {
return { anAction: () => dispatch(actions.doAnyStaffWith(state)) }
}
export default connect(mapStateToProps, mapDispatchToProps)(BeerReviewer)
actions file:
export function actionThree(state) {
return { type: ACTION_TYPE, state }
}
That's what you're looking for?
You'll need thunk in order to enhance your action creators. the reason why you're getting the "must be plain objects" error is because your action creator is returning a function(), not an object. Thunk allows you to return functions in your action creators and with it, the code you wrote should work.
import { createStore, combineReducers, applyMiddleware } from "redux"
import thunk from "redux-thunk"
const store = createStore(combineReducers({
data1: reducer1,
data2: reducer2
}), {}, applyMiddleware(thunk))
Hope that works.
You should be able to access your store like this (illustrating with a fetch request):
import { someAPICALL } from '../api'
export function setCurrentUser(username) {
return async (dispatch, getState) => {
const { yourStateVariable } = getState().YourReducer
, data = await someAPICall(yourStateVariable)
.catch(e => dispatch({ type: FETCH_ERROR, payload: e }))
if (data) {
dispatch( { type: SET_CURRENT_USER, payload: username } );
dispatch( { type: ACTION_TWO, payload: username } );
dispatch( { type: ACTION_THREE, payload: username } );
} else {
dispatch( { type: SOME_OTHER_ACTION, payload: 'whatever state update you like' } )
}
}
}
Redux thunk would be the ideal way to do it but you can still chain actions by returning promises as your payload.(If you do not want to use thunk)
export function setCurrentUser(username) {
return {
type: SET_CURRENT_USER,
payload: new Promise(resolve => resolve(username))
}
}
export function actionTwo(username) {
return {
type: ACTION_TWO,
payload: new Promise(resolve => resolve(username))
}
}
export function actionThree(username) {
return {
type: ACTION_THREE,
payload: new Promise(resolve => resolve(username))
}
}
And then you can call all three like:
this.props.setCurrentUser(this.state.newUsername)
.then(newUsername => this.props.actionTwo(newUsername))
.then(newUsername => this.props.actionThree(newUsername))
I think you mapDispatchToProps function should look like this: const mapDispatchToProps = {
setCurrentUser: (data) => dispatch(setCurrentUser(data)),
}. Regarding accessing store from your action creators, you need to export it from where you declared it(index.js). import it to your actionCreators file and then use it there. It goes as follows:
in index.js file: export const store = createStore(...) and in your actionCreators file: import { store } from "your index.js file's path" and you are good to go. Hope that solves your problem.
Sorry very common error, but in the test below don't understand how the offending action storeMgrAnnouncement(result) is not a plain object. The api call and the thunk action are mocked, but the offending action isn't.
home.test.js
/* not showing 3rd party imports */
import Home from '../../components/home';
import {getMgrAnnouncement} from "../../__mocks__/actions";
import {STORE_MGR_ANNOUNCEMENT} from "../../constants";
import {success} from "../../__fixtures__/announcementGet";
const mockStore = configureStore([thunk]);
describe('Home Page', () => {
var store = null;
const initialState = {};
beforeEach(() => {
store = mockStore(initialState);
shallow(<Home store={store} />);
});
it ('should store manager announcement after retrieving it', async () => {
await store.dispatch(getMgrAnnouncement());
expect(store.getActions()).toContainEqual({
type: STORE_MGR_ANNOUNCEMENT,
payload: success
});
});
__mocks__/actions/index.js
import { storeMgrAnnouncement } from '../../actions';
import { success } from '../../__fixtures__/announcementGet';
/* mock api call */
function announcementGet() {
return new Promise((resolve, reject) => {
process.nextTick(() => {
resolve(success)
})
})
}
/* mock thunk action */
export function getMgrAnnouncement() {
return function(dispatch, getState) {
return announcementGet()
.then(result => {
/*
ERROR: Actions must be plain objects. Use custom middleware for async actions.
*/
dispatch(storeMgrAnnouncement(result));
})
}
}
actions/index.js
import { STORE_MGR_ANNOUNCEMENT } from '../../constants';
export function storeMgrAnnouncement(result) {
return {
type: STORE_MGR_ANNOUNCEMENT,
payload: result
}
}
You are dispatching the result of getMgrAnnouncement()
store.dispatch(getMgrAnnouncement())
But that function returns a function:
function getMgrAnnouncement() {
return function(dispatch, getState) {
You must dispatch an object, not a function.
I can't lie, i'm a bit confused about react-redux. I think lot of the actions require parameters (for an example deleting items from the store), but even if i'm still reading about how to dispatch from component in that way to pass a parameter, about 2 hours now, i didn't get any answers. I was tried with this.props.dispatch and with mapDispatchToProps, i always get the this.props... is not a function message. Here's what i'm trying to do:
const getTheArray = (array) => {
return {
type: 'GET_ARRAY',
array
}
}
class Example extends......
componentDidUpdate(){
//i have a button, and when it clicked, turns the status: 'deleted'
if (this.state.status === 'deleted'){
fetch('http://localhost:3001/deleteitem', {
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: this.props.id
})
})
.then(res => res.json())
.then(data => this.props.deleteFromArray(data))
.catch(err => console.log(err))
}
}
function mapStateToProps(state) {
return {
array: state.array
};
}
function mapDispatchToProps(dispatch) {
return({
deleteFromArray: (array) => {dispatch(getTheArray(array))}
})
}
export default connect(mapStateToProps, mapDispatchToProps)(Example);
It isn't the only place where i need to dispatch with an action, where the action's object's properties depending on another property passed to the function, so i really want to do, what's the best way to pass property to action, and dispatch it within a react component.
import { bindActionCreators } from "redux"
function mapDispatchToProps(dispatch) {
return(bindActionCreators({
deleteFromArray: (array) => {getTheArray(array)}
}, dispatch))
}
In your dumbComponents, just call e.g this.props.deleteFromArray([1,2,3,4])
This should solve the issue. you are not binding with dispatch
use of react-redux
you need to import action first
import { storeProfiles } from 'actions/profiles';
define props
const {
storeProfiles
} = props;
get data use of userEffect
useEffect(() => {
fetchProfiles().then(storeProfiles);
}, [ fetchProfiles, storeProfiles ]);
map to props
const mapDispatchToProps = dispatch => ({
storeProfiles: x => dispatch(storeProfiles(x))
});
export default connect(mapStateToProps, mapDispatchToProps)(Component);
Read official documentation
Using es6:
const mapDispatchToProps = (dispatch) => ({ addToCart: (params) => dispatch(actions.addToCart(params)), });
Actually, you don't have to pass params to actions like that, in my opinion.
I'm using React Hooks with TypeScript and Redux and thunk. The way I delete a post using an action creator looks like this:
PostDetails.tsx component:
import {deletePost, getPost, LanguageActionTypeEnum} from "../../../../app/store/aspian-core/actions";
interface IPostDetailsProps {
getPost: Function;
// action for deleting a post
deletePost: Function;
loadingInitial: boolean;
submitting: boolean;
post: IPost | undefined;
lang: LanguageActionTypeEnum;
}
const PostDetails: FC<PostDetailsProps> = ({
match, getPost,
lang, loadingInitial,
submitting, deletePost, post
}) => {
}
// To delete a post
const onDeleteBtnClick = async (id: string) => {
try {
/// using deletePost action and passing id to it
await deletePost(id);
message.success(t('messages.post-deleting-success'));
} catch (error) {
message.error(t('messages.post-deleting-error'));
}
};
return (
// omitted for brevity
<Popconfirm
key={uuidv4()}
title={t('popconfirm.title')}
/// calling onDeleteBtnClick() here
onConfirm={() => onDeleteBtnClick(post!.id)}
okText={t('popconfirm.ok-text')}
cancelText={t('popconfirm.cancel-text')}
placement={lang === LanguageActionTypeEnum.en ? 'left' : 'right'}
okButtonProps={{
danger: true,
}}
>
<Button
loading={submitting}
size="small"
type="primary"
icon={<DeleteFilled/>}
danger
>
{t('buttons.delete')}
</Button>
</Popconfirm>
// omitted for brevity
)
}
// Redux State To Map
const mapStateToProps = ({post, locale}: IStoreState): { post: IPost | undefined, submitting: boolean, loadingInitial: boolean, lang: LanguageActionTypeEnum } => {
return {
post: post.post,
submitting: post.submitting,
loadingInitial: post.loadingInitial,
lang: locale.lang
}
}
// Redux Dispatch To Map
const mapDispatchToProps = {
getPost,
// mapped deletePost action to props here
// and I will be able to pass id to its argument later
deletePost
}
export default connect(mapStateToProps, mapDispatchToProps)(PostDetails);
and my the action creator looks like this:
postActions.ts:
export const deletePost = (id: string) => async (dispatch: Dispatch) => {
try {
dispatch(setPostSubmitting(true));
await agent.Posts.delete([id]);
dispatch<IDeletePostAction>({
type: PostActionTypes.DELETE_SINGLE_POST,
payload: {
id,
submitting: false
}
})
} catch (error) {
console.log(error);
dispatch(setPostSubmitting(false));
throw error;
}
}
And it works fine. Hopefully it could help you with your situation.
If you use actions as arrow function so there would be no need to bindActionsCreators, let me do it with a very simple way.
your action should be of arrow function like so:
export const formHandler=({key,value})=>{
/*for checking that you get the values on function call or not*/
console.log(key,value)
return {
type:'formHandler',
payload:{key,value},
}
}
the below action function takes to arguments so you have to pass likewise
const mapDispatchToProps = dispatch=> {
/*this will add the functions as a props to the components
which can be trigger on order to change the state*/
return {
inputHandler: ({key,value})=>dispatch(formHandler({key,value}))
}
}
at the end you can use connect on yourComponent
export default connect(mapStatesToProps,mapDispatchToProps)(yourComponent)
then in your component
you can call the function using
class component
this.props.inputHandler({key:'k',value:2})
functional component
props.inputHandler({key:'k',value:2})
I'm trying to make some middleware which refreshes my JWT if it's expired. I currently have this middleware:
import { isTokenExpired, refreshToken } from '../modules/auth/auth.service'
export const jwt = store => next => action => {
if (typeof action === 'function') {
console.log("Middleware triggered:", action);
var theState = store.getState();
if (theState.auth && theState.auth.authToken && isTokenExpired(theState.auth.authToken)) {
console.log('This is logging')
//this function is not being called
refreshToken(theState.auth.refreshToken);
}
}
return next(action)
}
I wish to call the refreshToken() function which will start the process, but so far even the console.log() inside the refreshToken is not calling:
export const refreshToken = (refreshToken) => dispatch => {
console.log('Not logging')
}
Also, inside the refreshToken function will be asynchronous while it refreshes the token. I was thinking of setting a "REFRESH_TOKEN_PENDING" type in the reducer, and once the response is received send "REFRESH_TOKEN_SUCCESS" to the reducer. Will this be possible with the middleware?
Thanks
You have to dispatch refreshToken(). If you do have access to dispatch() into jwt function you should call it like dispatch(refreshToken()).
Considering that I'm not sure how you are triggering the jwt action, I think this is a valid solution of how you can easily trigger middleware functions inside actions:
// ================
// Container file
// ================
import {connect} from "react-redux";
import { bindActionCreators } from "redux";
// ... other
function mapActionsToProps(dispatch) {
return bindActionCreators({
jwt: path_to_actions_file.jwt
}, dispatch)}
const mapStateToProps = (state) => ({
// ... your state properties
});
export default connect(mapStateToProps, mapActionsToProps)(YourComponent); // <== YourComponent has to be a React Component
// ==============
// Actions file
// ==============
export const setConvertTypeActive = store => dispatch => {
console.log('This is logging.');
dispatch(refreshToken("md5Token"));
}
export const refreshToken = refreshToken => dispatch => {
console.log('Should log now.');
}
And regarding your last question, yeah, you can create middlewares similar to refreshToken action for setting some statuses on reducer. Check the code below:
// ==============
// Actions file
// ==============
export const refreshToken= {
pending: () => ({
// "REFRESH_TOKEN_PENDING"
}),
success: (data) => ({
// "REFRESH_TOKEN_SUCCESS"
})
};